了解并发世界的发展方向

本文的目的是了解最近几年我们如何以并发程序员的身份发展。 我们中的许多人都使用线程来实现并发。 我们将尝试找出您之前可能已经遇到的问题,并讨论一些解决问题的方法。

为什么我们在一台处理器计算机上产生多个线程?
线程可以具有多个状态,如运行可运行等待 。 如果处于运行状态的线程需要执行一些I / O,则它将进入等待状态。 如果那时只有一个线程,则处理器将处于空闲状态。 因此,我们创建了更多线程,以便处理器可以继续在其他线程上运行。

通常,线程可能具有不同的优先级。 线程调度程序以抢占方式工作。 在有多个线程的情况下,调度程序将尝试在处于可运行状态的线程之间共享处理器时间。
因此,很容易看出我们付出了上下文切换的额外费用,这与线程数成正比。

让我们考虑以下每个周期0.27 ns的四核机器。
L1延迟— 1 ns
L2延迟-3.5 ns
L3延迟— 12 ns
RAM — 60 ns
现在考虑对适合RAM但不适用于L3高速缓存的较大输入的算法二进制搜索 。 假设它是单线程解决方案。 当程序开始运行时,您期望什么? 很多页面错误 。 二进制搜索算法本质上会尝试访问距离很远并且无法容纳在单个页面中的数据。 因此,当处理器空闲时,我们将大部分时间花在从RAM上进行昂贵的内存读取。 请注意,当进程正在等待内存读取时,在多线程程序中不会发生线程切换,因为线程切换本身将花费接近一微秒的时间,这非常昂贵。

请注意,二进制搜索是只读的,但是,如果我们要更新数据本身,则可能会遇到一些不同的问题。 考虑两个具有一个线程的核心处理器,每个线程共享一个RAM阵列。 现在,考虑一个简单的算法,其中第一个线程更新所有奇数索引元素,而另一个线程对偶数索引执行相同的操作。 即使它们不在同一个索引上工作,但只要一个线程更新了一个索引,整个缓存的页面就会变脏,从而迫使另一个线程再次从RAM中获取所有数据。 当然,事情变得更加棘手,因为相同的索引被更多的线程更新。 这种情况下效率低下的原因是共享的可变状态

因此,
1.我们需要比线程本身更快切换的东西。
2.我们希望有尽可能多的处理器利用率。
3.避免共享的可变状态。

因此,在接下来的几年中,某些语言已经做了或提出了建议,那就是提出线程上的这些轻量级抽象。 这种抽象自1958年由Melvin Conway引入以来就被称为Coroutines

了解协程
协程子例程都可以被任何调用者调用/初始化,并且它们最终可以返回一些结果。 但是, 协程还具有两个其他的suspendresume结构 。 这些构造实质上允许协程停止进一步处理,直到它等待其输入/触发。 以下示例(来自python)显示了一个生成器函数,其中yield挂起执行并返回该值。

因此,如果x = countdown(10),则随后调用x.next()应该给我10、9、8 ,依此类推。
在更一般的示例中,我们看到协程在恢复之前接受新的参数(请注意, yield在赋值的右侧)。

因此,如果y = grep(“ pattern”),y.next()应该打印消息并等待输入。 可以通过y.send(“存在模式”)提供输入或搜索文本

请注意,我们编写协程的方式,如果谨慎编写,则不需要抢先式调度。 有些人将其称为协作调度范例。 因此,基本上会有许多协程以某种管道的形式相互发送输入

一些相关的工作
C ++社区中已有更新,其中引入了协程,从一个协程切换到另一个协程仅需1 ns 。 它们通常重量很轻,我们可以拥有数百万个。 请注意,这在本质上与休伊特的演员模型在精神上非常相似,在休伊特中,我们看到演员像蚂蚁一样工作-他们很多而且他们在一起工作。

Go语言中,人们会注意到每个内核只有一个线程(在超线程的情况下,它将是两个)。 在线程之上,他们引入了goroutines(它们的协程名称),它由Go运行时由调度程序调度。 对于通常是网络的异步I / O,协程将由Net poller处理-以某种方式暂停goroutine,直到系统API做出响应为止。 在阻塞I / O的情况下,它们最终会产生一个线程T2 ,该线程现在将使用CPU执行其他goroutine。 当该协程准备好执行时,忙于执行I / O的线程T1随后将被丢弃。 Go语言当然基于CSP,其中两个实体(在这种情况下为goroutine)通过阻塞消息通过某个全局消息通道进行交互。 在这种情况下,协程有助于跳过所有I / O密集度并提高CPU效率。

关于可变的共享状态,务必避免这种状态绝对重要。 我们确实看到过诸如ErlangHaskell之类的语言,这使得很难拥有这种状态。 即使其他确实允许这样做的语言,我们作为程序员也应注意避免出现这种全局状态。 在基于参与者的模型中,重要的区别因素之一是参与者之间以异步非阻塞方式传递的直接消息。 这需要在参与者之间传递不可变的消息,从而解决了共享问题。 Erlang或其他一些功能语言进一步将状态限制为不可变的。