【crossbeam系列】3 crossbeam-deque:work-stealing算法

【crossbeam系列】3 crossbeam-deque:work-stealing算法work-stealing算法简介crossbeam-deque包提供了一个无锁的双向队列(deque)。那么这个双向队列在并发中又起到了什么重要的作用呢?这就涉及到了在多任务环境下的一…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

work-stealing算法简介

crossbeam-deque包提供了一个无锁的双向队列(deque)。那么这个双向队列在并发中又起到了什么重要的作用呢?这就涉及到了在多任务环境下的一个重要算法:work-stealing算法,既工作窃取算法。

最初,工作窃取算法是在join/fork的模型下,作为调度算法用来给多线程计算分配任务的。分配任务(这里是线程)有很多需要注意的点,比如线程的数量需要平衡(太少则有些处理器会处于空闲,太多可能内存会枯竭),并且相关的线程应该尽量分配在相同的处理器中,等等。而工作窃取算法简单的说就是,每个处理器先处理自己的任务,如果处理完了,就去别的处理器的任务列表里“偷”一个过来执行。与之相对的有一个work-sharing算法,在该算法中,新产生的任务由调度算法直接分配给相应的处理器,每个处理器都是被动的。而在工作窃取算法中,每个处理器都是主动的。

在Burton&Sleep[3]和Halstead[5]中已经可以看到工作窃取算法的雏形。而Blumofe&Leiserson[4]的随机工作窃取算法算是比较经典的对该算法的描述,以下是该算法的一个概述,其中每个处理器都维护一个双向队列。

初始状态下,计算是一个线程(类比main函数)并被分配给某个处理器,而其他的处理器都处于空闲状态。处于空闲状态的处理器会立即执行窃取操作。每个处理器按指令逐一执行当前线程,直到遇到以下四种情况:

  1. 遇到了spawn指令,产生了一个新的线程。当前线程被放入双向队列底部,处理器开始执行新的线程

  2. 线程被挂起(处于阻塞状态)。这个时候处理器会从双向队列的底部取出一个线程去执行。如果双向队列为空,那么处理器就会去执行窃取。

  3. 指令导致线程死亡,这时和2相同处理

  4. 指令激活了另一个线程。此时被激活的线程会被放入双向队列的底部,处理器继续执行现在的线程 窃取操作:处理器随机选取另一处理器,如果被选择的处理器的双向队列非空,那么从该队列的头部取出一个线程并开始执行,否则再次进入随机选取。

可以看到在该算法中,双向队列是一个关键数据结构。双向队列在本地被当作栈来使用:从本地取任务总是从栈顶(也既双向队列的底部)取出,这在crossbeam中被成为工作者队列(Worker queue)。而在窃取时,则把它当作队列来使用:总是从队列的头部窃取。

crossbeam的双向队列

Arora,Blumofe和Plaxton[1]基于Blumofe&Leiserson[4],提出了使用yield系统调用以及无锁数据结构的无锁工作窃取算法,即ABP工作窃取算法。(论文中后者使用的就是上一讲中提到的CAS。为什么需要CAS呢?试想,当双向队列只有一个元素,而窃取和本地取任务同时发生时就会产生竞态。基本上和上一讲提到的无锁并发栈的问题类似)。而Chase&Lev[2]则是改进了Blumofe&Leiserson[4]的deque,使其在保持简洁和高性能的同时,底层不受限于固定长数组(固定长数组会有溢出的问题)。而crossbeam的deque是在Chase&Lev[2]的deque的基础上又作出了一些改进:(注意,接下来我们就不讨论处理器中的线程调度,而是线程中的任务调度问题了。比如tokio,goroutine面临的都是这样的问题)

  1. 支持先进先出的工作者队列(既本地可以当队列而不是栈使用)

  2. 支持一次窃取多个任务

  3. 加入了一个注水器队列(Injector queue),和原来的工作者队列可以额配合使用。

这里我们先来说一下这个注水器队列。这是一个先进先出MPMC队列(任务从一端入队,从另一端被窃取),被线程共享(全局)。新的任务可以被放到这个队列中供空闲的线程窃取。相对于将新任务放进工作者队列的,这一操作更加公平(即只要有空闲的线程,这个队列的处理就会有进展)

