美文网首页
C++程序员的go学习之路(3)——goroutine、chan

C++程序员的go学习之路(3)——goroutine、chan

作者: 丑角的晨歌 | 来源:发表于2018-08-18 15:19 被阅读0次

    goroutine

    go中的并发主要靠协程(goroutine)。不同于C和C++中常用的多线程,协程并不与操作系统中的线程一一对应,操作系统是不知道有协程的存在的,协程间的调度由用户程序自己控制。go在runtime、系统调用等多方面对goroutine调度进行了封装处理,从语言层面支持了协程,这也是go的一大特色。只需要在函数前加一个go关键字就可以创建一个协程。

    协程就是在应用层模拟的线程,降低了线程间切换的耗费。协程也是基于线程实现的,程序在内部维护一组数据结构和线程,真正执行的还是线程,而协程执行的代码被放进一个队列中,由工作线程从队列中拉出来执行。

    go的协程是目前各类有协程概念的语言中实现得最完整和成熟的,十万个协程同时运行也毫无压力,虽然我们不会这么写代码。go对各种io函数进行了封装,当这些异步函数阻塞时go就会利用这个时机将现有的执行序列压栈,切换到另外一个协程。但是由于协程是非抢占式的调度,无法实现公平的任务调用,也无法直接利用多核优势,所以我们也不能直接说协程是比线程更高级的技术。

    虽然在任务调度上,协程弱于线程,但资源消耗方面协程远远小于线程。一个线程的内存在MB级别,而协程只需要KB级别。我们可以把协程的基本特点归纳为:

    1. 协程调度机制无法实现公平调度
    2. 协程的资源开销非常低,一台普通的服务器就可以支持百万协程

    在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行,当函数返回时该goroutine也自动结束,且返回值会被丢弃:

    func Add(x, y int) {
      z := x+y
      fmt.Println(z)
    }
    
    func main() {
      Add(1, 1)
      go Add(1, 1)
    }
    

    上面的代码创建了一个协程,并在主函数与协程中打印值。但是运行一下我们会发现只有一个输出。因为在协程开始工作之前,主函数已经退出了。
    为了阻止主函数过早退出,显然我们可以Sleep一下,但是这并不是一个优雅的解决方案。想想C++中的thread库为我们提供了join函数用来等待线程执行完毕,这里我们也需要一个东西来让主函数阻塞住,在go里可以用channel来达到这个目的。

    channel

    channel用于goroutine之间的通信,类似于管道。可以看做一个FIFO队列。它的操作符是箭头,<-
    它的类型定义如下:

    ChannelType = ("chan" | "chan" "<-" | "<-" "chan") ElementType
    chan T  // 可以接收或发送类型为T的数据
    chan<- float64  // 只能发送类型为float64的数据
    <-chan int   // 只能接收类型为int的数据
    

    如果没有指定方向就是双向,既可以接收,也可以发送。
    应该在生产者处关闭channel,如果在消费者处关闭容易引起panic:往一个已经关闭的channel发送数据会panic,接收会返回0值,可以用一个额外的参数检查channel是否已经关闭:

    x, ok := <- ch
    

    使用make初始化channel,并且可以设置缓冲区容量。如果channel没有缓存,那么只有通信两端都准备好后才会开始通信,否则保持阻塞。如果设置了缓存,则缓存满时发送方才会阻塞,缓存空时接收方才会阻塞。
    往一个nil channel发送或接收数据会一直阻塞。
    可以在多个goroutine中读写同一个channel,不必加锁。

    // 使用make建立一个channel
    var channel chan int = make(chan int)
    // 或者这样写也行
    channel := make(chan int)
    // 初始化并设置缓存容量
    channel := make(chan int, 1024)
    
    // 写channel,在有其他goroutine来读之前会保持阻塞
    ch <- value
    // 读channel,如果channel之前没有写入数据,也会一直阻塞直至有人写入数据
    value := <-ch
    value, ok := <-ch
    

    可以通过类型转换将一个channel转换为单向的,从而限制对其的操作(类似于C++中对const的使用):

    ch4 := make(chan, int)
    ch5 := <-chan int(ch4)
    ch6 := chan<- int(ch4)
    

    channel与slice类似,也对应一个由make创建的底层数据结构的引用。当我们复制一个channel或者将channel作为函数参数时,我们只是拷贝了一个引用,它们指向的底层数据结构是一致的。
    所以使用channel改写一下上面的那个例子,使协程可以正常执行:

    var complete chan int = make(chan int)
    func Add(x, y int) {
      z := x+y
      fmt.Println(z)
      complete <- 0 // 发个消息通知已经执行完了
    }
    
    func main() {
      Add(1, 1)
      go Add(1, 1)
      <-complete // 直到取到消息之前会在这里阻塞住
    }

    相关文章

      网友评论

          本文标题:C++程序员的go学习之路(3)——goroutine、chan

          本文链接:https://www.haomeiwen.com/subject/eoxvvftx.html