forked from mikespook/Learning-Go-zh-cn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
go-channels.tex
167 lines (154 loc) · 7.44 KB
/
go-channels.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
\epi{%
\begin{itemize}
\item{``并行是关于性能的;}
\item{并发是关于程序设计的。''}
\end{itemize}%
}{\textit{Google IO 2010}\\\textsc{ROB PIKE}}
\noindent{}在这章中将展示 Go 使用 channel 和 goroutine 开发并行程序的能力。
goroutine 是 Go 并发能力的核心要素。但是,goroutine 到底 \emph{是}什么?来自
\cite{effective_go}:
\begin{quote}
叫做 goroutine 是因为已有的短语——线程、协程、进程等等——传递了不准确的含义。
goroutine 有简单的模型:\emph{它是与其他 goroutine 并行执行的,有着相同地址空间的函数。}。
它是轻量的,仅比分配栈空间多一点点消耗。而初始时栈是很小的,所以它们也是廉价的,
并且随着需要在堆空间上分配(和释放)。
\end{quote}
\first{goroutine}{goroutine} 是一个普通的函数,只是需要使用关键字
\first{\key{go}}{keyword!go} 作为开头。
\begin{lstlisting}
ready("Tea", 2) |\coderemark{普通函数调用}|
go ready("Tea", 2) |\coderemark{\func{ready()} 作为 goroutine 运行}|
\end{lstlisting}
下面程序的思路来自 \cite{go_course_day3}。
让一个函数作为两个 goroutine 执行,goroutine 等待一段时间,然后打印一些内容到屏幕。
在第 14 和 15 行,启动了 goroutine。
\func{main} 函数等待足够的长的时间,这样每个 goroutine 会打印各自的文本到屏幕。
现在是在第 17 行等待 5 秒钟,
但实际上没有任何办法知道,当所有 goroutine 都已经退出应当等待多久。
\lstinputlisting[numbers=right,label=src:sleeping,firstnumber=8,caption=Go routine 实践,linerange={8,18}]{src/sleep.go}
表 \ref{src:sleeping} 输出:
\begin{display}
I'm waiting \coderemark{立刻}
Coffee is ready! \coderemark{1 秒后}
Tea is ready! \coderemark{2 秒后}
\end{display}
如果不等待 goroutine 的执行(例如,移除第 17 行),程序立刻终止,而任何正在执行的
goroutine 都\emph{会停止}。
为了修复这个,需要一些能够同 goroutine 通讯的机制。这一机制通过 \first{channels}{channels}
的形式使用。\first{channel}{channel} 可以与 Unix sehll 中的双向管道做类比:
可以通过它发送或者接收值。这些值只能是特定的类型:channel 类型。
定义一个 channel 时,也需要定义发送到 channel 的值的类型。注意,必须使用
\key{make} 创建 channel:
\begin{lstlisting}
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
\end{lstlisting}
创建 channel \var{ci} 用于发送和接收整数,创建 channel \var{cs} 用于字符串,
以及 channel \var{cf} 使用了空接口来满足各种类型。
向 channel 发送或接收数据,是通过类似的操作符完成的:
\lstinline{<-}. \index{operator!channel}
具体作用则依赖于操作符的位置:
\begin{lstlisting}
ci <- 1 |\coderemark{\emph{发送}整数 1 到 channel \var{ci}}|
<-ci |\coderemark{从 channel \var{ci} \emph{接收}整数}|
i := <-ci |\coderemark{从 channel \var{ci} \emph{接收}整数,并保存到 \var{i} 中}|
\end{lstlisting}
将这些放到实例中去。
\begin{lstlisting}[numbers=none,caption=Go routines 和 channel,label=src:sleeping with channels]
var c chan int |\longremark{定义 \var{c} 作为 int 型的 channel。就是说:这个 channel 传输整数。%
注意这个变量是全局的,这样 goroutine 可以访问它;}|
func ready(w string, sec int) {
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, "is ready!")
c <- 1 |\longremark{发送整数 1 到 channel \var{c};}|
}
func main() {
c = make(chan int) |\longremark{初始化 \var{c};}|
go ready("Tea", 2) |\longremark{用关键字 \key{go} 开始一个 goroutine;}|
go ready("Coffee", 1)
fmt.Println("I'm waiting, but not too long")
<-c |\longremark{等待,直到从 channel 上接收一个值。注意,收到的值被丢弃了;}|
<-c |\longremark{两个 goroutines,接收两个值。}|
}
\end{lstlisting}
\showremarks
这里仍然有一些丑陋的东西;不得不从 channel 中读取两次(第 14 和 15 行)。
在这个例子中没问题,但是如果不知道有启动了多少个 goroutine 怎么办呢?
这里有另一个 Go 内建的关键字:\first{\key{select}}{keyword!select}。
通过 \key{select}(和其他东西)可以监听 channel 上输入的数据。
在这个程序中使用 \key{select},并不会让它变得更短,因为运行的 go\-routine 太少了。
移除第 14 和 15 行,并用下面的内容替换它们:
\begin{lstlisting}[caption=使用 select,numbers=right,firstnumber=14]
L: for {
select {
case <-c:
i++
if i > 1 {
break L
}
}
}
\end{lstlisting}
现在将会一直等待下去。只有当从 channel \var{c} 上收到多个响应时才会退出循环 \var{L}。
\subsection{使其并行运行}
虽然 goroutine 是并发执行的,但是它们并不是并行运行的。如果不告诉 Go 额外的东西,
同一时刻只会有一个 goroutine 执行。利用 \func{runtime.GOMAXPROCS(n)} 可以设置 goroutine 并行执行的数量。
来自文档:
\begin{quote}
GOMAXPROCS 设置了同时运行的 CPU 的最大数量,并返回之前的设置。如果 n < 1,不会改变当前设置。
\emph{当调度得到改进后,这将被移除。}
\end{quote}
如果不希望修改任何源代码,同样可以通过设置环境变量
\verb|GOMAXPROCS| 为目标值。
%% test
%%\marginpar{%
%%$$\left\{
%%\begin{array}{l}
%%\parbox{2cm}{
%%hallo Yppp hallo Yppp hallo Yppp
%%hallo Yppp hallo Yppp hallo Yppp
%%hallo Yppp
%%}
%%\end{array}
%%\right.$$
%%}
%%\section{So many channels and still \ldots}
\section{更多关于 channel}
\label{sec:more on channels}
当在 Go 中用 \lstinline{ch := make(chan bool)} 创建 chennel 时,bool 型的
\first{无缓冲 channel}{channel!unbuffered} 会被创建。
这对于程序来说意味着什么呢?首先,如果读取(\lstinline{value := <-ch})它将会被阻塞,直到有数据接收。
其次,任何发送(\lstinline{ch<-5}) 将会被阻塞,直到数据被读出。
无缓冲 channel 是在多个 goroutine 之间同步很棒的工具。
\index{channel!blocking read}
\index{channel!blocking write}
不过 Go 也允许指定 channel 的缓冲大小,很简单,就是 channel 可以存储多少元素。
\lstinline{ch := make(chan bool, 4)},创建了可以存储 4 个元素的 bool 型 channel。
在这个 channel 中,前 4 个元素可以无阻塞的写入。当写入第 5$^{个}$ 元素时,代码
\emph{将会}阻塞,直到其他 goroutine 从 channel 中读取一些元素,腾出空间。
\index{channel!non-blocking read}
\index{channel!non-blocking write}
一句话说,在 Go 中下面的为 true:
$$
\textrm{\lstinline{ch := make(chan type, value)}}
\left\{
\begin{array}{ll}
value == 0 & \rightarrow \textrm{无缓冲} \\
value > 0 & \rightarrow \textrm{缓冲 \emph{value} 的元素}
\end{array}
\right.
$$
\subsection{关闭 channel}
当 channel 被关闭后,读取端需要知道这个事情。下面的代码演示了如何检查 channel 是否被关系。
\begin{lstlisting}
x, ok = <-ch
\end{lstlisting}
当 \lstinline{ok} 被赋值为 \lstinline{true} 意味着 channel 尚未被关闭,\emph{同时} 可以读取数据。
否则 \var{ok} 被赋值为 \lstinline{false}。在这个情况下表示 channel 被关闭。
\section{练习}
\input{ex-channels/ex-for-channels.tex}
\input{ex-channels/ex-fib.tex}
\cleardoublepage
\section{答案}
\shipoutAnswer