API

// 先进先出MPMC队列(任务从一端入队,从另一端被窃取)
struct Injector<T>;

// 本地的工作者队列
struct Worker<T>;

// 用来从相应的工作者队列窃取任务
struct Stealer<T>;

#[must_use]
enum Steal<T> {
    Empty,
    Success(T),
    Retry,
}

impl<T> Injector<T> {
    fn new() -> Injector<T>;

    fn is_empty(&self) -> bool;
    fn len(&self) -> usize;
    fn push(&self, task: T);

    // 从队列窃取一个任务
    fn steal(&self) -> Steal<T>;

    // 从队列窃取多个任务并交给目标工作者
    fn steal_batch(&self, dest: &Worker<T>) -> Steal<()>;

    // 从队列窃取多个任务并交给工作者,并弹出第一个任务
    fn steal_batch_and_pop(&self, dest: &Worker<T>) -> Steal<T>;
}

impl<T> Worker<T> {
    // 初始化一个先进先出工作者队列
    fn new_fifo() -> Worker<T>;
    // 初始化一个后进先出工作者队列
    fn new_lifo() -> Worker<T>;

    // 从当前队列产生一个窃取者
    fn stealer(&self) -> Stealer<T>;
    fn is_empty(&self) -> bool;

    fn push(&self, task: T);
    fn pop(&self) -> Option<T>;
}

impl<T> Stealer<T> {
    fn is_empty(&self) -> bool;

    // 从队列窃取一个任务
    fn steal(&self) -> Steal<T>;

    // 从队列窃取多个任务并交给目标工作者
    fn steal_batch(&self, dest: &Worker<T>) -> Steal<()>;

    // 从队列窃取多个任务并交给工作者,并弹出第一个任务
    fn steal_batch_and_pop(&self, dest: &Worker<T>) -> Steal<T>;
}

impl<T> Steal<T> {
    fn is_empty(&self) -> bool;
    fn is_success(&self) -> bool;
    fn is_retry(&self) -> bool;

    // 如果是Success(T)则返回内容
    fn success(self) -> Option<T>;

    // 如过没有steal到任务,则执行F
    fn or_else<F: FnOnce() -> Steal<T>>(self, f: F);
}

// 一直找到一个Success(T)为止
impl<T> FromIterator<Steal<T>> for Steal<T>;

例子

对于一个简单的包含注水器队列的工作窃取算法,可以写出如下的窃取逻辑:

  1. 先从工作者队列本地试图获得一个任务

  2. 试图从全局的注水器队列中窃取一打任务

  3. 试图从另一个线程窃取一个任务

use crossbeam_deque::{Injector, Steal, Stealer, Worker};
use std::iter;

fn find_task<T>(
    local: &Worker<T>,
    global: &Injector<T>,
    stealers: &[Stealer<T>],
) -> Option<T> {
    // Pop a task from the local queue, if not empty.
    local.pop().or_else(|| {
        // Otherwise, we need to look for a task elsewhere.
        iter::repeat_with(|| {
            // Try stealing a batch of tasks from the global queue.
            global.steal_batch_and_pop(local)
                // Or try stealing a task from one of the other threads.
                .or_else(|| stealers.iter().map(|s| s.steal()).collect())
        })
        // Loop while no task was stolen and any steal operation needs to be retried.
        .find(|s| !s.is_retry())
        // Extract the stolen task, if there is one.
        .and_then(|s| s.success())
    })
}

当然,Chase&Lev的不包含注水器的窃取算法也可以很容易写出来。有了crossbeam-deque,写无锁工作窃取算法是不是容易了很多?

小结

好了,今天我们简单入门了以下工作窃取算法,以及它和双向队列的关系。也看到了crossbeam-deque就是为此而生的。下期我们来看看crossbeam中的channel

参考文献

[1]Arora, N. S., Blumofe, R. D., and Plaxton, C. G. Thread scheduling for multiprogrammed multiprocessors. Theory of Computing Systems 34, 2 (2001), 115–144.

