Java并发编程实战~协程
Golang 是一门号称从语言层面支持并发的编程语言,支持并发是 Golang 一个非常重要的特性。在上一篇文章《44 | 协程:更轻量级的线程》中我们介绍过,Golang 支持协程,协程可以类比 Java 中的线程,解决并发问题的难点就在于线程(协程)之间的协作。
那 Golang 是如何解决协作问题的呢?
总的来说,Golang 提供了两种不同的方案:一种方案支持协程之间以共享内存的方式通信,Golang 提供了管程和原子类来对协程进行同步控制,这个方案与 Java 语言类似;另一种方案支持协程之间以消息传递(Message-Passing)的方式通信,本质上是要避免共享,Golang 的这个方案是基于 CSP(Communicating Sequential Processes)模型实现的。Golang 比较推荐的方案是后者。
什么是 CSP 模型
我们在《42 | Actor 模型:面向对象原生的并发模型》中介绍了 Actor 模型,Actor 模型中 Actor 之间就是不能共享内存的,彼此之间通信只能依靠消息传递的方式。Golang 实现的 CSP 模型和 Actor 模型看上去非常相似,Golang 程序员中有句格言:“不要以共享内存方式通信,要以通信方式共享内存(Don’t communicate by sharing memory, share memory by communicating)。”虽然 Golang 中协程之间,也能够以共享内存的方式通信,但是并不推荐;而推荐的以通信的方式共享内存,实际上指的就是协程之间以消息传递方式来通信。
下面我们先结合一个简单的示例,看看 Golang 中协程之间是如何以消息传递的方式实现通信的。我们示例的目标是打印从 1 累加到 100 亿的结果,如果使用单个协程来计算,大概需要 4 秒多的时间。单个协程,只能用到 CPU 中的一个核,为了提高计算性能,我们可以用多个协程来并行计算,这样就能发挥多核的优势了。
在下面的示例代码中,我们用了 4 个子协程来并行执行,这 4 个子协程分别计算[1, 25 亿]、(25 亿, 50 亿]、(50 亿, 75 亿]、(75 亿, 100 亿],最后再在主协程中汇总 4 个子协程的计算结果。主协程要汇总 4 个子协程的计算结果,势必要和 4 个子协程之间通信,Golang 中协程之间通信推荐的是使用 channel,channel 你可以形象地理解为现实世界里的管道。另外,calc() 方法的返回值是一个只能接收数据的 channel ch,它创建的子协程会把计算结果发送到这个 ch 中,而主协程也会将这个计算结果通过 ch 读取出来。
import ("fmt""time"
)
func main() {// 变量声明var result, i uint64// 单个协程执行累加操作start := time.Now()for i = 1; i <= 10000000000; i++ {result += i}// 统计计算耗时elapsed := time.Since(start)fmt.Printf("执行消耗的时间为:", elapsed)fmt.Println(", result:", result)// 4个协程共同执行累加操作start = time.Now()ch1 := calc(1, 2500000000)ch2 := calc(2500000001, 5000000000)ch3 := calc(5000000001, 7500000000)ch4 := calc(7500000001, 10000000000)// 汇总4个协程的累加结果result = <-ch1 + <-ch2 + <-ch3 + <-ch4// 统计计算耗时elapsed = time.Since(start)fmt.Printf("执行消耗的时间为:", elapsed)fmt.Println(", result:", result)
}
// 在协程中异步执行累加操作,累加结果通过channel传递
func calc(from uint64, to uint64) <-chan uint64 {// channel用于协程间的通信ch := make(chan uint64)// 在协程中执行累加操作go func() {result := fromfor i := from + 1; i <= to; i++ {result += i}// 将结果写入channelch <- result}()// 返回结果是用于通信的channelreturn ch
}
CSP 模型与生产者 - 消费者模式
你可以简单地把 Golang 实现的 CSP 模型类比为生产者 - 消费者模式,而 channel 可以类比为生产者 - 消费者模式中的阻塞队列。不过,需要注意的是 Golang 中 channel 的容量可以是 0,容量为 0 的 channel 在 Golang 中被称为无缓冲的 channel,容量大于 0 的则被称为有缓冲的 channel。
无缓冲的 channel 类似于 Java 中提供的 SynchronousQueue,主要用途是在两个协程之间做数据交换。比如上面累加器的示例代码中,calc() 方法内部创建的 channel 就是无缓冲的 channel。
而创建一个有缓冲的 channel 也很简单,在下面的示例代码中,我们创建了一个容量为 4 的 channel,同时创建了 4 个协程作为生产者、4 个协程作为消费者。
// 创建一个容量为4的channel
ch := make(chan int, 4)
// 创建4个协程,作为生产者
for i := 0; i < 4; i++ {go func() {ch <- 7}()
}
// 创建4个协程,作为消费者
for i := 0; i < 4; i++ {go func() {o := <-chfmt.Println("received:", o)}()
}
Golang 中的 channel 是语言层面支持的,所以可以使用一个左向箭头(<-)来完成向 channel 发送数据和读取数据的任务,使用上还是比较简单的。Golang 中的 channel 是支持双向传输的,所谓双向传输,指的是一个协程既可以通过它发送数据,也可以通过它接收数据。
不仅如此,Golang 中还可以将一个双向的 channel 变成一个单向的 channel,在累加器的例子中,calc() 方法中创建了一个双向 channel,但是返回的就是一个只能接收数据的单向 channel,所以主协程中只能通过它接收数据,而不能通过它发送数据,如果试图通过它发送数据,编译器会提示错误。对比之下,双向变单向的功能,如果以 SDK 方式实现,还是很困难的。
CSP 模型与 Actor 模型的区别
同样是以消息传递的方式来避免共享,那 Golang 实现的 CSP 模型和 Actor 模型有什么区别呢?
第一个最明显的区别就是:Actor 模型中没有 channel。虽然 Actor 模型中的 mailbox 和 channel 非常像,看上去都像个 FIFO 队列,但是区别还是很大的。Actor 模型中的 mailbox 对于程序员来说是“透明”的,mailbox 明确归属于一个特定的 Actor,是 Actor 模型中的内部机制;而且 Actor 之间是可以直接通信的,不需要通信中介。但 CSP 模型中的 channel 就不一样了,它对于程序员来说是“可见”的,是通信的中介,传递的消息都是直接发送到 channel 中的。
第二个区别是:Actor 模型中发送消息是非阻塞的,而 CSP 模型中是阻塞的。Golang 实现的 CSP 模型,channel 是一个阻塞队列,当阻塞队列已满的时候,向 channel 中发送数据,会导致发送消息的协程阻塞。
第三个区别则是关于消息送达的。在《42 | Actor 模型:面向对象原生的并发模型》这篇文章中,我们介绍过 Actor 模型理论上不保证消息百分百送达,而在 Golang 实现的 CSP 模型中,是能保证消息百分百送达的。不过这种百分百送达也是有代价的,那就是有可能会导致死锁。
比如,下面这段代码就存在死锁问题,在主协程中,我们创建了一个无缓冲的 channel ch,然后从 ch 中接收数据,此时主协程阻塞,main() 方法中的主协程阻塞,整个应用就阻塞了。这就是 Golang 中最简单的一种死锁。
func main() {// 创建一个无缓冲的channel ch := make(chan int)// 主协程会阻塞在此处,发生死锁<- ch
}
总结
Golang 中虽然也支持传统的共享内存的协程间通信方式,但是推荐的还是使用 CSP 模型,以通信的方式共享内存。
Golang 中实现的 CSP 模型功能上还是很丰富的,例如支持 select 语句,select 语句类似于网络编程里的多路复用函数 select(),只要有一个 channel 能够发送成功或者接收到数据就可以跳出阻塞状态。鉴于篇幅原因,我就点到这里,不详细介绍那么多了。
CSP 模型是托尼·霍尔(Tony Hoare)在 1978 年提出的,不过这个模型这些年一直都在发展,其理论远比 Golang 的实现复杂得多,如果你感兴趣,可以参考霍尔写的Communicating Sequential Processes这本电子书。另外,霍尔在并发领域还有一项重要成就,那就是提出了霍尔管程模型,这个你应该很熟悉了,Java 领域解决并发问题的理论基础就是它。
Java 领域可以借助第三方的类库JCSP来支持 CSP 模型,相比 Golang 的实现,JCSP 更接近理论模型,如果你感兴趣,可以下载学习。不过需要注意的是,JCSP 并没有经过广泛的生产环境检验,所以并不建议你在生产环境中使用。
Java并发编程实战~协程相关推荐
- 《Java 并发编程实战》--读书笔记
Java 并发编程实战 注: 极客时间<Java 并发编程实战>–读书笔记 GitHub:https://github.com/ByrsH/Reading-notes/blob/maste ...
- JAVA并发编程实战-任务执行
目录 思维导图 1 在线程中执行任务 1.1 顺序执行任务 1.2 显式的为任务创建线程 1.3 无限制创建线程的缺点 2 Executor框架 2.1 使用Executor实现WebServer 2 ...
- python并发编程:协程asyncio、多线程threading、多进程multiprocessing
python并发编程:协程.多线程.多进程 CPU密集型计算与IO密集型计算 多线程.多进程与协程的对比 多线程 创建多线程的方法 多线程实现的生产者-消费者爬虫 Lock解决线程安全问题 使用线程池 ...
- 学习笔记:Java 并发编程②_管程
若文章内容或图片失效,请留言反馈. 部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 视频链接:https://www.bilibili.com/video/av81461839 配套资料: ...
- 【极客时间】《Java并发编程实战》学习笔记
目录: 开篇词 | 你为什么需要学习并发编程? 内容来源:开篇词 | 你为什么需要学习并发编程?-极客时间 例如,Java 里 synchronized.wait()/notify() 相关的知识很琐 ...
- Java并发编程实战基础概要
文章目录 Java并发编程实战基础概要 开篇 多线程问题有啥难点呢? 为啥要学习并发编程? 并发问题的根源是什么? CPU切换线程执导致的原子性问题是如何发生的? 缓存导致的可见性问题是如何发生的? ...
- JAVA并发编程实战——共享对象
目录 思维导图 1. 可见性 1. 1 过期数据 1.2 锁和可见性 1.3 Volatile变量 2. 发布和逸出 2.1 安全构建实践 3. 线程封闭 3.1 栈限制 3.2 ThreadLoca ...
- Java并发编程实战笔记2:对象的组合
设计线程安全的类 在设计现车让安全类的过程之中,需要包含以下三步: 找出构成对象状态的所有变量 找出约束状态变量的不变性条件 建立对象状态的并发访问策略 实例封闭 通过封闭机制与合适的加锁策略结合起来 ...
- python并发编程之协程
python并发编程之协程 1.协程: 单线程实现并发 在应用程序里控制多个任务的切换+保存状态 优点: 应用程序级别速度要远远高于操作系统的切换 缺点: 多个任务一旦有一个阻塞没有切,整个线程都阻塞 ...
最新文章
- tensorflow学习(6.Alexnet实现及猫狗分类)
- 各视频、各音频之间格式任意玩弄(图文详解)
- 硕博士的朋友圈都有哪些特点?
- leetcode 781. 森林中的兔子(hashmap)
- 如何将sql server数据库转化成sqlite数据库
- Python中68个内置函数的总结
- php 遍历所有网站网址,使用selenium获取网址所加载所有资源url列表信息
- bitnami redmine mysql_linux下bitnami一键安装redmine后无法远程访问mysql的问题
- 计算机病毒正确探讨目录,毕业论文-计算机病毒的正确防御探讨.doc
- IBM DB2各版本下载地址
- IT战略规划项目方法论(德勤、埃森哲、IBM、凯捷)
- 无法确认设备和计算机之间的连接,代码45的8种解决方法 - 硬件设备没有连接到计算机...
- 怎么在win10服务器上搭建网站,win10怎么搭建web服务器 win10搭建web服务器的方法...
- 计算机专业论文摘要,★计算机专业概论论文摘要范文计算机专业概论论文摘要写(8页)-原创力文档...
- 常用上市公司控制变量数据集
- C语言 文件操作 深度解析 #重点知识:文件操作函数的使用#
- 计算机网络应用技术技能大赛
- 主流报表开发工具FastReport.Net全新发布,邀您体验最新版试用
- eclipse快捷键及各种设置
- JavaCV开发详解之36:使用delogo滤镜实现去除和屏蔽视频水印