«

Golang 并发编程goroutine

时间:2025-1-10 15:09     作者:杨佳乐     分类: Golang


package main

import "fmt"

// 学习并发前应了解的一些基本内容

// 操作系统基础:进程与线程
// 进程(Process):一个进程是操作系统中正在执行的程序。每个进程都有自己的内存空间、代码和数据。进程是操作系统进行资源管理的基本单位。
// 线程(Thread):线程是进程中的一个执行单元,一个进程可以有多个线程。线程共享进程的资源,但每个线程有自己的执行栈。线程是操作系统调度的基本单位。
// 多线程和多进程:多进程是指操作系统同时运行多个独立的进程,而多线程是指在一个进程内部运行多个线程。Go 中的 goroutines 是一种轻量级线程,与操作系统线程类似但更轻量。
// 上下文切换:操作系统会根据一定的调度算法,暂停当前进程或线程的执行,并切换到另一个进程或线程的执行,这个过程称为上下文切换。

// 并发和并行
// 并发(Concurrency):并发指的是程序中有多个任务正在进行,但是它们可能并不同时执行。不同任务之间通过切换执行来模拟“同时”进行。
// 并行(Parallelism):并行指的是多个任务真正同时执行。并行通常发生在多核处理器上,多个任务在不同的 CPU 核心上同时执行。
// 你可以将 并发 想象成同时处理多个任务,但每次只做其中一个任务,通过快速切换来产生“同时”的效果。Go 的 goroutine 是一种并发模型,它可以非常高效地处理成千上万的任务。

