当前位置: 首页 > 后端技术 > PHP

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

时间:2023-03-29 19:53:39 PHP

声明本系列文章不会停留在Go语言的语法层面,而是更加关注语言的特性,学习和使用中的问题,以及引起的一些想法。我们知道Go实现了两种形式的并发。首先是多线程共享内存,在Java、C++等语言中其实就是多线程并发,通过锁来访问。另一种是Go-specificCSP(communicatingsequentialprocesses)并发模型。什么是CSP?CSP是CommunicatingSequentialProcess的缩写。中文可以叫做CommunicationSequentialProcess。是TonyHoare在1977年提出的并发编程模型,是串行时代提出的概念,慢慢演变成现在的并发模型。简单来说,CSP模型是由并发执行的实体(线程或进程)组成的。实体通过发送消息进行通信。在这里,通道用于发送消息或通道。那么,CSP模型的关键是关注渠道,而不是发送消息的实体。Go语言实现了CSP的部分理论,具体模式如下图所示。Channel在gouroutine之间建立管道,在pipeline中传输数据,实现gouroutine之间的通信;因为它是线程安全的,所以使用起来非常方便;通道还提供“先进先出”功能;它还会影响goroutines的阻塞和唤醒。说到这里,可能有同学会有一些疑惑,为什么要用channel,Goroutine可以看成是一个线程,然后线程之间的通信就不能用共享内存来通信了?请阅读下文。为什么要用channel相信大家都听过这样一句话,Donotcommunicatebysharingmemory;instead,sharememorybycommunication(不通过共享内存进行通信,而是通过通信实现内存共享),这两句话不是什么意思吗?本质上,线程和协程在计算机上的同步信息实际上是通过共享内存进行的,因为不管是哪种通信模型,线程或者协程最终都会从内存中获取数据,所以更准确的说法是我们为什么要使用发送消息的方式来同步信息,而不是直接与多个线程或协程共享内存?下面分析一下使用场景。首先,前半句应该是指我们多应用于多线程通信的方式。通常,线程同步中线程间交换的信息只是控制信息。比如线程A释放了锁,线程B就可以拿到锁开始运行,这个不涉及数据交换。数据交换主要通过共享内存(共享变量或队列)实现。为了保证数据的安全性和正确性,共享内存必须要有锁等线程同步机制。线程同步使用起来特别麻烦,而且很容易造成死锁,锁太多会导致线程阻塞和这个过程中上下文切换带来的额外开销。我们通常会对在代码中添加锁感到恼火。后半句呢?我理解后半句是指channel共享内存。Go的这种方式,如果你想传递一些数据给另一个goroutine(协程),你可以把这个数据封装成一个对象,然后把这个对象的指针放到一个channel中,另一个goroutine从channel中读取指针并处理它指向的内存对象。channel本身保证同一时刻只有一个goroutine可以访问channel的数据,所以开发者不需要处理锁。我们根据它们的区别来总结一下:首先,使用发送消息来同步信息是比直接使用共享内存和互斥锁更高级的抽象。使用更高层次的抽象可以为我们提供更多的程序设计,良好的封装让程序的逻辑更加清晰;其次,消息发送在解耦方面与共享内存相比有一定的优势。我们可以将线程的职责划分为生产者和消费者,通过消息传递来解决。耦合,无需依赖共享内存;最后,Go语言选择发送消息的方式,通过保证一次只有一个活动线程可以访问数据,自然可以从设计上避免线程竞争和数据冲突的问题;另外,并不是我们所有人都必须使用channel而不是共享内存mutex,当然这是不可能的,我们在这里解释一个原因:如果我们发送一个指向Channel的指针而不是一个值,发送方实际上会发送消息后发送消息保留修改指针对应值的权利。如果此时发送方和接收方都试图修改指针对应的值,仍然会造成数据冲突的问题。当然,这在大多数情况下是一个设计问题,但是对于这种情况使用较低级别的互斥锁才是正确的做法。当然,我们会问通道如何保证同一时刻只有一个活动线程可以访问数据?其实channel本身也是通过锁来实现的,这和我们上面说的抽象思想的结论是相反的。它是如何实现的?我们将在下一篇文章中讨论它。不同类型的通道和常见错误通道分为两种类型,缓冲通道和非缓冲通道。我们使用以下代码示例来区分不同的通道类型。funcmain(){pipline:=make(chanstring)//构造一个无缓冲的通道pipline<-"helloworld"//发送数据fmt.Println(<-pipline)//读取数据}操作会抛出错误,如下:fatalerror:allgoroutinesaresleep-deadlock!想一想,我们正在创建一个无缓冲的通道,对于一个无缓冲的通道,发送操作被阻塞,直到接收者还没有准备好。那么,我们如何解决这个问题呢?请参阅下面的代码。funchello(piplinechanstring){<-pipline}funcmain(){pipline:=make(chanstring)gohello(pipline)//如果我们切换到直接在同一个协程中读取数据,pipline将永远阻塞<-"helloworld"}所以如果我们把这个例子改成缓冲通道,它还会阻塞吗?我们看下面的例子:funcmain(){pipline:=make(chanstring,1)pipline<-"helloworld"fmt.Println(<-pipline)}运行正常,你能看到buffer和whatis没有缓冲的区别?是的,区别在于有receiver时是否发生send操作。那么,缓冲通道会发生什么特殊情况?funcmain(){ch1:=make(chanstring,1)ch1<-"helloworld"ch1<-"helloChina"fmt.Println(<-ch1)}看这个例子,没错,它也会阻塞,每个缓冲通道都有一个容量。当通道中的数据量等于通道的容量时,此时向通道发送数据会造成拥塞。在有人从通道中消费数据之前,程序不会继续。执行。比如这段代码中,通道容量为1,但是向通道中写入两份数据会导致协程死锁。那么问题来了,当程序一直在等待从channel中读取数据的时候,这个时候不会有人往channel中写入数据。这时候程序就会陷入死循环,造成死锁。我们如何解决它?参见下面的示例:funcmain(){pipline:=make(chanstring)gofunc(){pipline<-"helloworld"pipline<-"helloChina"}()fordata:=rangepipline{fmt.Println(data)}}运行结果当然是所有的goroutine都睡着了——死锁!,通道没有关闭,程序一直在等待读取值,如何解决?funcmain(){pipline:=make(chanstring)gofunc(){pipline<-"helloworld"pipline<-"helloChina"close(pipline)//重点}()fordata:=rangepipline{fmt.Println(data)}}注意我标注的重点地方。关闭通道。这是一个非常明确的方法。既然问题是通道没有因为关闭而阻塞,那我发送完数据再关闭就ok了~下一篇预告【Go语言踩坑系列(十)】通道(下)关注我们。欢迎对本系列文章感兴趣的读者订阅我们的公众号。关注博主,下次不迷路了~