以常规方式编写并发程序,需要对共享变量作正确的访问控制,处理起来很困难。而golang提出一种不同的方式,即共享变量通过channel传递,共享变量从不被各个独立运行的线程(goroutine)同时享有,在任一时刻,共享变量仅可被一个goroutine访问。所以,不会产生数据竞争。并发编程,golang鼓励以此种方式进行思考,精简为一句口号——“勿通过共享内存来进行通信,而应通过通信来进行内存共享”。
<br />
<strong>1 Unbuffered channels与Buffered channels</strong>
<br />
Unbuffered channels的接收者阻塞直至收到消息,发送者阻塞直至接收者接收到消息,该机制可用于两个goroutine的状态同步。Buffered channels在缓冲区未满时,发送者仅在值拷贝到缓冲区之前是阻塞的,而在缓冲区已满时,发送者会阻塞,直至接收者取走了消息,缓冲区有了空余。
<br />
<strong>1.1 Unbuffered channels</strong>
<br />
如下代码使用Unbuffered channel作同步控制。给定一个整型数组,在主routine启动另一个goroutine将该数组排序,当其完成时,给done channel发送完成消息,主routine会一直等待直至排序完成,打印结果。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> ( </span></span></li><li class=""><span> <span class="string">"fmt"</span><span> </span></span></li><li class="alt"><span> <span class="string">"sort"</span><span> </span></span></li><li class=""><span> <span class="string">"time"</span><span> </span></span></li><li class="alt"><span>) </span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> main() { </span></li><li class=""><span> done := <span class="keyword">make</span>(<span class="keyword">chan</span> <span class="keyword">bool</span>) </span></li><li class="alt"><span> nums := []<span class="keyword">int</span><span>{</span><span class="number">2</span><span>, </span><span class="number">1</span><span>, </span><span class="number">3</span><span>, </span><span class="number">5</span><span>, </span><span class="number">4</span><span>} </span></span></li><li class=""><span> <span class="keyword">go</span> <span class="keyword">func</span>() { </span></li><li class="alt"><span> time.Sleep(time.Second) </span></li><li class=""><span> sort.Ints(nums) </span></li><li class="alt"><span> done <- <span class="keyword">true</span><span> </span></span></li><li class=""><span> }() </span></li><li class="alt"><span> <-done </span></li><li class=""><span> fmt.Println(nums) </span></li><li class="alt"><span>} </span></li></ol></div>
<br />
<strong>1.2 Buffered channels</strong>
<br />
如下代码中,messages chan的缓冲区大小为2,因其为Buffered channel,所以消息发送与接收无须分开到两个并发的goroutine中。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> ( </span></span></li><li class=""><span> <span class="string">"fmt"</span><span> </span></span></li><li class="alt"><span>) </span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> main() { </span></li><li class=""><span> messages := <span class="keyword">make</span>(<span class="keyword">chan</span> string, <span class="number">2</span><span>) </span></span></li><li class="alt"><span> messages <- <span class="string">"hello"</span><span> </span></span></li><li class=""><span> messages <- <span class="string">"world"</span><span> </span></span></li><li class="alt"><span> fmt.Println(<-messages, <-messages) </span></li><li class=""><span>} </span></li></ol></div>
<br />
<strong>2 配套使用</strong>
<br />
<strong>2.1 指明channel direction</strong>
<br />
函数封装时,对仅作消息接收或仅作消息发送的chan标识direction可以借用编译器检查增强类型使用安全。如下代码中,ping函数中pings chan仅用来接收消息,所以参数列表中将其标识为接收者。pong函数中,pings chan仅用来发送消息,pongs chan仅用来接收消息,所以参数列表中二者分别标识为发送者与接收者。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> </span><span class="string">"fmt"</span><span> </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> ping(pings <span class="keyword">chan</span><- string, msg string) { </span></li><li class=""><span> pings <- msg </span></li><li class="alt"><span>} </span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> pong(pings <-<span class="keyword">chan</span> string, pongs <span class="keyword">chan</span><- string) { </span></li><li class=""><span> pongs <- <-pings </span></li><li class="alt"><span>} </span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> main() { </span></li><li class=""><span> pings, pongs := <span class="keyword">make</span>(<span class="keyword">chan</span> string, <span class="number">1</span><span>), <span class="keyword">make</span>(<span class="keyword">chan</span> string, </span><span class="number">1</span><span>) </span></span></li><li class="alt"><span> ping(pings, <span class="string">"ping"</span><span>) </span></span></li><li class=""><span> pong(pings, pongs) </span></li><li class="alt"><span> fmt.Println(<-pongs) </span></li><li class=""><span>} </span></li></ol></div>
<br />
<strong>2.2 select</strong>
<br />
使用select可以用来等待多个channel的消息,如下代码,创建两个chan,启动两个goroutine耗费不等时间计算结果,主routine监听消息,使用两次select,第一次接收到了ch2的消息,第二次接收到了ch1的消息,用时2.000521146s。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> ( </span></span></li><li class=""><span> <span class="string">"fmt"</span><span> </span></span></li><li class="alt"><span> <span class="string">"time"</span><span> </span></span></li><li class=""><span>) </span></li><li class="alt"><span> </span></li><li class=""><span><span class="keyword">func</span> main() { </span></li><li class="alt"><span> c1, c2 := <span class="keyword">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span><span>, </span><span class="number">1</span><span>), <span class="keyword">make</span>(<span class="keyword">chan</span> </span><span class="keyword">int</span><span>, </span><span class="number">1</span><span>) </span></span></li><li class=""><span> go <span class="keyword">func</span>() { </span></li><li class="alt"><span> time.Sleep(<span class="number">2</span><span> * time.Second) </span></span></li><li class=""><span> c1 <- <span class="number">1</span><span> </span></span></li><li class="alt"><span> }() </span></li><li class=""><span> go <span class="keyword">func</span>() { </span></li><li class="alt"><span> time.Sleep(time.Second) </span></li><li class=""><span> c2 <- <span class="number">2</span><span> </span></span></li><li class="alt"><span> }() </span></li><li class=""><span> <span class="keyword">for</span><span> i := </span><span class="number">0</span><span>; i < </span><span class="number">2</span><span>; i++ { </span></span></li><li class="alt"><span> <span class="keyword">select</span> { </span></li><li class=""><span> <span class="keyword">case</span><span> msg1 := <-c1: </span></span></li><li class="alt"><span> fmt.Println(<span class="string">"received msg from c1"</span><span>, msg1) </span></span></li><li class=""><span> <span class="keyword">case</span><span> msg2 := <-c2: </span></span></li><li class="alt"><span> fmt.Println(<span class="string">"received msg from c2"</span><span>, msg2) </span></span></li><li class=""><span> } </span></li><li class="alt"><span> } </span></li><li class=""><span>} </span></li></ol></div>
<br />
<strong>2.3 select with default</strong>
<br />
select with default可以用来处理非阻塞式消息发送、接收及多路选择。如下代码中,第一个select为非阻塞式消息接收,若收到消息,则落入<-messages case,否则落入default。第二个select为非阻塞式消息发送,与非阻塞式消息接收类似,因messages chan为Unbuffered channel且无异步消息接收者,因此落入default case。第三个select为多路非阻塞式消息接收。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> </span><span class="string">"fmt"</span><span> </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> main() { </span></li><li class=""><span> messages := <span class="keyword">make</span>(<span class="keyword">chan</span> string) </span></li><li class="alt"><span> signal := <span class="keyword">make</span>(<span class="keyword">chan</span> <span class="keyword">bool</span>) </span></li><li class=""><span> </span></li><li class="alt"><span> <span class="comment">// receive with default</span><span> </span></span></li><li class=""><span> <span class="keyword">select</span> { </span></li><li class="alt"><span> <span class="keyword">case</span><span> <-messages: </span></span></li><li class=""><span> fmt.Println(<span class="string">"message received"</span><span>) </span></span></li><li class="alt"><span> <span class="keyword">default</span><span>: </span></span></li><li class=""><span> fmt.Println(<span class="string">"no message received"</span><span>) </span></span></li><li class="alt"><span> } </span></li><li class=""><span> </span></li><li class="alt"><span> <span class="comment">// send with default</span><span> </span></span></li><li class=""><span> <span class="keyword">select</span> { </span></li><li class="alt"><span> <span class="keyword">case</span><span> messages <- </span><span class="string">"message"</span><span>: </span></span></li><li class=""><span> fmt.Println(<span class="string">"message sent successfully"</span><span>) </span></span></li><li class="alt"><span> <span class="keyword">default</span><span>: </span></span></li><li class=""><span> fmt.Println(<span class="string">"message sent failed"</span><span>) </span></span></li><li class="alt"><span> } </span></li><li class=""><span> </span></li><li class="alt"><span> <span class="comment">// muti-way <span class="keyword">select</span></span><span> </span></span></li><li class=""><span> <span class="keyword">select</span> { </span></li><li class="alt"><span> <span class="keyword">case</span><span> <-messages: </span></span></li><li class=""><span> fmt.Println(<span class="string">"message received"</span><span>) </span></span></li><li class="alt"><span> <span class="keyword">case</span><span> <-signal: </span></span></li><li class=""><span> fmt.Println(<span class="string">"signal received"</span><span>) </span></span></li><li class="alt"><span> <span class="keyword">default</span><span>: </span></span></li><li class=""><span> fmt.Println(<span class="string">"no message or signal received"</span><span>) </span></span></li><li class="alt"><span> } </span></li><li class=""><span>} </span></li></ol></div>
<br />
<strong>2.4 close</strong>
<br />
当无需再给channel发送消息时,可将其close。如下代码中,创建一个Buffered channel,首先启动一个异步goroutine循环消费消息,然后主routine完成消息发送后关闭chan,消费goroutine检测到chan关闭后,退出循环。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> </span><span class="string">"fmt"</span><span> </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> main() { </span></li><li class=""><span> messages := <span class="keyword">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span><span>, </span><span class="number">10</span><span>) </span></span></li><li class="alt"><span> done := <span class="keyword">make</span>(<span class="keyword">chan</span> <span class="keyword">bool</span>) </span></li><li class=""><span> </span></li><li class="alt"><span> <span class="comment">// consumer</span><span> </span></span></li><li class=""><span> <span class="keyword">go</span> <span class="keyword">func</span>() { </span></li><li class="alt"><span> <span class="keyword">for</span><span> { </span></span></li><li class=""><span> msg, more := <-messages </span></li><li class="alt"><span> <span class="keyword">if</span><span> !more { </span></span></li><li class=""><span> fmt.Println(<span class="string">"no more message"</span><span>) </span></span></li><li class="alt"><span> done <- <span class="keyword">true</span><span> </span></span></li><li class=""><span> <span class="keyword">break</span><span> </span></span></li><li class="alt"><span> } </span></li><li class=""><span> fmt.Println(<span class="string">"message received"</span><span>, msg) </span></span></li><li class="alt"><span> } </span></li><li class=""><span> }() </span></li><li class="alt"><span> </span></li><li class=""><span> <span class="comment">// producer</span><span> </span></span></li><li class="alt"><span> <span class="keyword">for</span><span> i := </span><span class="number">0</span><span>; i < </span><span class="number">5</span><span>; i++ { </span></span></li><li class=""><span> messages <- i </span></li><li class="alt"><span> } </span></li><li class=""><span> close(messages) </span></li><li class="alt"><span> <-done </span></li><li class=""><span>} </span></li></ol></div>
<br />
<strong>2.5 for range</strong>
<br />
for range语法不仅可对基础数据结构(slice、map等)作迭代,还可对channel作消息接收迭代。如下代码中,给messages chan发送两条消息后将其关闭,然后迭代messages chan打印消息。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> </span><span class="string">"fmt"</span><span> </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> main() { </span></li><li class=""><span> messages := <span class="keyword">make</span>(<span class="keyword">chan</span> string, <span class="number">2</span><span>) </span></span></li><li class="alt"><span> messages <- <span class="string">"hello"</span><span> </span></span></li><li class=""><span> messages <- <span class="string">"world"</span><span> </span></span></li><li class="alt"><span> close(messages) </span></li><li class=""><span> </span></li><li class="alt"><span> <span class="keyword">for</span><span> msg := <span class="keyword">range</span> messages { </span></span></li><li class=""><span> fmt.Println(msg) </span></li><li class="alt"><span> } </span></li><li class=""><span>} </span></li></ol></div>
<br />
<strong>3 应用场景</strong>
<br />
<strong>3.1 超时控制</strong>
<br />
资源访问、网络请求等场景作超时控制是非常必要的,可以使用channel结合select来实现。如下代码,对常规sum函数增加超时限制,sumWithTimeout函数中,select的v := <-rlt在等待计算结果,若在时限范围内计算完成,则正常返回计算结果,若超过时限则落入<-time.After(timeout) case,抛出timeout error。
<div class="dp-highlighter"><div class="bar"></div><ol start="1" class="dp-j"><li class="alt"><span><span class="keyword">package</span><span> main </span></span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">import</span><span> ( </span></span></li><li class=""><span> <span class="string">"errors"</span><span> </span></span></li><li class="alt"><span> <span class="string">"fmt"</span><span> </span></span></li><li class=""><span> <span class="string">"time"</span><span> </span></span></li><li class="alt"><span>) </span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> sum(nums []<span class="keyword">int</span><span>) </span><span class="keyword">int</span><span> { </span></span></li><li class=""><span> rlt := <span class="number">0</span><span> </span></span></li><li class="alt"><span> <span class="keyword">for</span><span> _, num := <span class="keyword">range</span> nums { </span></span></li><li class=""><span> rlt += num </span></li><li class="alt"><span> } </span></li><li class=""><span> <span class="keyword">return</span><span> rlt </span></span></li><li class="alt"><span>} </span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> sumWithTimeout(nums []<span class="keyword">int</span><span>, timeout time.Duration) (</span><span class="keyword">int</span><span>, error) { </span></span></li><li class=""><span> rlt := <span class="keyword">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span><span>) </span></span></li><li class="alt"><span> <span class="keyword">go</span> <span class="keyword">func</span>() { </span></li><li class=""><span> time.Sleep(<span class="number">2</span><span> * time.Second) </span></span></li><li class="alt"><span> rlt <- sum(nums) </span></li><li class=""><span> }() </span></li><li class="alt"><span> <span class="keyword">select</span> { </span></li><li class=""><span> <span class="keyword">case</span><span> v := <-rlt: </span></span></li><li class="alt"><span> <span class="keyword">return</span><span> v, nil </span></span></li><li class=""><span> <span class="keyword">case</span><span> <-time.After(timeout): </span></span></li><li class="alt"><span> <span class="keyword">return</span><span> </span><span class="number">0</span><span>, errors.New(</span><span class="string">"timeout"</span><span>) </span></span></li><li class=""><span> } </span></li><li class="alt"><span>} </span></li><li class=""><span> </span></li><li class="alt"><span><span class="keyword">func</span> main() { </span></li><li class=""><span> nums := []<span class="keyword">int</span><span>{</span><span class="number">1</span><span>, </span><span class="number">2</span><span>, </span><span class="number">3</span><span>, </span><span class="number">4</span><span>, </span><span class="number">5</span><span>} </span></span></li><li class="alt"><span> timeout := <span class="number">3</span><span> * time.Second </span><span class="comment">// time.Second</span><span> </span></span></li><li class=""><span> rlt, err := sumWithTimeout(nums, timeout) </span></li><li class="alt"><span> <span class="keyword">if</span><span> nil != err { </span></span></li><li class=""><span> fmt.Println(<span class="string">"error"</span><span>, err) </span></span></li><li class="alt"><span> <span class="keyword">return</span><span> </span></span></li><li class=""><span> } </span></li><li class="alt"><span> fmt.Println(rlt) </span></li><li class=""><span>} </span></li></ol></div>
<br />
本文代码托管地址:<a href="https://github.com/olzhy/go-excercises/tree/master/channels" target="blank">https://github.com/olzhy/go-excercises/tree/master/channels</a>
原文地址:https://leileiluoluo.com/posts/golang-channels.html
有疑问加站长微信联系(非本文作者))