1、通过通信共享

并发编程是一个很大的主题,这里只提供一些特定于go的重点内容。

在许多环境中,实现对共享变量的正确访问所需要的微妙之处使并发编程变得困难。Go鼓励一种不同的方法,在这种方法中,共享值在通道中传递,实际上,从不由单独的执行线程主动共享。在任何给定时间,只有一个goroutine可以访问该值。根据设计,数据竞争是不可能发生的。为了鼓励这种思维方式,我们把它简化为一句口号:

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存进行通信;相反,通过通信共享内存。

这种方法可能走得太远。例如,引用计数最好通过在整数变量周围放置互斥来实现。但是作为一种高级方法,使用通道来控制访问可以更容易地编写清晰、正确的程序。

考虑这个模型的一种方法是考虑一个典型的单线程程序运行在一个CPU上。它不需要同步原语。现在运行另一个这样的实例;它也不需要同步。现在让这两个程序通信;如果通信是同步器,则仍然不需要其他同步。例如,Unix管道就完美地符合这个模型。尽管Go的并发方法起源于Hoare的通信顺序处理(communication Sequential Processes, CSP),但它也可以被视为Unix管道的类型安全的泛化。

2、Goroutines

它们之所以被称为goroutine,是因为现有的术语——线程、协程、进程等等——传达了不准确的含义。goroutine有一个简单的模型:它是一个与相同地址空间中的其他goroutine并发执行的函数。它是轻量级的,比分配栈空间的成本高不了多少。而且栈开始时很小,所以它们很便宜,并通过根据需要分配(和释放)堆存储来增长。

goroutine被多路复用到多个操作系统线程上,因此如果一个线程阻塞,比如在等待I/O时,其他线程继续运行。它们的设计隐藏了线程创建和管理的许多复杂性。

go
function literal

在Go中,函数字面量( function literals )是闭包: 实现确保函数引用的变量只要处于活动状态就能存活。

3、Channels

make

无缓冲通道将通信(值的交换)与同步结合起来,确保两个计算(gorout例程)处于已知状态。

有很多使用通道的好习语。这是一个开始。在前一节中,我们在后台启动了排序。通道可以允许启动goroutine等待排序完成。

接收者总是阻塞,直到有数据接收。如果通道无缓冲,发送方将阻塞,直到接收方接收到该值。如果通道有缓冲区,发送方只阻塞直到值被复制到缓冲区;如果缓冲区已满,这意味着需要等待到某个接收器接收到一个值。 (参考3.1)

有缓冲通道可以像信号量(semaphore)一样使用,例如限制吞吐量。在本例中,传入的请求被传递给handle, handle将一个值发送到通道中,处理请求,然后从通道接收一个值,以便为下一个使用者准备“信号量”。通道缓冲区的容量限制了要处理的同时调用的数量。

一旦MaxOutstanding处理程序正在执行进程,试图向已充满的通道缓冲区发送的请求都将阻塞,直到现有的一个处理程序完成并从缓冲区接收。

ServeMaxOutstandingServe
 forreqreqreq

将此版本与前一个版本进行比较,查看闭包的声明和运行方式的差异。另一个解决方案是创建一个同名的新变量,如下例所示:

这样写似乎有些奇怪

req := req

但在Go 中这样做是合法的和惯用的。您将得到一个具有相同名称的新变量,故意在局部掩盖循环变量,但对每个goroutine都是惟一的。

handle handle processServe

3.1 Channel都有哪些特性

Go语言中的channel具有以下几个特性:

线程安全

channel是线程安全的,多个协程可以同时读写一个channel,而不会发生数据竞争的问题。这是因为Go语言中的channel内部实现了锁机制,保证了多个协程之间对channel的访问是安全的。

阻塞式发送和接收

当一个协程向一个channel发送数据时,如果channel已经满了,发送操作会被阻塞,直到有其他协程从channel中取走了数据。同样地,当一个协程从一个channel中接收数据时,如果channel中没有数据可供接收,接收操作会被阻塞,直到有其他协程向channel中发送了数据。这种阻塞式的机制可以保证协程之间的同步和通信。

