写点什么

Golang 并发控制方式有几种?

作者:EquatorCoco
  • 2024-01-29
    福建
  • 本文字数:3991 字

    阅读完需:约 13 分钟

Go 语言中的 goroutine 是一种轻量级的线程,其优点在于占用资源少、切换成本低,能够高效地实现并发操作。但如何对这些并发的 goroutine 进行控制呢?


一提到并发控制,大家最先想到到的是锁。Go 中同样提供了锁的相关机制,包括互斥锁sync.Mutex和读写锁sync.RWMutex;除此之外 Go 还提供了原子操作sync/atomic。但这些操作都是针对并发过程中的数据安全的,并不是针对 goroutine 本身的。


本文主要介绍的是对 goroutine 并发行为的控制。在 Go 中最常见的有三种方式:sync.WaitGroupchannel Context


1. sync.WaitGroup


sync.WaitGroup 是 Go 语言中一个非常有用的同步原语,它可以帮助我们等待一组 goroutine 全部完成。在以下场景中,我们通常会使用 sync.WaitGroup:


  • 当我们需要在主函数中等待一组 goroutine 全部完成后再退出程序时。

  • 当我们需要在一个函数中启动多个 goroutine,并确保它们全部完成后再返回结果时。

  • 当我们需要在一个函数中启动多个 goroutine,并确保它们全部完成后再执行某个操作时。

  • 当我们需要在一个函数中启动多个 goroutine,并确保它们全部完成后再关闭某个资源时。

  • 当我们需要在一个函数中启动多个 goroutine,并确保它们全部完成后再退出循环时。


在使用sync.WaitGroup时,我们需要先创建一个sync.WaitGroup对象,然后使用它的Add方法来指定需要等待的 goroutine 数量。接着,我们可以使用 go 关键字来启动多个 goroutine,并在每个 goroutine 中使用sync.WaitGroup对象的Done方法来表示该 goroutine 已经完成。最后,我们可以使用sync.WaitGroup对象的Wait方法来等待所有的 goroutine 全部完成。


下面是一个简单的示例,会启动 3 个 goroutine,分别休眠 0s、1s 和 2s,主函数会在这 3 个 goroutine 结束后退出:


package main import (	"fmt"	"sync"	"time") func main() {	var wg sync.WaitGroup 	for i := 0; i < 3; i++ {		wg.Add(1)		go func(i int) {			defer wg.Done()			fmt.Printf("sub goroutine sleep: %ds\n", i)			time.Sleep(time.Duration(i) * time.Second)		}(i)	} 	wg.Wait()	fmt.Println("main func done")}
复制代码


2. channel


在 Go 语言中,使用 channel 可以帮助我们更好地控制 goroutine 的并发。以下是一些常见的使用 channel 来控制 goroutine 并发的方法:


2.1 使用无缓冲 channel 进行同步


我们可以使用一个无缓冲的 channel 来实现生产者-消费者模式,其中一个 goroutine 负责生产数据,另一个 goroutine 负责消费数据。当生产者 goroutine 将数据发送到 channel 时,消费者 goroutine 会阻塞等待数据的到来。这样,我们可以确保生产者和消费者之间的数据同步。


下面是一个简单的示例代码:


package main import (    "fmt"    "sync"    "time") func producer(ch chan int, wg *sync.WaitGroup) {    defer wg.Done()    for i := 0; i < 10; i++ {        ch <- i        fmt.Println("produced", i)        time.Sleep(100 * time.Millisecond)    }    close(ch)} func consumer(ch chan int, wg *sync.WaitGroup) {    defer wg.Done()    for i := range ch {        fmt.Println("consumed", i)        time.Sleep(150 * time.Millisecond)    }} func main() {    var wg sync.WaitGroup    ch := make(chan int)     wg.Add(2)    go producer(ch, &wg)    go consumer(ch, &wg)     wg.Wait()}
复制代码


在这个示例中,我们创建了一个无缓冲的 channel,用于在生产者 goroutine 和消费者 goroutine 之间传递数据。生产者 goroutine 将数据发送到 channel 中,消费者 goroutine 从 channel 中接收数据。在生产者 goroutine 中,我们使用 time.Sleep 函数来模拟生产数据的时间,在消费者 goroutine 中,我们使用 time.Sleep 函数来模拟消费数据的时间。最后,我们使用 sync.WaitGroup 来等待所有的 goroutine 全部完成。


2.2 使用有缓冲 channel 进行限流


我们可以使用一个有缓冲的 channel 来限制并发 goroutine 的数量。在这种情况下,我们可以将 channel 的容量设置为我们希望的最大并发 goroutine 数量。然后,在启动每个 goroutine 之前,我们将一个值发送到 channel 中。在 goroutine 完成后,我们从 channel 中接收一个值。这样,我们可以保证同时运行的 goroutine 数量不超过我们指定的最大并发数量。


下面是一个简单的示例代码:


package main import (    "fmt"    "sync") func main() {    var wg sync.WaitGroup    maxConcurrency := 3    semaphore := make(chan struct{}, maxConcurrency)     for i := 0; i < 10; i++ {        wg.Add(1)        go func() {            defer wg.Done()            semaphore <- struct{}{}            fmt.Println("goroutine", i, "started")            // do some work            fmt.Println("goroutine", i, "finished")            <-semaphore        }()    }     wg.Wait()}
复制代码


在这个示例中,我们创建了一个带缓冲的 channel,缓冲区大小为 3。然后,我们启动了 10 个 goroutine,在每个 goroutine 中,我们将一个空结构体发送到 channel 中,表示该 goroutine 已经开始执行。在 goroutine 完成后,我们从 channel 中接收一个空结构体,表示该 goroutine 已经完成执行。这样,我们可以保证同时运行的 goroutine 数量不超过 3。


3. Context


在 Go 语言中,使用 Context 可以帮助我们更好地控制 goroutine 的并发。以下是一些常见的使用 Context 来控制 goroutine 并发的方法:


3.1 超时控制


在某些情况下,我们需要对 goroutine 的执行时间进行限制,以避免程序长时间阻塞或者出现死锁等问题。使用 Context 可以帮助我们更好地控制 goroutine 的执行时间。我们可以创建一个带有超时时间的 Context,然后将其传递给 goroutine。如果 goroutine 在超时时间内没有完成执行,我们可以使用 Context 的 Done 方法来取消 goroutine 的执行。


下面是一个简单的示例代码:


package main import (    "context"    "fmt"    "time") func main() {    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)    defer cancel()     go func() {        for {            select {            case <-ctx.Done():                fmt.Println("goroutine finished")                return            default:                fmt.Println("goroutine running")                time.Sleep(500 * time.Millisecond)            }        }    }()     time.Sleep(3 * time.Second)}
复制代码


在这个示例中,我们创建了一个带有超时时间的 Context,然后将其传递给 goroutine。在 goroutine 中,我们使用 select 语句来监听 Context 的 Done 方法,如果 Context 超时,我们将会取消 goroutine 的执行。


3.2 取消操作


在某些情况下,我们需要在程序运行过程中取消某些 goroutine 的执行。使用 Context 可以帮助我们更好地控制 goroutine 的取消操作。我们可以创建一个带有取消功能的 Context,然后将其传递给 goroutine。如果需要取消 goroutine 的执行,我们可以使用 Context 的 Cancel 方法来取消 goroutine 的执行。


下面是一个简单的示例代码:


package main import (    "context"    "fmt"    "sync"    "time") func main() {    ctx, cancel := context.WithCancel(context.Background())     var wg sync.WaitGroup    wg.Add(1)    go func() {        defer wg.Done()        for {            select {            case <-ctx.Done():                fmt.Println("goroutine finished")                return            default:                fmt.Println("goroutine running")                time.Sleep(500 * time.Millisecond)            }        }    }()     time.Sleep(2 * time.Second)    cancel()    wg.Wait()}
复制代码


在这个示例中,我们创建了一个带有取消功能的 Context,然后将其传递给 goroutine。在 goroutine 中,我们使用 select 语句来监听 Context 的 Done 方法,如果 Context 被取消,我们将会取消 goroutine 的执行。在主函数中,我们使用 time.Sleep 函数来模拟程序运行过程中的某个时刻需要取消 goroutine 的执行,然后调用 Context 的 Cancel 方法来取消 goroutine 的执行。


3.3 资源管理


在某些情况下,我们需要对 goroutine 使用的资源进行管理,以避免资源泄露或者出现竞争条件等问题。使用 Context 可以帮助我们更好地管理 goroutine 使用的资源。我们可以将资源与 Context 关联起来,然后将 Context 传递给 goroutine。当 goroutine 完成执行后,我们可以使用 Context 来释放资源或者进行其他的资源管理操作。


下面是一个简单的示例代码:


package main import (    "context"    "fmt"    "sync"    "time") func worker(ctx context.Context, wg *sync.WaitGroup) {    defer wg.Done()    for {        select {        case <-ctx.Done():            fmt.Println("goroutine finished")            return        default:            fmt.Println("goroutine running")            time.Sleep(500 * time.Millisecond)        }    }} func main() {    ctx, cancel := context.WithCancel(context.Background())     var wg sync.WaitGroup    wg.Add(1)    go worker(ctx, &wg)     time.Sleep(2 * time.Second)    cancel()    wg.Wait()}
复制代码


在这个示例中,我们创建了一个带有取消功能的 Context,然后将其传递给 goroutine。在 goroutine 中,我们使用 select 语句来监听 Context 的 Done 方法,如果 Context 被取消,我们将会取消 goroutine 的执行。在主函数中,我们使用 time.Sleep 函数来模拟程序运行过程中的某个时刻需要取消 goroutine 的执行,然后调用 Context 的 Cancel 方法来取消 goroutine 的执行。


文章转载自:落雷

原文链接:https://www.cnblogs.com/lianshuiwuyi/p/17991829

体验地址:http://www.jnpfsoft.com/?from=001

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Golang并发控制方式有几种?_Go_EquatorCoco_InfoQ写作社区