Online Go tutorial(2) - goroutine / channel / select / Mutex

  Go 对协程的支持使其对于并发编程有天生的优势,标准库提供了原生的协程支持( goroutine ),协程间的通信与竞争保护工具( channel 和 sync.Metux )以及类似 C/C++ 编程中的多路复用工具 select 关键字。这里根据 Online Go tutorial 做的简单记录,适合进行备忘。

 

  goroutine

  Go 中通过 go 关键字实现协程的创建,标准的使用方法为 go + 函数调用。其中,函数调用的参数等信息在当前协程中计算和获得,而具体函数的执行流程则在新的协程中进行。与一般的协程定义一致,go 中多个协程共享地址空间等进程资源,同时由 go runtime 进行调度,故而在访问共享内存时需要资源竞争保护。

    go f( x, y, z )            // 在新的协程中执行函数 f,对 x, y, z 的求值在当前协程中进行

  注意,在 go 中,当 main 函数执行结束时,程序即直接退出,而不会等待其他 goroutine 的执行完成,故而在编写简单的并发代码时需要注意。常用的并发模型包括通过 channel 等待其他 go 协程,通过 sync 中的 groupwait 关键字等待一组 goroutine 的完成等。

 

  channel

  Go 中的 channel 是一种可以通过 channel 运算符 <- 进行数据发送和接收操作的命名管道。每个 channel 在使用前都必须通过 make 函数进行创建,每个 channel 的类型为 chan T。在创建 channel 时可以同时指定长度参数,指定 channel 最多可以存放的数据长度。

    ch := make(chan int)            // 创建处理 int 类型的 channel
    ch := make(chan int, 100)    // 创建处理 int 类型的 channel,其长度为 100

  在创建完成之后,通过 channel 运算符进行数据处理。使用 channel 操作时,channel 的接收方在没有数据时以及 channel 的发送方在 channel 为满时会阻塞,这某种程度上简化了同步操作流程,如可以通过等待 channel 信息等待其他 goroutine 的结束。

    ch <- v
    v := <- ch        // 注意 channel 运算符只有一种,其方向即为数据流动方向

  channel 支持发送方对 channel 的关闭以及接收方对 channel 是否关闭进行检测。对于数据发送方,可以通过 close 函数关闭 channel,表示后续不再发送数据。对于数据的接收放,可以在接收数据时通过额外的第二个参数判断 channel 是否被关闭,注意若 channel 已被关闭时,接收方获得的数据为该 channel 类型的默认零值。for 循环的 range 语法也支持对 channel 的操作,其会循环直到 channel 中的数据被完全接收。

    close( ch )            // 关闭 channel
    v, flag := <- ch       // 通过 flag 的返回值确定 channel 是否被关闭,flag 为 false 时表示已关闭
    for i := range ch {    // range 语句会循环 ch 中所有的数据,注意此时若 channel 没有被关闭,for 循环会保持等待...
     // statements   
    }

  Google I/O 中有一些关于 channel 编程的技巧,如 Google I/O 2012 - Go Cocurrency Pattern

 

  select

  Go 中的 select 操作通过 select 和 case 语句实现,其中 case 语句由数据操作组成( 如 channel 操作 ),当其中某个数据操作可以进行时,则会执行 case 语句对应的执行体,若多个 case 语句可以执行时,select 会任选一个 case 语句进行执行。select 语句也可以加入 default 语句,当没有 case 符合执行条件时,会执行对应的 default 语句。

    select {
     case ch1 <- x:    // 可以向 ch1 中写入数据时,该 case 语句被执行( 注意此时 x 会被写入 ch1 )
        // statements
     case x <- ch2:    // 可与向 ch2 中读出数据时,该 case 语句被执行( 注意此时 x 会读取 ch2 中的值 )
        // statements
    default:                // 没有 case 符合执行情况时,default 被执行
        // statements
    }

 

  sync

  在基础的 Go 语法层面的支持之外,Go 通过 sync 标准库提供了诸如锁,条件变量以及 wait 操作等,具体可以可以参考官方的文档 sync package。这里做一个简单的记录。

  sync.Mutex

  Go 中除了支持 channel 进行协程间的数据交互外,还可以通过 sync.Mutex 来提供竞争保护。标准库的 sync.Mutex 类型实现了 Lock() 与 Unlock() 接口来完成加锁和解锁操作。  

    var mu sync.Mutex  // 定义一个锁类型变量
    mu.Lock()        // 加锁操作
    mu.Unlock()    // 解锁操作

 

  sync.WaitGroup

  Go 中也提供了如 C/C++ 中的 waitpid 原语类似的接口,用于主协程等待其他协程的结束( 注意 Go 程序在 main 函数返回后即退出,默认不会等待其他协程的运行 )。Go 提供的操作主要基于 sync 包提供的 WaitGroup 类型的方法,包括 Add/Done/Wait。WaitGroup 可用于描述了一组需要等待结束的协程,主协程通过调用 Add 方法增加需要等待的协程的数量(增加内部维护的计数器),每个协程在结束时调用 Done 方法来表示运行结束( 从而减少内部计数器的值 )。主协程通过调用 Wait 方法来等待其他协程完成( 阻塞直到内部计数器变为 0 )。

    var gw sync.GroupWait
    gw.Add( delta int )  // delta 常设置为 1,表示有一个协程需要等待 
    gw.Done()        // 协程结束运行,内部计数器减一
    gw.Wait()        // 阻塞直至内部计数器变为 0

 

posted on 2021-12-15 14:58  yhjoker  阅读(74)  评论(0编辑  收藏  举报

导航