// 并发编程
func main(){

    // Go 的并发编程非常强大,它通过 goroutines 和 channels 来帮助开发者轻松处理并发任务。
    // Go 的并发模型基于 CSP(Communicating Sequential Processes)理论,通过通信而不是共享内存来处理并发。

    // Goroutines
    // Go 中的并发单元是 goroutine,它是一个轻量级的线程。goroutine 通过 go 关键字启动,Go 的调度器会自动管理多个 goroutine 的执行

    // 启动一个新的 goroutine
    go printMessage()

    // 主线程等待 goroutine 完成
    // fmt.Scanln()

    // go printMessage() 启动了一个新的 goroutine,这个函数会并发执行。

    // 主函数 fmt.Scanln() 用于阻塞,等待用户输入,确保 goroutine 可以有足够的时间执行。

    // Channels
    // Channel 是 Go 提供的一个强大的并发原语,允许 goroutines 之间进行数据传递。Go 中的 Channel 可以是有缓冲的或无缓冲的。

    // 无缓冲 Channel
    // 无缓冲 Channel 是一种同步的通信方式,当一个 goroutine 向 Channel 发送数据时,另一个 goroutine 必须接收这个数据才能继续。

    ch := make(chan int)

    go sender(ch)

    // 接收数据并打印
    data := <-ch
    fmt.Println("Received data:", data)

    // 有缓冲Channel
    // 有缓冲 Channel 允许多个数据被发送到 Channel 中,而不需要立即被接收。它允许一定数量的缓冲区

    chs := make(chan int, 3) // 创建一个缓冲区为 3 的 Channel

    go senders(chs)

    for i := 0; i < 5; i++ {
        datas := <-chs
        fmt.Println("Received:", datas)
    }
    //缓冲 Channel ch := make(chan int, 3) 允许最多存储 3 个整数。
    //发送者可以发送多个数据而不需要立即被接收。

    // 关闭Channel
    // 当所有的发送操作完成后,可以关闭 Channel。关闭 Channel 使得接收者能够知道不再有更多数据。
    // func sender(ch chan int) {
    //  for i := 0; i < 3; i++ {
    //      ch <- i
    //  }
    //  close(ch) // 关闭 Channel
    // }

    // func main() {
    //  ch := make(chan int)

    //  go sender(ch)

    //  for data := range ch {
    //      fmt.Println("Received:", data)
    //  }
    // }
    // close(ch) 关闭 Channel,表示不再有数据发送到该 Channel。
    // 使用 for range 循环来接收数据,直到 Channel 被关闭

    // Select 语句
    // select 语句允许你等待多个 Channel 的操作。它会选择一个可用的 Channel 操作执行

    // func sender(ch chan int) {
    //  ch <- 42
    // }

    // func main() {
    //  ch1 := make(chan int)
    //  ch2 := make(chan int)

    //  go sender(ch1)
    //  go sender(ch2)

    //  select {
    //  case data := <-ch1:
    //      fmt.Println("Received from ch1:", data)
    //  case data := <-ch2:
    //      fmt.Println("Received from ch2:", data)
    //  }
    // }

    //select 等待多个 Channel 中的一个操作完成,执行对应的 case 语句。
    //这个例子中,select 会从 ch1 或 ch2 中接收数据,并执行相应的打印操作。

    // WaitGroup
    // WaitGroup 用于等待一组 goroutine 执行完成。它通常用于等待并发任务完成
    // import (
    //  "fmt"
    //  "sync"
    // )

    // func task(id int, wg *sync.WaitGroup) {
    //  defer wg.Done() // 告诉 WaitGroup 任务已完成
    //  fmt.Println("Task", id, "started")
    // }

    // func main() {
    //  var wg sync.WaitGroup

    //  for i := 1; i <= 5; i++ {
    //      wg.Add(1) // 每启动一个 goroutine,就增加计数
    //      go task(i, &wg)
    //  }

    //  wg.Wait() // 等待所有 goroutines 完成
    //  fmt.Println("All tasks finished.")
    // }
    // wg.Add(1) 增加等待计数,每启动一个 goroutine 都调用。
    // defer wg.Done() 在每个任务完成时减少计数。
    // wg.Wait() 等待所有任务完成。

    // Worker Pool 模式
    // 在实际应用中,常常需要将任务分配给多个 worker goroutine 来处理,这就是 worker pool 模式

    // import (
    //  "fmt"
    //  "sync"
    // )

    // func worker(id int, ch chan int, wg *sync.WaitGroup) {
    //  defer wg.Done()
    //  for task := range ch {
    //      fmt.Printf("Worker %d processing task %d\n", id, task)
    //  }
    // }

    // func main() {
    //  var wg sync.WaitGroup
    //  ch := make(chan int, 10)

    //  // 启动 3 个 worker
    //  for i := 1; i <= 3; i++ {
    //      wg.Add(1)
    //      go worker(i, ch, &wg)
    //  }

    //  // 发送任务
    //  for task := 1; task <= 10; task++ {
    //      ch <- task
    //  }

    //  close(ch) // 关闭 Channel,表示没有更多任务
    //  wg.Wait()  // 等待所有 worker 完成
    // }

    // 这里有 3 个 worker,每个 worker 从 Channel 中获取任务进行处理。
    // 通过 close(ch) 关闭 Channel 来通知 worker 没有更多任务。

    // 并发调试

    // Go 提供了 -race 标志来检测并发代码中的数据竞争问题。你可以使用它来运行程序并查找潜在的并发问题。
    // -race 标志会检测并发访问共享数据时的竞争条件,帮助你找出代码中的潜在问题。

    // 总结
    // Go 的并发编程非常强大,核心原语是 goroutines 和 channels。你可以通过以下步骤提升并发编程能力:

    // 理解 goroutines 和它们如何启动。
    // 学习如何通过 channels 在 goroutines 之间传递数据。
    // 使用 select 语句在多个 channels 之间进行选择。
    // 利用 WaitGroup 来同步多个 goroutines。
    // 掌握 worker pool 模式来处理并发任务。
    // 调试并发代码,确保没有数据竞争问题。

}

func senders(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("Sent:", i)
    }
}

func sender(ch chan int) {
    ch <- 42
    fmt.Println("Sent data")
}

func printMessage(){
    fmt.Println("Hello from Goroutines")
}

// 更通俗简单的理解go 并发
// 什么是并发?
// 想象一下,你正在做一份工作报告,而你有两个任务:

// 任务 1:查找资料。
// 任务 2:整理内容。
// 在传统的工作模式下,你可能会先做任务 1,然后再做任务 2,这叫做 顺序执行。但假设你现在有两个人,你可以让一个人做任务 1,另一个人做任务 2,这样你可以 并行 完成这两个任务。

// 并发 是指我们有多个任务,它们可能并不会真正同时执行,但它们的执行是交替进行的,就像你在忙碌的时刻,切换任务一样。并行则是任务在不同的 CPU 核心上同时执行。

// 在 Go 中,虽然我们通常是通过 goroutine 来实现并发,但很多时候我们只是希望它们看起来像是“同时”运行的。

// 什么是 Goroutine?
// Goroutine 是 Go 中的轻量级线程,它和操作系统的线程不一样。你可以把 goroutine 想象成你在做任务时的“临时助手”。你给它一个任务,它就开始工作。