[2]David Chase and Yossi Lev. Dynamic circular work-stealing deque. SPAA ’05: Proceedings of the seventeenth annual ACM symposium on Parallelism in algorithms and architecturesJuly 2005 Pages 21–28https://doi.org/10.1145/1073970.1073974

[3]F. Warren Burton and M. Ronan Sleep. Executing functional programs on a virtual tree of processors. In Proceedings of the 1981 Conference on Functional Programming Languages and Computer Architecture, pages 187{194, Portsmouth, New Hampshire, October 1981.

[4]Robert D. Blumofe and Charles E. Leiserson. Scheduling multithreaded computations by work stealing. In Proceedings of the 35th Annual Symposium on Foundations of Computer Science (FOCS), pages 356–368, Santa Fe, New Mexico, November 1994.

[5]Robert H. Halstead, Jr. Implementation of Multilisp: Lisp on a multiprocessor. In Conference Record of the 1984 ACM Symposium on Lisp and Functional Programming, pages 9{17, Austin, Texas, August 1984.

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/193720.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • html下拉框设置默认值_html下拉列表框默认值[通俗易懂]

    html下拉框设置默认值_html下拉列表框默认值[通俗易懂]HTML和JavaScript综合练习题一、单项选择1.Web使用(D)在服务器和客户端之间传输数据。A.FTPB.TelnetC.E-mailD.HTTP2.HTTP服务默认……name的属性值必须要相同,必须有一个value值实现默认选中的属性:checked=”checked”-文件输入项(在后期上传时候用到):-下拉………

    2022年10月31日
  • python百度文库怎么免费下载文档(python编程是啥)

    每天早晨8点50分,准点开车打卡今天跟大家推荐一个简单实用的下载百度积分文档方法。虽然现在很多人都很少用百度文库中的东西了,不过后台还是经常收到一些读者留言,求帮忙下载百度文档或者请教如何才能免下载券下载。其实下载百度文库的方法,我很久之前就推荐过给大家了,可能你不记得了,推荐个免费的积分文档下载神器。这篇文章是推荐你使用冰点下载器,就可以自由下载百度,豆丁,畅享,mbalib,hp009,…

  • vue webpak版本 查看_vue版本以及webpack版本

    vue webpak版本 查看_vue版本以及webpack版本vue作为大前端的主流框架更新速度也是极快。那么vue的更新会有哪些问题呢?最近在搭建vue框架的时候发现由于vue版本的快速迭代已经与原本般配的webpack产生了隔阂。webpack作为大前端的主流打包工具如果与之不兼容,会有越来越多的麻烦事情。经过反复测试,得出结论一篇vue与webpack最佳拍档组合版本号公布。npminitnpminstallwebpack@3.10.0v…

  • output device(storage devices)

    dockertheinputdeviceisnotaTTY.Ifyouareusingmintty,tryprefixingthecommandwith’winp解决方法执行命令报错dockerexec-it8ea8a375e686/bin/bashtheinputdeviceisnotaTTY.Ifyouareusingmintty,tryprefixingthecommandwith’winpty’解决方案

  • 线程指令重排[通俗易懂]

    线程指令重排[通俗易懂]1、指令重排JVM为优化执行效率对线程内的执行顺序进行重排,对单线程来说执行指令重排并不会影响程序从上到下执行的代码逻辑。但是在多线程的情况下,则可能会出现问题。2、指令重排原则程序顺序原则:一个线程内保证语义的串行性volatile规则:volatile变量的写,先发生于读锁规则:解锁(unlock)必然发生在随后的加锁(lock)前传递性:A先于B,B先于C那么A必然先于C线程的start方…

    2022年10月18日
  • 【AekdyCoin】求小于等于N的与N互质的数的和

    【AekdyCoin】求小于等于N的与N互质的数的和又向大牛学到了一点。以下内容转大牛文章:ifgcd(n,i)=1thengcd(n,n-i)=1(1反证法:如果存在K!=1使gcd(n,n-i)=k,那么(n-i)%k==0而n%k=0那么必须保证i%k=0k是n的因子,如果i%k=0那么gcd(n,i)=k,矛盾出现;于是问题变的非常简单ANS=N*phi(N)/2i,n-i总是成对

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号