我的“并发编程”15 年,架构进阶之路

在 三类安全的故事 中,我们讨论了三种安全:类型安全,内存安全和并发安全。 在接下来的这篇文章中,我们将深入到最后一个,也许是最新奇但也是最困难的一个。 第一次引领我进入并发安全的是 Midori 项目,我花了多年的时间在.NET和C ++并发模型上最终进入这个领域。 我们做了一些伟大的事情,在这段时间我感到自豪。 然而,或许更有趣的是,在离开项目几年后对这种经历的反思。

从今年早些时候我大约6次试图写这篇文章,很高兴终于能够把它分享给大家。 我希望它对任何对该领域感兴趣的人都有用,特别是在这一领域积极创新的人。 虽然代码示例和经验教训深深根植于C#,.NET和 Midori 项目,但我已经尝试概括其中的想法以致于它们更容易理解而与编程语言无关。 希望你喜欢!

背景

2000年的大部分时间,我在微软的 CLR team 开始从事相对小众的工作,我的工作内容主要是找到让开发人员习惯于并发的方法。

小众起点

当时,很大程度上需要构建更好的经典线程,锁机制和同步原语,以及努力巩固最佳实践。 例如,我们向 .NET 1.1 引入了一个线程池,并使用这种经验来提高Windows内核及其调度程序和线程池的可扩展性。 有了这个疯狂的128处理器 NUMA 机器,我们忙于各种各样深奥的性能挑战。 我们制定了正确处理并发的规则 – 锁层级等等 – 进行了静态分析实验。 我甚至为此写了一本书。

为什么把并发放在第一位?

简而言之,从技术上讲,这是非常具有挑战性的,从中可以收获很多乐趣。

我一直专注于语言。 所以,我自然而然地着迷于在学术界几十年的深入工作,包括编程语言和并发运行(特别是 Cilk 和 NESL),高级类型系统,甚至是专门的并行硬件架构(特别是像 the Connection Machine 和 MIMD 这样的超级计算机, 这些创新超越了我们值得信赖的朋友,von Neumann)。

虽然一些非常大的客户实际上在运行 symmetric multiprocessor (SMP)服务器 – 是的,我们曾经这样称呼它 – 但我不会特意说并发是一个非常受欢迎的领域。提到这些酷炫的“研究” 来源的时候,我的同行和经理只会瞥一眼。 然而,我会坚持研究下去。

尽管研究有乐趣,但我不会说我们在这段时间内做的工作对临时观察者会有极大的影响。 我们提出了一些抽象概念以便于开发人员可以安排逻辑工作,考虑更高级别的同步等等 – 但游戏规则没有改变。 当时我并不知道,这一时期在技术和社交上为后来的发展奠定了基础。

没有免费的午餐; 进入多核时代

然后有大事发生了。

2004年, Intel 和 AMD告诉我们摩尔定律 即将消失。Power wall 挑战(即过去5年CPU时钟速度没有增加) 将严重削弱该行业已经习惯的逐年提高的时钟速度。

突然,管理层开始更加关注并发性。 Herb Sutter 在2005年发表的 “Free Lunch is Over” 一文使并发发展到了极度狂热状态。 如果我们不能使开发人员编写大规模并行软件 – 或者说准入门槛没有显著降低,一些事情会非常困难乃至不可能发生 – Microsoft 和 Intel 的业务和互利的商业模式都会有麻烦。 如果硬件没有变得更快,软件不能自动变得更好,人们没有理由购买新的硬件和软件。 这标志着 Wintel era和 Andy and Bill’s Law 的结束,“ Andy 给我们带来的,Bill带走了”。

或者说,这种思想过时了。

当 “multicore” 一词成为主流时,我们开始设想一个拥有 1,024个核心处理器的世界,甚至借鉴DSP更具有前瞻性的 “manycore” architectures,将通用核心与能处理加密压缩等繁重任务的特定核心处理器混合在一起。

顺便说一句,10年后的事情并没有完全像我们所想的那样发展。 我们没有运行具有1024个传统内核的PC,尽管我们的GPU已经超过了这个数量,我们看到了更多的异质性,尤其是在数据中心,在那里 FPGA 承载着加密和压缩之类的关键任务。

