2025年1月1日
Learn Concurrent Programming With Go
学习 Go 并发编程

Learn Concurrent Programming With Go
书中详细介绍了Go语言的并发编程,涵盖了并发编程的基础知识、常见的并发模式(如管道和工作池)、线程间的通信方式(内存共享和消息传递)、以及如何避免死锁和竞争条件等内容。 书中使用了大量的代码示例和图表,帮助读者理解并掌握Go语言中的并发编程技巧,并提升软件性能和响应速度。
第一章:步入并发编程
- 并发(Concurrency) 是一种程序代码的属性,它将指令分组为独立的任务,并规划任务之间的边界和同步点。并发存在于我们日常生活和工作中,例如多人同时在线购物或汽车行驶。
- 并行(Parallelism) 是程序执行的属性,它指同时执行多个任务。并行是并发的子集,只有并发程序才能并行执行。
- Go 语言 在设计时就考虑了高性能的并发,因此是学习并发编程的理想选择。Go 使用 goroutine 作为基本的并发执行单元,它是一种轻量级的用户级线程,由 Go 的运行时管理。
- Go 提供了两种并发模型:通信顺序进程(CSP) 模型和传统的 内存共享 模型。CSP 模型通过消息传递进行通信,而内存共享模型使用互斥锁和条件变量等原语进行同步。
第二章:处理线程
- 操作系统 将作业的状态分为多个阶段,包括等待执行、正在执行、等待 I/O 等。
- 绿色线程 (green threads) 是用户级线程的一种实现,但 Go 的 goroutine 可以充分利用多核 CPU。
- 并发 是关于如何同时做多件事的计划,而 并行 是关于同时执行多件事。
- 即使只有一个处理器,通过频繁的 上下文切换 ,也可以实现伪并行执行。
第三章:使用内存共享进行线程通信
- 竞态条件 (race condition) 指多个 goroutine 并发访问共享资源时,由于执行顺序不确定而导致结果错误。
- 原子操作 (atomic operation) 是不可中断的操作,可以避免竞态条件。
- 为了避免竞态条件,需要进行 同步 和 通信 。Go 提供了多种同步工具,例如互斥锁(mutex)和条件变量。
- Go 的竞态检测器可以帮助我们找到代码中的竞态条件。
第四章:使用互斥锁进行同步
- 互斥锁(Mutex) 是一种并发控制机制,它只允许一个执行单元(例如 goroutine)进入 临界区。
- Go 的
sync
包提供了Mutex
类型,它提供了Lock()
和Unlock()
操作来保护临界区。 - 当多个 goroutine 同时尝试锁定互斥锁时,只有一个 goroutine 会成功获得锁,其他 goroutine 将被挂起,直到锁被释放。
TryLock()
操作尝试获取锁,如果锁不可用,则立即返回false
,而不会阻塞。- 读写锁(RWMutex) 允许多个 goroutine 同时读取共享资源,但只允许一个 goroutine 写入共享资源。
- 读优先 的读写锁可能会导致 写饥饿 ,即写 goroutine 无法获得锁。
第五章:条件变量和信号量
- 条件变量(Condition Variable) 提供了在互斥锁基础上额外的功能,允许 goroutine 等待特定条件发生。
Wait()
操作会原子地释放互斥锁并挂起 goroutine 的执行。Signal()
或Broadcast()
操作会唤醒等待在条件变量上的 goroutine。- 如果调用
Signal()
或Broadcast()
时没有 goroutine 在等待,则信号会被丢失。 - 可以使用条件变量实现 写优先 的读写锁,避免写饥饿。
- 信号量(Semaphore) 用于控制允许同时执行临界区的 goroutine 的数量。信号量可以记录信号,即使没有 goroutine 在等待。
- 二元信号量 是一种只允许一个许可证的信号量,类似于互斥锁。
第六章:使用 WaitGroup 和 Barrier 进行同步
- WaitGroup 用于等待一组任务完成。
Add(delta int)
增加 WaitGroup 的计数器。Done()
减少 WaitGroup 的计数器。Wait()
阻塞直到计数器变为 0。
- 可以使用信号量或条件变量来实现 WaitGroup。
- Barrier 用于同步多个 goroutine 到达一个共同点。所有 goroutine 必须都到达 barrier 才能继续执行.
- 可以使用条件变量来实现 Barrier.
- WaitGroup 用于等待一组任务完成。
第七章:使用消息传递进行通信
- 通道(Channel) 是 goroutine 之间通信的管道,它可以是 无缓冲 的或 有缓冲 的。
- 使用
make(chan T)
创建无缓冲通道,使用make(chan T, capacity)
创建有缓冲通道。 <-
操作符用于发送和接收消息,发送时通道在左边,接收时通道在右边。- 无缓冲通道的发送和接收操作是同步的,发送方会阻塞直到接收方接收消息。
- 有缓冲通道可以在缓冲区未满时允许发送方继续发送消息。
- 通道可以指定发送或接收方向,例如
chan<- int
表示只能发送的通道,<-chan int
表示只能接收的通道。 - 可以使用
close(channel)
关闭通道,关闭后不能再向通道发送消息。 - 从已关闭的通道接收消息会得到通道类型的零值,例如
int
类型的通道返回 0. - 可以使用
for range
循环从通道接收消息,直到通道关闭. - 通道内部类似于固定大小的队列数据结构,需要使用互斥锁保护共享数据结构.
- 可以使用信号量来阻塞发送方或接收方,当缓冲区满时阻塞发送方,当缓冲区空时阻塞接收方.
第八章:选择通道
select
语句允许一个 goroutine 从多个通道读取消息。- 当多个
case
都准备好时,select
语句会随机选择一个case
执行。 - 可以使用
select
语句进行 非阻塞 通道操作,当通道没有消息时,执行default
case。 - 可以将
default
case 用于并发计算,并使用一个通道来发出停止信号. - 可以将通道的 nil 值用作禁用
select
语句中的case
,从而实现动态地添加和删除通信源。 - 可以使用
select
语句将多个通道的数据合并到一个流中,这种模式称为 扇入 (fan-in). - 消息传递 比 内存共享 更容易理解,代码更简洁。
- 松耦合 的系统更容易测试和维护。
- 在某些情况下,内存共享可能比消息传递性能更好.
第九章:使用通道的常见模式
- 哨兵值 (Sentinel Value) 或 毒丸消息 (Poison Pill Message) 是用于通知执行终止的预定义值.
- 扇出(fan-out)模式用于将一个计算的输出分发给多个并发的 goroutine.
- 可以使用
select
语句和动态通道来实现更复杂的扇入模式,动态地合并多个数据源. CreateAll()
和CloseAll()
函数是使用动态通道时常用的辅助函数.
第十章:并发模式
- 可以使用通道在循环的每次迭代中进行并发计算.
- 可以使用通道和
select
语句实现非阻塞的工作池,避免因工作池满而阻塞客户端. - 可以使用通道和映射函数实现可重用的 管道模式.
第十一章:避免死锁
- 死锁 是指两个或多个 goroutine 互相等待对方释放资源而无法继续执行的情况。
- 资源分配图 (Resource Allocation Graph) 可以用于表示系统中资源的分配和请求情况.
- 死锁发生需要同时满足以下四个条件:互斥、等待条件、不可抢占、循环等待.
- 可以使用死锁检测来发现死锁,或使用死锁预防技术来避免死锁.
- 银行家算法 可用于避免死锁,它通过动态地检查资源分配请求来确保系统处于安全状态.
- 可以使用 仲裁器 (Arbitrator) 来避免循环等待导致的死锁,仲裁器用于控制资源的分配.
- 可以使用
select
语句来避免通道之间的循环等待导致的死锁.
第十二章:原子操作,自旋锁和 Futex
- 原子变量 提供了对基本数据类型进行原子操作的能力,例如读取,写入,增加和比较.
- 可以使用原子变量实现计数器和互斥锁等同步机制.
- CompareAndSwap 函数可以原子地比较变量值并进行交换.
- 自旋锁 (spin lock) 是一种完全在用户空间实现的互斥锁,它使用循环等待来获取锁.
- Futex 是一种操作系统调用,允许进程在等待锁释放时减少 CPU 消耗.
- 可以使用 Futex 来优化互斥锁的实现,仅当有等待锁的执行单元时才调用
futex_wakeup()
. - Go 的
sync.mutex
结合了原子操作,自旋锁和 Futex 等多种技术来提高性能. sync.mutex
有 普通模式 和 饥饿模式 两种操作模式,根据情况选择合适的模式.