CSP与并发编程


随着Go语言的逐渐走红,CSP(Communicating Sequential Process)并发模型也更多地被程序员所谈论。

计算机科学中,CSP是一种描述并发系统交互模式的形式化语言,其交互模式是通过channel进行消息传递。Go语言借鉴了CSP模型的理论,使用goroutine(一种运行在用户态的协程)和在goroutine之间收发消息的channel实现了高效并发。为什么Go语言要使用CSP模型来实现并发呢?


本文将做一些粗浅的探讨,最后用支持CSP并发模型的编程语言实现简短的并发程序,并与Java实现的并发程序做性能比较。





先来看看几种并发编程模型。



多线程 


提起并发编程,最常见的就是多线程编程。线程是操作系统能够进行调度的最小单位,共享同一进程的数据和资源,有内核线程和用户线程之分,由操作系统或用户进程调度。多线程的程序可以利用多核CPU,并行地处理多个任务。随着并发量增大,线程数增加,多线程的并发模型面临一些问题。

  内存占用  


64位JVM线程默认栈空间是1M,启动1024个线程理论上消耗1G的栈空间。由于线程需要内存较多,为避免内存耗尽,应用程序不应该大量创建线程。

 线程调度 


操作系统对线程进行调度也需要成本。线程挂起前会保存线程上下文到栈空间,再切换到可执行线程。线程数很多时,线程上下文切换会导致CPU开销变大。


使用线程池技术可以对多线程并发进行优化,线程池实现线程复用,避免频繁创建新线程和线程切换,控制了线程数量,减少了内存的消耗。但是在竞争共享数据的时候,需要用加锁来保护共享数据,这样也降低了程序的并发效率。



异步回调


为了充分利用CPU,不让线程空等待,在线程阻塞的时候,注册一个回调方法,让当前线程不再阻塞,去处理新的请求。等结果准备好,调度器把结果传给回调方法,在回调方法中继续处理结果。然而回调方法并不在发起请求的线程里执行。


异步回调的缺点是所谓的callback hell。原本顺序同步的执行逻辑拆分到回调方法中,而且回调方法中可能再嵌套回调方法。这种写程序的方式还是让很多程序员不太习惯。异步回调的典型实现是NodeJS,目前也有一些第三方模块将异步代码同步化。



协程(纤程)


协程(纤程)也是一种异步方案。在代码IO阻塞时,当前协程让出CPU执行权,让其它协程执行。待IO操作完毕,阻塞的协程继续执行。虽然代码是异步执行,但写代码看起来像是同步的。协程是用户态的轻量级线程,现在的机器可以启动百万数量的协程。支持协程的编程语言实现了协程的调度器,提供了channel机制进行协程间通信(CSP模型中消息传递的实现)。基于CSP模型的协程方案,实现了无共享内存无锁的并发,可以匹配异步回调的性能。





CSP模型最初于Tony Hoare的1977年的论文中被描述,影响了许多编程语言的设计。

下面从被CSP模型所影响的编程语言中选择一个无名小卒,写一段简短的例子程序完成一个计算任务。



任务:计算0, 1, 2, ... 9999999的和。

硬件环境:4核i7-3520M 2.90GHz, MemTotal:       16121944 kB


N = 10 * 1000 * 1000
M = N / 4t1 = Time.now channel = Channel(Int32).new(M) #使用单线程4协程
4.times { |i|  spawn {    M.times { |j|      channel.send(i * M + j)    }  } } sum = 0_i64 N.times {  sum += channel.receive } t2 = Time.now puts "\telapsed time: #{t2 - t1}, sum=#{sum}"

运行结果:



Java1.8.0_121版本的例子程序

 public class Java8Language {
  private static final int TIMES = 10 * 1000 * 1000;  
 
  public static void main(String[] args) throws Exception {  
    final CountDownLatch c = new CountDownLatch(TIMES);    
    final Accumulator a = new Accumulator(c);    long t1 = System.currentTimeMillis();     ExecutorService service = Executors.newFixedThreadPool(4);    
    for (long i=0;i<TIMES;i++) {  
       final Long k = i;        service.submit(() -> {          a.add(k);          c.countDown();       });     }       c.await();    
    long t2 = System.currentTimeMillis();     System.out.printf("\telapsed time: %.3fs, sum=%d\n", (t2-t1)/1000f,
a.getSum().longValue());     service.shutdown();  } }

class Accumulator {
  private AtomicLong sum = new AtomicLong(0L);  
  private CountDownLatch c;  
  public Accumulator(CountDownLatch c) {  
    this.c = c;   }  
  AtomicLong getSum() {  
    return sum;   }  
  public void add(long k) {     sum.addAndGet(k);  } }

运行结果:



从结果来看,时间差距还是挺大的。



END

有兴趣的读者还可以用Go语言实现程序来比较。虽然Java在执行时间上落败,但这只是语言特性的原因。从解决实际的高并发问题来看,Java还有第三方库和框架可用。





点击回顾往期精彩内容

使用 Mesos 管理 Docker 实践

来共享密钥吧

浅谈交易手续费设计#1

多Header or Footer的RecyclerViewAdapter

浅谈 golang channel

你有掉进过“心理账户”的坑么?

基于HTML5实现跨文档通信

大咖分享|互联网金融投资产品的体验设计

走近黑帮|了不起的点融,了不起的黑帮

TextKit框架介绍

LING距离|孔令欣对话有赞 CEO 白鸦:CEO 与 CTO 的“爱恨情仇”

浅谈质量管理 - 有关JIRA的那些事儿

点融账户系统建设

Iconfont 多色图标和渐变色图标的应用



想了解更多请关注我们



相关文章
相关标签/搜索