day864.csp模型-java并发编程实战(代码片段)

阿昌喜欢吃黄桃 阿昌喜欢吃黄桃     2023-03-29     522

关键词:

CSP模型

Hi,我是阿昌,今天学习记录的是关于CSP模型的内容。

CSP模型,是通过以消息传递(Message-Passing)的方式多线程通信,以通信方式共享内存

Golang 是一门号称从语言层面支持并发的编程语言,支持并发是 Golang 一个非常重要的特性。在协程,Golang 支持协程,协程可以类比 Java 中的线程,解决并发问题的难点就在于线程(协程)之间的协作。那 Golang 是如何解决协作问题的呢?

总的来说,Golang 提供了两种不同的方案:

  • 一种方案支持协程之间以共享内存的方式通信,Golang 提供了管程原子类来对协程进行同步控制,这个方案与 Java 语言类似;
  • 另一种方案支持协程之间以消息传递(Message-Passing)的方式通信,本质上是要避免共享,Golang 的这个方案是基于 CSP(Communicating Sequential Processes)模型实现的。

Golang 比较推荐的方案是后者。


一、什么是 CSP 模型

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 := from
    for i := from + 1; i <= to; i++ 
      result += i
    
        // 将结果写入channel
    ch <- result
  ()
    // 返回结果是用于通信的channel
  return 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 := <-ch
      fmt.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 中发送数据,会导致发送消息的协程阻塞。

  • 第三个区别则是关于消息送达的。 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 并没有经过广泛的生产环境检验,所以并不建议在生产环境中使用。


day861.actor模型-java并发编程实战(代码片段)

Actor模型:面向对象原生的并发模型Hi,我是阿昌,今天学习记录的是关于Actor模型:面向对象原生的并发模型的内容。上学的时候,有门计算机专业课叫做面向对象编程,学这门课的时候有个问题困扰࿰... 查看详情

day861.actor模型-java并发编程实战(代码片段)

Actor模型:面向对象原生的并发模型Hi,我是阿昌,今天学习记录的是关于Actor模型:面向对象原生的并发模型的内容。上学的时候,有门计算机专业课叫做面向对象编程,学这门课的时候有个问题困扰࿰... 查看详情

day839.并发容器-java并发编程实战(代码片段)

并发容器Hi,我是阿昌,今天学习记录的是关于并发容器。Java1.5之前提供的同步容器虽然也能保证线程安全,但是性能很差。Java1.5版本之后提供的并发容器在性能方面则做了很多优化,并且容器的类型也更加丰富... 查看详情

day839.并发容器-java并发编程实战(代码片段)

并发容器Hi,我是阿昌,今天学习记录的是关于并发容器。Java1.5之前提供的同步容器虽然也能保证线程安全,但是性能很差。Java1.5版本之后提供的并发容器在性能方面则做了很多优化,并且容器的类型也更加丰富... 查看详情

day821.并发编程bug的源头-java并发编程实战(代码片段)

并发编程Bug的源头Hi,我是阿昌,今天学习记录的是关于并发编程Bug的源头。哪一门编程语言,并发类的知识都是在高级篇里。换句话说,这块知识点对于程序员来说,是比较进阶的知识。这么多年学习过来&#x... 查看详情

day821.并发编程bug的源头-java并发编程实战(代码片段)

并发编程Bug的源头Hi,我是阿昌,今天学习记录的是关于并发编程Bug的源头。哪一门编程语言,并发类的知识都是在高级篇里。换句话说,这块知识点对于程序员来说,是比较进阶的知识。这么多年学习过来&#x... 查看详情

day846.并发工具类一些问题-java并发编程实战(代码片段)

并发工具类一些问题Hi,我是阿昌,今天学习记录的是关于并发工具类一些问题的内容。关于JavaSDK提供的并发工具类(JUC),这些工具类都是久经考验的,所以学好用好它们对于解决并发问题非常重要。在... 查看详情

day847.immutability模式-java并发编程实战(代码片段)

...bility模式的内容。“多个线程同时读写同一共享变量存在并发问题”,这里的必要条件之一是读写,如果只有读,而没有写,是没有并发问题的。解决并发问题,其实最简单的办法就是让共享变量只有读操作&#... 查看详情

day828.多线程原语:管程-java并发编程实战(代码片段)

...f0c;今天学习记录的是关于多线程原语:管程的内容。并发编程这个技术领域已经发展了半个世纪了,相关的理论和技术纷繁复杂。那有没有一种核心技术可以很方便地解决的并发问题呢?这个问题如果让我选择,... 查看详情

day828.多线程原语:管程-java并发编程实战(代码片段)

...f0c;今天学习记录的是关于多线程原语:管程的内容。并发编程这个技术领域已经发展了半个世纪了,相关的理论和技术纷繁复杂。那有没有一种核心技术可以很方便地解决的并发问题呢?这个问题如果让我选择,... 查看详情

day836.readwritelock-java并发编程实战(代码片段)

...0c;理论上用这两个同步原语中任何一个都可以解决所有的并发问题。那JavaSDK并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性。一种非常普遍的并发场景& 查看详情

day836.readwritelock-java并发编程实战(代码片段)

...0c;理论上用这两个同步原语中任何一个都可以解决所有的并发问题。那JavaSDK并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性。一种非常普遍的并发场景& 查看详情

day863.协程-java并发编程实战(代码片段)

...,今天学习记录的是关于协程的内容。Java语言里解决并发问题靠的是多线程,但线程是个重量级的对象,不能频繁创建、销毁,而且线程切换的成本也很高,为了解决这些问题,JavaSDK提供了线程池。然而... 查看详情

day863.协程-java并发编程实战(代码片段)

...,今天学习记录的是关于协程的内容。Java语言里解决并发问题靠的是多线程,但线程是个重量级的对象,不能频繁创建、销毁,而且线程切换的成本也很高,为了解决这些问题,JavaSDK提供了线程池。然而... 查看详情

day858.高性能网络应用框架netty-java并发编程实战(代码片段)

...富,也非常复杂,今天主要分析Netty框架中的线程模型,而 查看详情

day862.stm软件事务内存-java并发编程实战(代码片段)

...软件事务内存的内容。工作了挺长时间但是没有机会接触并发编程,实际上天天都在写并发程序,只不过并发相关的问题都被类似Tomcat这样的Web服务器以及MySQL这样的数据库解决了。尤其是数据库,在解决并发问题方... 查看详情

day823.java原子性问题解决方案-java并发编程实战(代码片段)

...,今天学习记录的是关于Java原子性问题解决方案。在并发编程Bug的源头中提到,一个或者多个操作在CPU执行的过程中不被中断的特性,称为“原子性”。理解这个特性有助于分析并发编程Bug出现的原因,例如利用... 查看详情

day823.java原子性问题解决方案-java并发编程实战(代码片段)

...,今天学习记录的是关于Java原子性问题解决方案。在并发编程Bug的源头中提到,一个或者多个操作在CPU执行的过程中不被中断的特性,称为“原子性”。理解这个特性有助于分析并发编程Bug出现的原因,例如利用... 查看详情