在我看来,真正的失误在于转变。这是明确的,围绕功率曲线,密度和异质性的想法告诉我们在很大程度上转变已经迫在眉睫。这时我们应该注意已有的电脑,而不是去寻找更强大的PC。 相反,自然的本能是坚持过去,即“拯救”PC业务。虽然在当时困难不止一种,但这是一个经典的创新者困境。 当然,电脑没有在一夜之间消失,所以创新没有浪费,只是感觉相对于历史背景不平衡。 无论如何,我跑题了。

让并发更容易

作为一个研究并发的极客,这是我期待的时刻。 几乎一夜之间,为创新工作寻找赞助商变得更容易了,因为它现在有真正的非常迫切的业务需求,这是我一直梦想的事情。

简而言之,我们需要:

  • 让并行代码编写起来更容易

  • 更容易避免并发陷阱。

  • 这两种事情只会“偶然发生”。”

我们已经有线程,线程池,锁和基本事件。 下一步该做什么?

在这一点上有三个具体的项目被孵化,得到了兴趣和人员的注入。

软件事务存储

讽刺的是,开始的时候我们坚持安全第一。 这预示着后来的故事,因为一般说来,安全占据次要位置,直到我能够在 Midori 的环境中备份它。

开发人员已经有几种引入并发的机制,但仍然很难编写正确的代码。 因此,我们寻求更高层次的抽象概念来纠正偶然的错误。

然后进入了 software transactional memory (STM)时代。 自从 Herlihy 和 Moss 在1993年发表硕士论文以来,许多有希望的研究涌现出来,尽管它不是灵丹妙药,但大多数人对它提高抽象水平的能力表示赞同。

STM的写法如下,它可以自动实现安全:

 
void Transfer(Account from, Account to, int amt) { atomic { from.Withdraw(amt); to.Deposit(amt); } }

看这个例子,没有锁!

STM可以透明地处理所有的决策,比如说明如何使用粗粒度或细粒度同步,围绕同步的竞争策略,死锁检测和预防,以及保证在访问共享数据结构时不会忘记锁定等 。 这些都基于一个诱人的简单的关键字,原子。

STM也有更简单更具说明性的协调机制,如 orElse 。 所以,尽管STM的重点在于消除了手动管理锁定的需要,但它对线程之间同步的演变也是有帮助的。

不幸的是,经过几年深度运行时间,操作系统,甚至硬件支持的原型设计,我们放弃了努力。 我的简要总结是,虽然我在这里写了更多简单的架构细节,但比起让它“能工作即可”更重要的是,应该鼓励良好的并发架构。 这种更高层次的架构,正是我们首先应该关注解决的问题,在尘埃落定后,再看看还有什么缺陷。 我甚至不清楚,一旦我们达到这一点,STM 能否成为适合这项工作的工具。 (事后看来,我认为它是工具集中很多合理的工具之一,即使更多的分布式应用程序架构在不断出现,这是一件给人们带来危险的事情。)

然而,我们在 STM 上的努力没有完全失败。 正是在这段时间,我开始试验安全并发类型系统。 此外,位和块最终并入 Intel 的 Haswell 处理器作为事务同步扩展(TSX)指令套件,提供了利用推测锁定精度完成最低同步和锁定操作的能力。 再次,这段时间里我和一些了不起的人在一起工作。

并行语言集成查询 (PLINQ)

除了STM之外,利用我们最近在语言集成查询(LINQ)中的工作成果,我在工作日晚上和周末还开发了一个 “skunkworks” 数据并行框架。

并行 LINQ(PLINQ)背后的想法是从三个研究领域借鉴的一点经验:

  1. 并行数据库,用户执行SQL查询的行为已经实现了并行,用户不需要知道这个概念,但通常结果令人印象深刻。

  2. 声明式和函数式语言,通常使用列表推导来表达可以被积极优化的高级语言操作,包括并行性。 为此,我受到 APL的启发对 Haskell 更加痴迷。

  3. 数据并行性,在学术界有相当长的历史,甚至衍化出一些更主流的学科,最值得注意的是 OpenMP。

这个想法很简单。 采用现有的LINQ查询并把它们自动并行化 – 这些查询已经包含了映射,过滤器和聚合等操作 – 在语言和数据库中都可以实现并行化。 好吧,副作用是它不是隐式的。 但它只需要 一个 AsParallel 就可以激活:

 
// Sequential:var q = (from x in xs where p(x) select f(x)).Sum();// Parallel:var q = (from x in xs.AsParallel() where p(x) select f(x)).Sum();