顺序性

通过channel发送的数据是按照发送的顺序进行排列的。也就是说,如果协程A先向channel中发送了数据x,而协程B再向channel中发送了数据y,那么从channel中接收数据时,先接收到的一定是x,后接收到的一定是y。

可以关闭

通过关闭channel可以通知其他协程这个channel已经不再使用了。关闭一个channel之后,其他协程仍然可以从中接收数据,但是不能再向其中发送数据了。关闭channel的操作可以避免内存泄漏等问题。

缓冲区大小

channel可以带有一个缓冲区,用于存储一定量的数据。如果缓冲区已经满了,发送操作会被阻塞,直到有其他协程从channel中取走了数据;如果缓冲区已经空了,接收操作会被阻塞,直到有其他协程向channel中发送了数据。缓冲区的大小可以在创建channel时指定,例如:

会panic的几种情况

1.向已经关闭的channel发送数据

2.关闭已经关闭的channel

3.关闭未初始化的nil channel

会阻塞的情况:

nil
nil

3.在没有读取的groutine时,向无缓冲channel发数据,

有缓冲区,但缓冲区已满,发送数据时

4.在没有数据时,从无缓冲或者有缓冲channel读数据

返回零值:

从已经关闭的channe接收数据

3.2 channel 的最佳实践

在使用channel时,应该遵循以下几个最佳实践:

避免死锁

使用channel时应该注意避免死锁的问题。如果一个协程向一个channel发送数据,但是没有其他协程从channel中取走数据,那么发送操作就会一直被阻塞,从而导致死锁。为了避免这种情况,可以使用select语句来同时监听多个channel,从而避免阻塞。

避免泄漏

在使用channel时应该注意避免内存泄漏的问题。如果一个channel没有被关闭,而不再使用了,那么其中的数据就无法被释放,从而导致内存泄漏。为了避免这种情况,可以在协程结束时关闭channel。

避免竞争

在使用channel时应该注意避免数据竞争的问题。如果多个协程同时读写一个channel,那么就可能会发生竞争条件,从而导致数据不一致的问题。为了避免这种情况,可以使用锁机制或者使用单向channel来限制协程的访问权限。

避免过度使用

在使用channel时应该注意避免过度使用的问题。如果一个程序中使用了大量的channel,那么就可能会导致程序的性能下降。为了避免这种情况,可以使用其他的并发编程机制,例如锁、条件变量等。

4、Channels of channels

Go最重要的属性之一是通道是first-class值,可以像其他值一样分配和传递。此属性的常见用途是实现安全的并行多路解复用。

handleRequest

客户端提供了一个函数及其参数,以及请求对象内用于接收answer的通道。

在服务器端,唯一需要更改的是处理程序函数。

显然,要实现它还有很多工作要做,但这段代码是一个速率受限、并行、非阻塞RPC系统的框架,而且还没有看到mutex 。

5、并行(Parallelization)

这些思想的另一个应用是跨多个CPU核并行计算。如果计算可以被分解成可以独立执行的独立部分,那么它就可以被并行化,并在每个部分完成时用一个通道发出信号。

假设我们有一个昂贵的操作要对一个items的向量执行,并且每个item的操作值是独立的,就像在这个理想的例子中一样。

我们在一个循环中独立地启动这些片段,每个CPU一个。它们可以按任何顺序完成,但这没有关系;我们只是在启动所有的goroutine之后通过排泄通道来计算完成信号。

numCPUruntime.NumCPU
 runtime.GOMAXPROCSruntime.NumCPU

请务必不要混淆并发性(concurrency,将程序构造为独立执行的组件)和并行性(parallelism, 在多个cpu上并行执行计算以提高效率)这两个概念。尽管Go的并发特性可以使一些问题很容易构建为并行计算,但Go是一种并发语言,而不是并行语言,并且并不是所有的并行化问题都适合Go的模型。关于区别的讨论,请参阅本文章中引用的谈话。

6、漏桶缓冲区(A leaky buffer)

serverChan

服务器循环从客户端接收每条消息,处理它,并将缓冲区返回到空闲列表。

freeListcaseselectdefault select