【Go语言踩坑系列(九)】Channel(上)

声明

本系列文章并不会停留在Go语言的语法层面,更关注语言特性、学习和使用中出现的问题以及引发的一些思考。编程

咱们知道,Go实现了两种并发形式,第一种是多线程共享内存,其实就是Java,C++等语言的多线程并发,经过锁来进行访问。另外一种则是Go特有的CSP(communicating sequential processes)并发模型。segmentfault

什么是CSP?

CSP 是 Communicating Sequential Process 的简称,中文能够叫作通讯顺序进程,是一种并发编程模型,由 Tony Hoare 于 1977 年提出。它是在串行时代提出的一个概念,慢慢的演化成了如今的一种并发模型。简单来讲,CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间经过发送消息进行通讯,这里发送消息时使用的就是通道,或者叫 channel。那么,CSP 模型的关键是关注 channel,而不关注发送消息的实体。而Go 语言实现了 CSP 部分理论,具体的模式以下图所示。
channel1.png
Channel 在 gouroutine 间架起了一条管道,在管道里传输数据,实现 gouroutine 间的通讯;因为它是线程安全的,因此用起来很是方便;channel 还提供 “先进先出” 的特性;它还能影响 goroutine 的阻塞和唤醒。安全

说到这,可能就有的同窗有些疑问,为何要用channel,Goroutine不就能够看做一个线程,而后线程间通讯用共享内存来通讯不行么?请往下看。多线程

为何要用channel

相信你们都听过这么一句话,Do not communicate by sharing memory; instead, share memory by communicating(不要经过共享内存来通讯,而要经过通讯来实现内存共享),这两句话难道不是一个意思么?从本质上来看,计算机上线程和协程同步信息其实都是经过共享内存来进行的,由于不管是哪一种通讯模型,线程或者协程最终都会从内存中获取数据,因此更为准确的说法是为何咱们使用发送消息的方式来同步信息,而不是多个线程或者协程直接共享内存?
咱们从使用场景分析一下,首先,前半句应该是指咱们多应用于多线程通讯的方式,通常线程同步在线程间交换的信息仅仅是控制信息,好比某个A线程释放了锁,B线程能获取到锁并开始运行,这个不涉及数据的交换。数据的交换主要仍是经过共享内存(共享变量或者队列)来实现,为了保证数据的安全和正确性,共享内存就必须要加锁等线程同步机制。而线程同步使用起来特别麻烦,容易形成死锁,且过多的锁会形成线程的阻塞以及这个过程当中上下文切换带来的额外开销。咱们一般会由于在代码中加锁而感到烦恼。
下半句呢?我理解后半句是说的channel来共享内存,在Go的这种方式中,要传递某个数据给另外一个goroutine(协程),能够把这个数据封装成一个对象,而后把这个对象的指针传入某个channel中,另一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。channel自己保证来同一时间只有一个goroutine能访问channel的数据,就不用开发者去处理锁了。
咱们根据他们的差别来总结一下:并发

  • 首先,使用发送消息来同步信息相比于直接使用共享内存和互斥锁是一种更高级的抽象,使用更高级的抽象可以为咱们在程序设计上提供更好的封装,让程序的逻辑更加清晰;
  • 其次,消息发送在解耦方面与共享内存相比也有必定优点,咱们能够将线程的职责分红生产者和消费者,并经过消息传递的方式将它们解耦,不须要再依赖共享内存;
  • 最后,Go 语言选择消息发送的方式,经过保证同一时间只有一个活跃的线程可以访问数据,可以从设计上自然地避免线程竞争和数据冲突的问题;

另外,是否是咱们都得使用channel来代替共享内存mutex,固然是不可能的,咱们在这来讲明一个缘由:若是咱们向 Channel 中发送了一个指针而不是值的话,发送方在发送该条消息以后其实也保留了修改指针对应值的权利,若是这时发送方和接收方都尝试修改指针对应的值,仍然会形成数据冲突的问题。固然这种大多数状况下是一种设计上的问题,然而针对这种状况使用更为底层的互斥锁才是一种正确的方式。学习

固然,咱们会问channel怎么保证同一时间只有一个活跃的线程可以访问数据的呢?其实channel自己也是经过锁来实现,这就对照咱们上边所说的抽象的思想的结论了。具体是怎么实现的呢?咱们将会在下一篇文章讲述。spa

channel的不一样种类以及常见的错误

channel分为两种,有缓冲channel和无缓冲channel。咱们经过下边的代码例子来区分不一样的channel种类。线程

func main() {
    pipline := make(chan string) //构造无缓冲通道
    pipline <- "hello world" //发送数据
    fmt.Println(<-pipline)  //读数据
}

运行会抛出错误,以下:设计

fatal error: all goroutines are asleep - deadlock!

思考一下,咱们建立的是一个无缓冲通道,而对于无缓冲通道,在接收者未准备好以前,发送操做是阻塞的。那么,咱们该怎么去解决这种问题呢?看下边代码。指针

func hello(pipline chan string)  {
    <-pipline
}

func main()  {
    pipline := make(chan string)
    go hello(pipline) //若是咱们换成直接在同一个协程里读数据会永远阻塞
    pipline <- "hello world"
}

那么咱们若是把这个例子改为有缓冲通道还会阻塞吗?咱们看下边的例子:

func main() {
    pipline := make(chan string, 1)
    pipline <- "hello world"
    fmt.Println(<-pipline)
}

运行正常,此时是否是就能看出缓冲和没有缓冲的区别呢?是的,区别在于在发送操做是否发生在有接受者时。那么,对于有缓冲通道会发生什么特殊状况呢?

func main() {
    ch1 := make(chan string, 1)

    ch1 <- "hello world"
    ch1 <- "hello China"

    fmt.Println(<-ch1)
}

看这个例子,没错,他也会阻塞,每一个缓冲通道,都有容量,当通道里的数据量等于通道的容量后,此时再往通道里发送数据,就失形成阻塞,必须等到有人从通道中消费数据后,程序才会往下进行
好比这段代码,通道容量为 1,可是往通道中写入两条数据,对于一个协程来讲就会形成死锁。

那么问题来了,当程序一直在等待从通道里读取数据,而此时并无人会往通道中写入数据。此时程序就会陷入死循环,形成死锁,咱们如何去解决呢?看下边的例子:

func main() {
    pipline := make(chan string)
    go func() {
        pipline <- "hello world"
        pipline <- "hello China"
    }()
    for data := range pipline{
        fmt.Println(data)
    }
}

运行结果固然是all goroutines are asleep - deadlock!,通道没有被关闭,程序就一直在等待读取值,怎么解决呢?

func main() {
    pipline := make(chan string)
    go func() {
        pipline <- "hello world"
        pipline <- "hello China"
        close(pipline) // 重点
    }()
    for data := range pipline{
        fmt.Println(data)
    }
}

注意看我标为重点的地方,关闭通道,很明确的方法,既然问题是由于通道没有被关闭形成的阻塞,那么我在发送完数据后关掉就ok了啊~

下期预告

【Go语言踩坑系列(十)】Channel(下)

关注咱们

欢迎对本系列文章感兴趣的读者订阅咱们的公众号,关注博主下次不迷路~

Nosay