上面的例子显现出有关数据并行性的一件伟大的事情。 数据量与针对数据的操作开销两者之一或者同时都可以与输入的大小成比例。 当用足够高级的语言(如LINQ)表达时,开发人员不必担心调度,只需要挑选适当数量的任务或同步即可。

这基本上就是 MapReduce ,在一台机器上跨越多个处理器。 事实上,我们后来与 MSR 合作开发了一个名为 DryadLINQ 的项目,它不仅可以在多台处理器上运行这样的查询,还可以将它们分布在许多机器上。 (最终,我们使用 SIMD 和 GPGPU 更好地实现。)这最终导致微软自己的内部系统等同于谷歌的 MapReduce,Cosmos ,到今天在微软仍然有很多大数据创新实验。

开发PLINQ的一段时间,是我职业生涯中一个真正的转折点。 我与一些了不起的人合作并建立了关系。 BillG 对这个想法写了一整页的评论,结束语是“我们必须在这个工作上投入更多的资源”。这种强烈的鼓励措词并没有损害实现这个想法的固定投资。 它也吸引了一些意想不到的人的注意。 例如, Jim Gray 注意到了,我从他那里得到了第一笔慷慨的投资,就在他不幸地消失前两个月。

不用说,这是一个激动人心的时刻!

针对上面的技术我特意整理了一下,有很多技术不是靠几句话能讲清楚,所以干脆找朋友录制了一些视频,很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然。如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:744642380,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。

插曲:成立PFX团队

在这段时间,我决定扩大我们的研究范围,不仅仅是数据并行性,同时解决任务并行性和其他并发抽象。 所以我提出组建一个新团队的想法。

令我惊讶的是,开发部门正在创立一个新的并行计算小组以应对不断变化的技术环境,他们想赞助这些项目。 这是一个机会,在顶级的业务主题下,统一进行招聘工作,并进一步采取措施,最终扩展到C ++,GPGPUs 等等。

显然,我答应了。

我为团队命名为 “PFX”, 最初简称“并行框架”,到我们感受营销工作的魔力时,又重命名为“.NET并行扩展”。这个团队的初始目标包括 PLINQ,任务并行, 和一个新的研究方向 —— 协调数据结构(CDS),旨在处理诸如 barrier-style synchronization,并发和无锁集合等等衍生自许多伟大研究论文级的高级同步。

任务并行库

这时我开始研究任务并行性。

作为 PLINQ 的一部分,我们需要创建自己的并行“任务”概念。还需要一个复杂的调度程序,可以根据机器的可用资源自动扩展。 大多数现有的调度器都是线程池,因为它们要求一个任务在单独的线程上运行,即使这样做不是最好的。尽管多年来我们做了改进,任务到线程的映射仍然是相当初级的。

鉴于我对Cilk的喜爱以及需要安排大量潜在递归的细粒度任务,毫无疑问应该为我们的调度体系结构选择一个 work stealing scheduler。

起初,我们把目标锁定在PLINQ上,所以没怎么注意抽象。 然后 MSR 开始探索独立的任务并行库将是什么样子。 这是一个完美的合作机会,所以我们开始一起研究。于是Task<T> 抽象诞生了,我们重写了PLINQ来使用它,并创建了一套用于常见模式的并行API,如 fork/join 和并行的for,foreach循环。

在上市之前,我们用新的work-stealing scheduler替代了线程池的核心,在进程内提供统一的资源管理保证多个调度器不会发生冲突。 到这一天,代码几乎与我早期实现 PLINQ 时相同(当然有很多修正和改进)。

我们长期以来对于相对较少数量的API的可用性非常痴迷。 虽然我们犯了错误,但事后我很高兴我们这样做了。 我有一个预感Task<T>将成为我们在并行空间中做的所有事情的核心,但我们没有人预测到异步编程的广泛使用,它真正地推广开来是在多年后。 现在,异步和等待都由它来驱动,我不能想象生活中没有 Task<T>。

大声说出:从Java中受到的启发

如果我没有提到Java以及它对我的想法的影响,是我的疏忽。

