Day864.CSP模型 -Java 并发编程实战

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

CSP模型

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

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

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

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

  • 一种方案支持协程之间以共享内存的方式通信Golang 提供了管程原子类来对协程进行同步控制这个方案与 Java 语言类似
  • 另一种方案支持协程之间以消息传递Message-Passing的方式通信本质上是要避免共享Golang 的这个方案是基于 CSPCommunicating 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 中协程之间通信推荐的是使用 channelchannel 你可以形象地理解为现实世界里的管道

另外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 并没有经过广泛的生产环境检验所以并不建议在生产环境中使用。


阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java