持续更新 Go 语言学习进度中 ......

  1. GO语言学习笔记-类型篇 Study for Go! Chapter one - Type - slowlydance2me - 博客园 (cnblogs.com)
  2. GO语言学习笔记-表达式篇 Study for Go ! Chapter two - Expression - slowlydance2me - 博客园 (cnblogs.com)
  3. GO语言学习笔记-函数篇 Study for Go ! Chapter three - Function - slowlydance2me - 博客园 (cnblogs.com)

Study for Go ! Chapter seven - Concurrency

1. What is concurrency ?

  1. concurrency vs parallelism

    • 并发:逻辑上具备同时处理多个任务的能力

    • 并行:物理上在同一时刻执行多个并发任务

  • 通常说的程序是并发设计的,也就是说它允许多个任务同时执行,但实际上并不一定真的在同一时刻发生

  • 单核处理器以间隔方式执行

  • 并行则依赖多核处理器等物理设备,让多个任务真正在同一时刻执行

  • 并行是并发设计的理想执行模式

  • 多线程或多进程是并行的基本条件,但是单线程也可用协程(coroutine)做到并发,尽管协程在单个线程上通过主动切换来实现多任务并发,但它也有自己的优势。除了将因阻塞而浪费的时间找回来以外,还免去了线程切换开销,有着不错的执行效率。协程上运行的多个任务本质上是依旧串行的,加上可控自主调度,所以并不需要做同步处理

  • 即便采用多线程也未必就能并行,Python 就因 GIL 限制,默认只能并发而不能并行,所以很多时候转用 " 多进程 + 协程 " 架构

  • 各种方式都有各自的使用场景,通常情况下,用多进程来实现分布式和负载平衡,减轻单进程垃圾回收能力;用多线程 (LWP)抢夺更多的处理器资源;用协程来提高处理器时间片利用率

  • 而简单将 goroutine 归纳为协程并不合适,运行时会创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行,这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力

  • 只需在函数调用前添加 go 关键字即可创建并发任务

  • 关键字 go 并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序

  • 每个任务单元除保存函数指针,调用参数外,还会分配执行所需的栈内存空间

  • 相比 默认 MB 级别的线程栈, goroutine 自定义栈初试仅需 2 KB,所以才能创建成千上万的并发任务

  • 自定义栈采取按需分配策略,在需要时进行扩容,最大能到 GB 规模

  • 与 defer 一样, goroutine 也会因 “ 延迟执行 ” 而立即计算并复制执行参数

Wait

  • 进程推出时不会等待并发任务结束,可用通道 (channel)阻塞,然后发出退出信号

  • 除关闭通道外,写入数据也可解除阻塞。

  • 如想要等待多个任务结束,推荐使用 sync.WaitGroup。通过设定计数器,让每个 goroutine 在退出前递减,直至归零时解除阻塞

  • 尽管 WaitGroup.Add 实现了原子操作,但建议在goroutine 外累加计数器,以免 Add 尚未执行,Wait 已经退出

  • 可在多处使用 Wait 阻塞,它们都能接收到通知

GOMAXPROCS

  • 运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行,该数量默认与处理器核数相等,可用 runtime.GOMAXPROCS 函数 (或环境变量)修改

    (如参数小于1,GOMAXPROCS 仅返回当前设置值,不做任何调整)

Local Storage

  • 与线程不同,goroutine 任务无法设置优先级,无法获取编号,没有局部存储(TLS),甚至连返回值都会被抛弃。但除优先级外,其他功能都很容易实现

  • 使用 map 作为局部存储容器,当前任务被放回队列,等待下次调度时恢复执行

Gosched

  • 暂停,释放线程去执行其他任务。当前任务被放回队列,等待下次调度时恢复执行

  • 该函数很少被使用,因为运行时会主动向长时间(10 ms)的任务发出抢占调度,只是当前版本实现的算法稍显粗糙,不能保证调度总能成功,所以主动切换还有使用场合

Goexit

  • Goexit 立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不会影响其他并发任务,不会引发 panic,自然也无法捕获

  • 如果在main.main 里调用 Goexit,它会等待其他任务结束,然后让进程直接崩溃

  • 无论身处那一层,Goexit 都能立即终止整个调用堆栈,这与 return 仅退出当前函数不同,标准库函数 os.Exit 可终止进程,但不会执行延迟调用

2. Channel

  • 相比 Erlang,golang 并未实现严格的并发安全

  • 允许全局变量、指针、引用类型这些非安全内存共享操作,就需要开发人员自行维护数据一致性和完整性。golang 鼓励使用 CSP 通道,以通信来代替内存安全共享,实现并发安全