在此之前,受到许多同样的学术资源的启发,由Doug Lea领导,我们在Java社区的邻居也开始做一些创新的工作。 Doug 出版于1999年的书 “Concurrent Programming in Java” 帮助这些想法在主流中普及,最终促成 JSR 166 并入JDK 5.0。 Java的内存模型也在同一时间被正式化为JSR 133,这是无锁数据结构的一个关键支撑,这些结构需要扩展到大量的处理器中。

这是我看到的第一个主流尝试,它将线程,锁和事件之外的抽象层次提升到更平易近人的方式:并发集合, fork/join 等等。 它也使该行业更接近于学术界一些漂亮的并发编程语言。 这些努力对我们有巨大的影响。 我特别钦佩学术界和工业界的密切合作,让学术界数十年的知识积累浮出水面,在之后的几年里我试图效仿这种方法。

不用说,从 .NET 和 Java 之间的相似之处以及竞争程度,我们受到了启发。

安全在哪里?

所有这些都有一个大问题。 它们不太安全。 我们几乎完全专注于引入并发机制,但没有任何保证安全使用的措施。

有一个很好的理由:实现这个是困难的。非常难。 特别是对于开发人员来说有许多不同种类的可用的并发。 但幸运的是,学术界在这方面也有几十年的经验,在并行性研究上比主流开发人员研究得更加“深奥”。 我开始不分昼夜地沉迷于此。

对我来说,转折点是我在 2008 年发表于 BillG ThinkWeek 的另一篇论文,Taming Side Effects。在其中,我描述了一个新的类型系统,当时我知道的还很少,但这构成我接下来5 年的工作的基础。 也许这是不正确的,因为后续工作可能会被束缚在我关于 STM 的经验里,但它是一个良好的开始。

Bill 再次以“我们需要这样做”结束,所以我开始为此工作!

你好, Midori

但仍然有一个巨大的问题。 我不能想象在现有的语言和运行时间环境中逐步地完成这项工作。 我不是在寻找一个温暖舒适近似安全的东西,而是如果你的程序编译,你可以知道它没有数据竞争。 它需要刀枪不入。

事实上,我曾经尝试过。 我使用C#自定义属性和静态分析来原型化系统的一个变体,但很快得出结论,问题在语言深处出现,必须集成到任何想法的类型系统中工作。 并且他们甚至应该可以远程使用。 虽然我们当时有一些有趣的孵化项目(如 Axum),考虑到愿景的范围以及文化和技术的原因,我知道这项工作需要一个新的家。

所以,我加入了 Midori.

一种结构,一个想法

Midori团队还有一些并发大师,直到我加入前的几年间我一直在和他们谈论这一切。 在最高层次上,我们知道现有的基础是错误的赌注。 我们认为共享内存多线程确实不是未来,特别是我以前的工作缺乏解决这个问题的能力。 Midori 团队是为了应对重大挑战而设立的。

我们做了一些事情:

  • 隔离是至关重要的,我们将尽可能地拥抱它。

  • 消息传递将通过强类型的RPC接口连接这样的隔离。

  • 也就是说,在进程内部,存在单个消息循环,并且默认情况下没有额外的并行。

  • “promises-like”编程模型是这种模式下的第一个类,所以:

  • 不允许同步阻塞。

  • 系统中的所有异步活动都是显式的。

  • 复杂的协调模式是可能的,但与锁和事件无关。

为了达到这些目标,我们受到了大量的启发,这些启发来自于 Hoare CSPs,Gul Agha和 Carl Hewitt关于 Actors, E, π, Erlang 的工作以及我们自己多年来在构建并发,分布式和各种基于RPC的系统的集体经验。

我以前没有这样说过,但是在 PFX 工作中,消息传递显然不存在。 有很多原因。 首先,有许多相互竞争的研究,没有一个人“感觉”是对的。 例如, Concurrency and Coordination Runtime (CCR)非常复杂(但有很多满意的客户); Axum语言是一种新的语言; MSR的 Cω 是强大的,但需要语言的变化,有些人对此犹豫(虽然衍生自图书馆的工作有一些保证); 等等。 第二,对于并行的基本概念是什么每个人有不同的想法,而它对此没什么帮助。

但它实际上归结为隔离。我们认为对于细粒度隔离 Windows 进程太重了,提供安全的无处不在的消息传递是必要的。 Windows上没有子进程隔离技术真的是为了任务:COM apartments,CLR AppDomains...可以立即想起来许多有缺陷的尝试; 坦率地说,我不想死在那里。

