Hello World
Spiga

编程语言的发展趋势及未来方向(6):并发

2010-05-30 22:53 by 老赵, 9865 visits

这是Anders Hejlsberg(不用介绍这是谁了吧)在比利时TechDays 2010所做的开场演讲。由于最近我在博客上关于语言的讨论比较多,出于应景,也打算将Anders的演讲完整地听写出来。在上一部分中,Anders谈论了“元编程”及他正在努力的“编译器即服务”功能。在这一部分中,Anders则谈论了“并发”,这也是他眼中编程语言发展的三种趋势之一,并演示了.NET 4.0中并行库的神奇效果。

如果没有特别说明,所有的文字都直接翻译自Anders的演讲,并使用我自己的口语习惯表达出来,对于Anders的口误及反复等情况,必要时在译文中自然也会进行忽略。为了方便理解,我也会将视频中关键部分进行截图,而某些代码演示则会直接作为文章内容发表。

(听写开始,接上篇

好,最后我想谈的内容是“并发”。

听说过摩尔定律的请举手……几乎是所有人。那么多少人听说了摩尔定律已经结束了呢?嗯,还是有很多人。我有好消息,也有坏消息。我认为摩尔定律并没有停止。摩尔定律说的是:可以在集成电路上低成本地放置晶体管的数目,约每两年便会增加一倍。有趣的是,这个定律从60年代持续到现在,而从一些迹象上来看,这个定律会继续保持20到30年。

摩尔定理有个推论,便是说时钟速度将根据相同的周期提高,也就是说每隔大约24个月,CPU的速度便会加倍──而这点已经停止了。再来统计一下,你们之中有谁的机器里有20GHz的CPU?看到了没?一个人都没有。但如果你从五年前开始计算的话,现在我们应该已经在使用20GHz的CPU了,但事实并非如此。这点在五年前就停止了,而且事实上最大速度还有些下降,因为发热量实在太大了,会消耗许多能源,让电池用的太快。

有些物理方面的基础因素让CPU不能运行的太快。然而,另一意义上的摩尔定理出现了。我们还是可以看到容量的增加,因为可以在同一个表盘上放置多个CPU了。目前已经有了双核、四核,Intel的CTO在三年前说,十年后我们可以出现80核的处理器。

到了那个时候,你的任务管理器中就可能是这样的。似乎有些吓人,不过这是我们实验室中真实存在的128核机器。你可以看到,计算能力已经完全用上了。这便是个问题,比如你在这台强大的机器上进行一个实验,你自然希望看到100%的使用状况,不过传统的实验都是在一个核上执行的,所以我们面临的挑战是,我们需要换一种写程序的方式来利用此类机器。

我的一个同事,Herb Sutter,他写过一篇文章,谈到“免费的午餐已经结束了”。没错,我们已经不能写一个程序,然后对客户说:啊,未来的硬件会让它运行的越来越快,我们不用关心太多──不,已经不会这样了,除非你换种不同的写法。实话说,这是个挑战,也是个机遇。说它是个挑战,是因为并发十分困难,至今我们对此还没有简单的答案,稍后我会演示一些正有所改善的东西,但……这也是一个机遇,在这样的机器上,你的确可以用完所有的核,这样便能获得性能提高,不过做法需要有所不同。

多核革命的一个有趣之处在于,它对于并发的思维方式会有所改变。传统的并发思维是在单个CPU上执行多个逻辑任务,使用旧有的分时方式、时间片模型来执行多个任务。但是,你想一下便会发现如今的并发情况正好相反,现在是要将一个逻辑上的任务放在多个CPU上执行。这改变了我们编写程序的方式,这意味着对于语言或是API来说,我们需要有办法来分解任务,把它拆分成多个小任务后独立的执行,而传统的编程语言中并不关注这点。

使用目前的并发API来完成工作并不容易,比如使用Thread,ThreadPool,lock,Monitor等等,你无法太好的进展。不过.NET 4.0提供了一些美妙的事物,我们称之为.NET并行扩展。它是一种现代的并发模型,将逻辑上的任务并发与我们实际使用的的物理模型分离开来。以前我们的API都是直接处理线程,也就是(上图)下方橙色的部分,不过有了.NET并行扩展之后,你可以使用更为逻辑化的编程风格。任务并行库(Task Parallel Library),并行LINQ(Parallel LINQ)以及协调数据结构(Coordination Data Structures)让你可以直接关注逻辑上的任务,而不必关心它们是如何运行的,或是使用了多少个线程和CPU等等。

下面我来简单演示一下它们的使用方式。我带来了一个PLINQ演示,这里是一些代码,读取XML文件的内容。这有个50M大小的popname.xml文件,保存了美国社会安全数据库里的信息,包含某个洲在某一年的人口统计信息。这个程序会读取这个XML文件,把它转化成一系列对象,并存放在一个List中。然后对其执行一个LINQ语句,查找所有在华盛顿名叫Robert的人,再根据年份进行排序:

Console.WriteLine("Loading XML data...");
var popNames =
    (from e in XElement.Load("popnames.xml").Elements("Name")
     select new
     {
         Name = (string)e.Attribute("Name"),
         State = (string)e.Attribute("State"),
         Year = (int)e.Attribute("Year"),
         Count = (int)e.Attribute("Count")
     })
    .ToList();

Console.WriteLine(popNames.Count + " records");
Console.WriteLine();

string targetName = "Robert";
string targetState = "WA";

var querySequential =
    from n in popNames
    where n.Name == targetName && n.State == targetState
    orderby n.Year
    select n;

我们来执行一下……首先加载XML文件,然后进行查询。利用PLINQ我们可以做到并行地查询。我们只要拷贝一份代码……改成queryParallel……现在我唯一要做的只是在数据源上使用AsParallel扩展方法,这样便会引入一套新的类型和实现,此时相同的LINQ操作使用的便是并行的实现:

var queryParallel =
    from n in popNames.AsParallel()
    where n.Name == targetName && n.State == targetState
    orderby n.Year
    select n;

我们重新执行两个查询。

再次加载XML数据……并行实现使用了1.5秒,我们再试着运行一次,一般结果会更好一些,现在可能刚好在执行一些后台任务。一般我们可以得到更快的结果……这次比较接近了。现在你可以观察到,我们并不需要做太多事情,便可以在我的双核机器上得到并发的效果。

这里我无法保证说,我们只要随时加上AsParallel便可以得到两倍的性能,有时可以有时不行,有些查询能够被并行,有的则不可以。然而,我想你一定同意一点,使用如LINQ这样的DSL能够方便我们编写并行的代码,也更有可能利用起并行效果。虽然不是每次都有效,但是尝试的成本也很低。如果我们使用普通的for循环来编写代码,在某个地方使用线程池等等,便很容易在这些API里失去方向。而这里我们只要简单地尝试一下,便能知道是否可以提高性能了。

这里你已经看到我使用的LINQ查询,而现在也有很多工作是通过循环来完成的。你可以想象主要的运算是从哪里来的,很自然会是在循环里操作数据。如果循环的每个迭代都是独立的,便有很大的机会可以利用并发操作──我知道这里是“如果”,不过长期来看则一定会出现这样的情况。这时候便可以使用并行扩展,或者说是.NET并行扩展里的新API,把循环转化成并行的循环,只要简单的改变……几乎只要用同样的循环体把for重构成Parallel.For就行了。如果你有foreach操作就可以使用Parallel.ForEach,或是一系列顺序执行的语句也可以用上Parallel.Invoke。此时任务并行库会接管并执行这些任务,根据你的CPU数量使用最优化的线程数量,你不需要关注更深的细节,只需要编写逻辑就可以了。

就像我说的那样,可能你会有独立的任务但也可能没有,所以很多时候我们需要编程语言来关注这方面的事情。比如“隔离性(Isolation)”。例如,编译器如何发现这段代码是独立的,可以安全地并发执行,好比我创建了一个对象,在分享给其他人之前,我对它的改变是安全的。但是我一旦把它们共享出去了,那么它们便不安全了。所以如果我们的类型系统可以跟踪到这样的共享,如Linear Types──这在学术界也有一些研究。我们也可以在函数的纯洁性(Purity)方面下功夫,如关注某个函数是否有副作用,有些时候编译器可以做这方面的检查,它可以禁止某些操作,以此保证我们写出纯函数。还有便是不可变性(Immutability),目前的C#或VB,我们需要额外的工作才能写出不可变的代码──但本不该这样,我们应该在语言层面上更好的支持不可变性。这些都是在并发方面需要考虑的问题。

如果说有哪个语言特性超出这个范畴,我想说这里还有一个原则:你不该期望C#中出现某个特别的并发模型,而应该是一种通用的,可用于各种不同的并发场景的特性,就像隔离性、纯洁性及不可变性那样。语言拥有这样的特性之后,就可以用于构建各种不同的API,各种并发方式都可以利用到核心的语言特性。

(未完待续)

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

15 条回复

  1. 老菜
    60.215.45.*
    链接

    老菜 2010-05-31 08:23:20

    沙发
    哥们,再多写点吧。

  2. shuhen
    222.173.95.*
    链接

    shuhen 2010-06-01 11:23:55

    问个很菜的问题:在传统的编程里面,是否子进程要比线程更容易实现并发?这种并发是编译器关心的多一些还是系统任务调度关心的多一些。

  3. shuhen
    222.173.95.*
    链接

    shuhen 2010-06-01 12:26:31

    当然,编译器是有很多种的,我只是说比较典型的,或者就说C#的也好。

  4. Snake
    120.43.212.*
    链接

    Snake 2010-06-01 12:34:12

    Good,老赵继续.

  5. 老赵
    admin
    链接

    老赵 2010-06-01 12:42:20

    @shuhen

    我不知道你是指什么“更容易”,就我来说用线程作并发更“容易”吧,比如可以直接通信。有人喜欢多进程的并发方式,可能隔离性更好,更稳定,只是跨进程通信总要麻烦和低效一些。

    不过这些应该都和编译器无关吧。

  6. shuhen
    222.173.95.*
    链接

    shuhen 2010-06-01 13:43:58

    @老赵

    我是这样理解的,在一个多核的cpu上并行运算会面临很多困难,比如说怎么保证这些并行任务的先后顺序、保证他们执行的结果是最初设计想得到的等等。而进程隔离性更好,所以更适合并行的实现(就是我说的“更容易”)。

    老赵提到了通信方面的问题,我想对通信的要求是否也会增加并行的困难(假设用线程替代进程就是为了通信方便的情况)。设想一个5核的cpu上并行了100个线程,每个核心有19个等待的线程(除去正在运行的一个),其中有个正在运行的线程A要和线程B通信,而线程B在某个核心等待队列靠后的位置,那这里可能就会出现情况,比如线程A会被挂起,但如果这类情况很多的话——比如这100个线程之间都有着千丝万缕的联系,是否会严重影响并行的效果,或至少降低了并行所带来的优势。

  7. 老赵
    admin
    链接

    老赵 2010-06-01 13:55:56

    @shuhen

    估计你误解了我说的隔离性是指什么,我不觉得隔离性对于并行实现有太大帮助。你关注的是任务调度方面,这用线程反而更容易控制,比如你可以关注一下.NET 4.0的并行库,这么好用的类库如果是要用多进程的话就相当不好写了。

    你要效率高,线程不是越多越好的,比如你有4个cpu,那么4个线程是最理想的,系统不会有多余的调度开销。如果你在4核的机器上做并行计算用到了100个线程,那么基本就说明你写错了。.NET 4.0的并行库,就是帮助你协调任务依赖关系,任务怎么分配给少量又足够的线程去执行。

  8. shuhen
    222.173.95.*
    链接

    shuhen 2010-06-01 14:39:39

    @老赵

    您是否是说,并行的处理要在程序设计上面多下功夫,比如我说的100个线程,可以把关系紧密的线程合并起来,这样就不会存在那么多乱七八糟的等待问题。

    看来是我最初的想法有问题,因为我看到你博文中说道 :C#中光靠一个Parallel实现并行不是最终解决途径,应该从语言层面去解决(希望我没有理解错,呵呵),因此我就想到是不是操作系统那边也要有所动作,但我只是简单的考虑到进程间相对独立,互不干扰,可以更“毫无顾忌”的并行,现在想来实在是有失偏彼了。

    非常感谢老赵耐心的解释,跟您讨论很高兴。

  9. 老赵
    admin
    链接

    老赵 2010-06-01 15:59:29

    @shuhen

    一个Parallel方法的确是不能解决所有问题的,在语言基础上提供支持是一个“方向”,一个设想吧,离实践还有点距离,至少C#是这样的,其他一些函数式编程语言,已经可以做到简单的自动并行(主要是计算方面,不是任务并行)。所以并行程序方面,至少目前,的确还是十分困难的……

  10. 链接

    zwt101927 2010-06-01 23:12:19

    一直以为老赵不写了,汗,原来搬这来了

  11. 链接

    pc 2010-06-02 20:35:29

    吃定老赵了, 还是自立门户好。

  12. TonyWoo
    58.251.171.*
    链接

    TonyWoo 2010-06-02 23:00:30

    翻译得不错,上个星期天看了一下视频,浑浑沉沉的最后还是睡着了,呵呵。

  13. 链接

    Ivony 2010-06-03 14:58:29

    我们也可以在函数的纯洁性(Purity)方面下功夫,如关注某个函数是否有副作用,有些时候编译器可以做这方面的检查,它可以禁止某些操作,以此保证我们写出纯函数。

    看来Anders的想法也是这样,C#引入public lambda int Function( int p )这样的语法有盼头了。

  14. michael
    60.171.124.*
    链接

    michael 2010-06-21 11:08:25

    这个系列我一直在跟着看。我觉得老赵的这个标题怪怪的,并发总是让我想到操作系统的时间片轮转啊,叫并行可能更好一些。毕竟是多核编程嘛

  15. 老赵
    admin
    链接

    老赵 2010-06-21 11:37:19

    @michael

    Concurrency是并发,Parallel是并行。这里不光是指多核的CPU密集型的并行计算,单核也要,也可以并发的……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我