Day 4:
goroutine是Go运行时管理的轻量级线程
>>> go f(x, y, z)
上面这条语句会在一个新的goroutine中执行
f(x, y, z)
f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。
Go程都在相同的地址空间中运行,所以必须以同步的方式访问共享内存。sync包提供了这个能力,尽管由于其他方法的存在使得你不必要使用这种方式。
通道是一种类型化的管道,使用通道运算符<-我们可以发送\接收值
类似箭头一样的通道运算符指示了数据的流动方向
像映射和切片一样,通道在使用前必须进行创建
>>> ch := make(chan int)
默认的,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
信道可以是带缓冲的信道。在创建信道的时候传入作为缓冲长度的第二个参数,即可创建带缓冲的信道
>>> ch := make(chan int, 100)
发送和接收分别会在信道满了和空了的情况下阻塞
sender可以关闭一个信道以表明没有值将被传入信道中。receivers可以通过在receive表达式中配置第二个参数来测试一个信道是否已经被关闭了
>>> v, ok := <- ch
如果没有值可接收并且信道被关闭了,则ok的值为false
>>> for i := range c
将会重复接收信道中的值,直到信道关闭
注意:
- 只有sender可以关闭信道,而receiver不能。因为给已关闭的信道发送值将会导致恐慌
- 信道不同于文件,通常情况下不需要关闭它们。仅当必须告知接受者不再有其他值时才需要关闭,比如终止range循环
select 语句使一个 Go 程可以等待多个通信操作
select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。
与该段代码功能相似的Python代码为:
import queue
from threading import Thread
def fibonacci(c):
x, y = 0, 1
while True:
if _quit:
print("quit")
return
else:
c.put(x)
x, y = y, x+y
def print_quit(c):
global _quit
for _ in range(10):
print(c.get())
_quit = 1
if __name__ == "__main__":
c = queue.Queue()
_quit = 0
thread = Thread(target = print_quit, args=(c,))
thread.start()
fibonacci(c)
thread.join()
select中的default分支将会被执行,如果其他分支没有就绪。
通过default可以实现无阻塞的send或者receive
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
练习:使用并发和信道比较两个二叉树是否存储相同的序列
我们已经看到了信道在go程间通信的优势,但是如果我们不需要通信或者如果我们仅仅希望保证某时刻仅有一个go程可以访问变量以避免冲突,该怎么做?
这个概念叫做互斥,它的数据结构的常规名称是mutex(互斥锁)
Go的标准库sync.Mutex提供互斥以及两个方法:
- Lock
- Unlock
我们可以通过将代码块使用Lock和Unlock包围的方式定义互斥地执行的代码块
我们也可以使用defer来保证互斥锁将会被unlocked,就像Value方法中的那样
关于defer关键字可以参考官方文档:https://golang.org/ref/spec#Defer_statements
deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller
练习:修改代码使其
- 实现并发
- 添加一个带有互斥锁的映射,用来记录url是否被提取过
参考:
https://gist.github.com/harryhare/6a4979aa7f8b90db6cbc74400d0beb49
https://studygolang.com/articles/12972