// 启动一个 goroutine:通过 go 关键字,你可以启动一个新的 goroutine。每个 goroutine 都是并发执行的。
// 举例
// func sayHello() {
//     fmt.Println("Hello, Goroutine!")
// }

// func main() {
//     // 启动一个新的 goroutine
//     go sayHello()

//     // 主线程继续执行
//     fmt.Println("Hello from main!")
// }

// 这里,go sayHello() 启动了一个新的 goroutine,它会和 main 函数一起并发执行。你会看到输出顺序是随机的,因为 sayHello 和 main 是并发运行的。

// Goroutine 是如何工作的?
// Go 的 goroutine 非常轻量,启动一个 goroutine 的成本非常低,可能只需要几 KB 的内存。
// 它们由 Go 运行时调度和管理,你不需要显式地创建或销毁 goroutine。
// 你可以理解为:在 Go 中,启动一个新的任务(goroutine)就像给你指派一个新的助手,而你无需关心每个助手的具体管理细节。

// 什么是 Channel?
// Channel 是 Go 中用于在 goroutines 之间传递信息的通道。你可以把它理解为“邮递员”,它负责把消息从一个 goroutine 传递到另一个 goroutine。

// 假设你有两个任务:

// 任务 1:从一堆数字中筛选出偶数。
// 任务 2:将筛选出的偶数做一些加法处理。
// 你不想让任务 1 和任务 2 直接互相操作,而是希望它们通过传递信息的方式进行合作。

// 示例
// func filterEven(numbers []int, ch chan int) {
//     for _, num := range numbers {
//         if num%2 == 0 {
//             ch <- num // 发送偶数到 channel
//         }
//     }
//     close(ch) // 关闭 channel,表示任务结束
// }

// func addNumbers(ch chan int) {
//     for num := range ch {
//         fmt.Println("Adding number:", num+1)
//     }
// }

// func main() {
//     numbers := []int{1, 2, 3, 4, 5, 6}
//     ch := make(chan int) // 创建一个 channel

//     // 启动两个 goroutine
//     go filterEven(numbers, ch)
//     go addNumbers(ch)

//     // 主线程等待 goroutines 执行完成
//     fmt.Scanln()
// }

// filterEven 任务会筛选出偶数,并通过 ch <- num 把偶数发送到 channel。
// addNumbers 任务会从 ch 中接收偶数,并在其基础上加 1 后打印出来。
// Channel 在这里充当了一个 消息传递系统,确保两个 goroutine 之间的信息能够正确传递。

// Go 中的并发调度:如何协作?
// Go 的调度器会将多个 goroutine 安排到有限的 CPU 核心上执行。Go 的并发执行并不意味着所有 goroutines 会真正“同时”运行。实际上,Go 会在不同的时刻给 goroutine 分配执行的时间片,快速切换它们的执行。你可以理解为:在一个 CPU 核心上,Go 的调度器会以非常高的速度在多个 goroutines 之间切换任务。

// 核心概念:Go 的并发调度是通过 抢占式调度 来管理的,而不像传统操作系统中那样使用重量级线程。

// 如何用 Select 同时处理多个 Channel?
// 假设你有多个任务需要同时处理,但它们的数据需要通过不同的 channel 传递。Go 提供了 select 语句,它可以让你同时监听多个 channel,哪个 channel 有数据,它就会处理哪个。

// func sender(ch chan string) {
//     ch <- "Hello from sender!"
// }

// func main() {
//     ch1 := make(chan string)
//     ch2 := make(chan string)

//     go sender(ch1)
//     go sender(ch2)

//     select {
//     case msg1 := <-ch1:
//         fmt.Println("Received from ch1:", msg1)
//     case msg2 := <-ch2:
//         fmt.Println("Received from ch2:", msg2)
//     }
// }

// 这里,select 语句会同时监听 ch1 和 ch2,哪个 channel 先有数据,它就会接收并执行对应的 case。

// 注意,select 可以立即为一次性的,一个select只会执行一次case ,比如同时AB两个任务,select 监听AB,A收到数据select中case会执行,B收到数据不会执行case

// 总结
// 并发编程的核心思想就是让多个任务并行或者交替地执行。Go 中通过 goroutine 来实现并发,通过 channel 在 goroutines 之间传递数据。这些机制让 Go 编程语言非常适合进行并发编程。理解并发的关键是:

// goroutine:让任务并发执行。
// channel:让 goroutines 之间进行通信。
// select:处理多个 channel 操作。