(从那以后,我注意到已经有一些很好的作品,像Orleans – 部分由一些前 Midori 成员建立 – TPL Dataflow 和 Akka.NET。今天如果你想用 .NET 做 actors 和 /or 消息传递 ,我建议借鉴他们。)

另一方面,从进程本身开始,Midori 拥有许多层次的隔离,由于软件隔离它甚至比Windows进程更便宜。 粗粒度的隔离以域的形式存在,增加了额外的belts-and-suspenders硬件保护来托管不可信或逻辑分离的代码。 早期,我们想要更细的粒度 - 受到 E’s concept of “vats”的启发,早已开始将抽象用于进程消息管理,但没有考虑安全问题。 所以我们在此停滞。 但这恰恰告诉我们一个稳健的,高性能的安全的消息传递机制需要的基础。

有关这种架构的讨论重要的是 shared nothing 这个概念, Midori将其作为核心工作原理。 无共享架构对可靠性的意义是巨大的,消除了单点故障,对于并发安全它们也很有用。 如果你不分享任何东西,就没有竞争的机会! (我们会看到,这带有一点谎言的成分,一般不足以说明问题。)

有趣的是,同一时间我们在进行Node.js开发的时候就是这样。 异步,非阻塞,单个进程范围事件循环的核心思想是非常相似的。在2007 - 2009期间,这个领域中有一些很诱人的东西。 事实上,这些特征中大多是事件循环并发的共性。

以上构成了在其上绘制整个并发模型的画布。 这一点在 asynchronous everything 一文中已经讨论过。 但还有更多...

为什么不在这里停下?

这是一个合理的问题。 一个非常强大的系统依据上述内容就可以构建,或者我应该说,多年来经历系统上的冲击,上述基础经受住了时间的考验,并经历了比下一步(语法周边)更少的变化 。 我觉得这时可以离开了。 事实上,依据完美的后见之明,我相信停在这里将是一个合理的故事第一个版本。

然而,还有很多事情需要我们继续向前:

  • 子进程没有并行性。 值得注意的是现在还缺乏任务和数据的并行性。 这对于构建.NET的任务和 PLINQ编程模型的人来说是痛苦的。 很多场景有潜在的并行性只是等待被发现,例如图像解码,多媒体管道,FRP渲染堆栈,浏览器,最终语音识别等等。 Midori的一个顶级目标是解决并发难题,尽管很多并行化是为了进程的“自由”,没有任务和数据并行性会使之受到损害。

  • 进程之间的所有消息都需要RPC数据调度,因此无法共享对象。 缺少任务并行性的一个解决方案可能是将所有事物抽象为进程。 需要任务? 那就创建一个进程。 在Midori,他们有充足的条件完成这个工作。 然而,这样做需要调度数据。 这不仅是一个成本高昂的操作,而且并不是所有类型都可管理,这会严重限制可并行操作。

  • 事实上,我们为缓冲区开发了一个现有的 “exchange heap”,这是一个松散的基于线性的概念。 为了避免封锁大型缓冲区,我们创建了一个用于在进程之间进行交换的系统,这样就不需要作为RPC协议的一部分进行复制。 这个想法似乎是有用的,足以推广到更高级别的数据结构。

  • 由于上述的单个消息循环模型,尽管缺少数据竞争,但是由于多个异步活动交叉,还存在内部竞争条件。 await 模型的一个好处在于交叉至少在源代码中可见可审计; 但是竞争仍然会触发并发错误。 我们看到了语言和框架可以帮助开发人员解决这个问题的机会。

  • 最后,我们还有一个模糊的愿望,让系统中有更多的不变性。 这样做对并发安全有帮助,当然,我们也认为语言应该帮助开发人员按照正确的构建来获得现有的常见模式。 如果编译器信任不变性,我们还可以看到性能优化的机会。

我们回到学术界和ThinkWeek paper寻找灵感。 这些方法如果以一种有趣的方式组合,可以给我们提供必要的工具 —— 不仅可以提供安全的任务和数据并行性,还能提供更细粒度的隔离,不可变性以及可能解决进程内竞争的工具。

所以,我们中一部分人开始继续研究C#编译器。

今天先讲到这里,可以关注我,明天继续分享,也可以进群讨论学习:744642380。

猜你喜欢

转载自my.oschina.net/u/3772106/blog/1798125
今日推荐