GO语言学习笔记-并发篇 Study for Go !  Chapter seven - Concurrency-LMLPHP

  • 通过消息来避免竞态的模型除了 CSP,还有Actor。它们有较大区别

  • 作为 CSP 核心,通道 (channel)是显式的,要求操作双方必须知道数据类型和具体通道,并不关系另一端操作者身份和数量。可如果另一端未准备妥当,或消息未能即使处理时,会阻塞当前端

  • 而 Actor 是透明的,它不在乎数据类型及通道,只要知道接收者信箱即可,默认是异步方式,发送方对消息是否被接受和处理并不关心。

  • 从层次实现上来说,通道只是一个队列,同步模式下,发送和接受双方配对,然后直接复制数据给对方,如果配对失败,则置入等待队列,直到另一方出现后才被唤醒。异步模式抢夺的则是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。需求不符时,同样加入等待队列,直到有另一方写入数据或腾出空槽后被唤醒

  • 除传递消息(数据)外,通道还常被用作事件通知

  • 同步模式必须有配对操作的 goroutine 出现,否则会一直阻塞。而异步模式在缓冲区未满或数据未读完前,不会阻塞

  • 多数时候,异步通道有助于提升性能,减少排队阻塞

  • 缓冲区大小只是内部属性,不属于类型组成成分,另外通道变量本身就是指针,可用相等操作符判断是否为同一对象或 nil

  • 内置函数 cap 和 len 返回缓冲区大小和当前已缓冲数量;而对于同步通道则都返回 0,据此可判断通道是同步还是异步

收发

  • 除了使用简单的发送和接受操作符以外,还可以用 ok-idom 或 range 模式处理数据

  • 对于循环接受数据,range 模式更简洁一点

  • 及时使用 close 函数关闭通道引发结束通知,否则可能会导致死锁

  • 通知可以是群体性的,也未必就是结束,可以是任何需要表达的事件

    ![image-20230307164012213](C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20230307164012213.png)

  • 对于 closed 或 nil 通道,发送和接受都有相规则:

    • 向已关闭通道发送数据,引发 panic

    • 从已关闭通道接收数据,返回已缓冲数据或 0 值

    • 无论收发,nil 通道都会阻塞

  • 重复关闭,或关闭 nil 通道 都会引发 panic

单向

  • 通道默认是双向的,并不区分发送和接收端。但某些时候,我们可限制收发操作的方向,来获得更严谨的操作逻辑

  • 尽管可用 make 创建 单向通道,但那没有任何意义。通常使用类型转换来获取单向通道,并分别赋予操作双方

  • 不能再单向通道上做逆向操作

  • 同样, close 不能用于接收端

  • 也无法将单向通道重新转换回去

选择

  • 如要同时处理多个通道,可选用 select 语句,它会随机选择一个可用通道做收发操作

  • 如要等全部通道消息处理结束 ( closed ), 可将已完成通道设置为nil。这样它就会被阻塞,不再被 select 选中

  • 及时是同一通道,也会随机选择case执行

  • 当所有通道都不可用时,select 会执行 default 语句,如此可避开 select 阻塞,但须注意处理外层循环,以免陷入空耗

  • 也可用 default 处理一些默认逻辑

模式

  • 通常使用工厂方法将 goroutine 和通道绑定

  • 介于通道本身就是一个并发安全的队列,可用作 ID generator、Pool 等用途

  • 可用通道实现 信号量 (samaphore)

性能

  • 将发往通道的数据打包,减少传输次数,可有效提升性能。从实现上来说,通道队列依旧使用锁同步机制,单次获取更多数据 (批处理),可改善因频繁加锁造成的性能问题

资源泄漏

  • 通道可能会引发 goroutine leak, 确切地说,是指 goroutine 处于发送或接受阻塞状态,但一直未被唤醒,垃圾回收器并不收集此类资源,导致它们会在等待队列里长久休眠,导致资源泄漏

3. 同步

  • 通道并非用来代替锁的,它们各自有不同的使用场景。通道倾向于解决逻辑层次的并发处理架构,而锁用来保护局部范围内的数据安全

  • 标准库 sync 提供了互斥和读写锁,另有原子操作等,可基本满足日常开发需要,Mutex,RWMutex 的使用并不复杂,只有几个地方需要注意

  • 将 Mutex 作为匿名字段时,相关方法必须实现为 pointer-receiver,否则会因复制导致锁机制失效

GO语言学习笔记-并发篇 Study for Go !  Chapter seven - Concurrency-LMLPHP

  • 应将 Mutex 锁粒度控制在最小范围内,及早释放

  • Mutex 不支持递归锁,即便在同一 goroutine 下也会导致死锁

GO语言学习笔记-并发篇 Study for Go !  Chapter seven - Concurrency-LMLPHP

03-10 11:42