{ 3 }CPP_并行算法的尝试、识别线程以及共享数据带来的问题

一、并行算法初步尝试

template<typename Iterator,typename T>
struct accumulate_block
{
    void operator()(Iterator first,Iterator last,T& result)
    {
        result = std::accumulate(first, last, result);//累积所有任务块的总和
    }
};


template<typename Iterator,typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{

    unsigned long const length = std::distance(first, last);//distance是计算两个iterator的距离。

    if(!length)
        return init;


    unsigned long const min_per_thread = 25;

    /*
     * 用范围内元素的总数量(length + min_per_thread)除以线程(块)中最小任务数,从而确定启动线程的最大数量。
     * 这样能避免无谓的计算资源的浪费。
     * */
    unsigned long const max_threads =
            (length+min_per_thread-1) / min_per_thread;


    unsigned long const hardware_threads =
            std::thread::hardware_concurrency();//返回能同时并发在一个程序中的线程数量


    /*
     * 计算量的最大值和硬件支持线程数中,较小的值为启动线程的数量。
     * 因为上下文频繁的切换会降低线程的性能,所以你肯定不想启动的线程数多于硬件支持的线程数量。
     *
     * 当 std::thread::hardware_concurrency() 返回0,你可以选择一个合适的数作为你的选择;在本例中,我选择了"2"。
     * 也就是说,无论如何都会开启两个线程
     * */
    unsigned long const num_threads =
            std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);


    //每个线程中处理的元素数量,是范围中元素的总量除以线程的个数得出的。
    unsigned long const block_size = length / num_threads;


    std::vector<T> results(num_threads);//用于存放当前能开启的线程中,每个线程处理的任务块
    std::vector<std::thread> threads(num_threads-1);//存放总共能开启多少线程,因为在启动之前已经有了一个线程(主线程), 所以-1。

    Iterator block_start = first;
    for(unsigned long i=0; i < (num_threads-1); ++i)
    {

        Iterator block_end = block_start;

        std::advance(block_end, block_size);//block_end迭代器指向当前任务块的末尾

        threads[i] = std::thread(
                accumulate_block<Iterator,T>(),
                block_start, block_end, std::ref(results[i]) );//启动一个新线程为当前块累加结果

        block_start = block_end;//当迭代器指向当前块的末尾时,启动下一个块
    }

    /*
     * 启动所有线程后,线程会处理最终块的结果。对于分配不均,因为知道最终块是哪一个,那么这个块中有多少个元素就无所谓了。
     * */
    accumulate_block<Iterator,T>()(block_start,last,results[num_threads-1]);

    /*
     * 把容器中的thread对象逐个join
     * */
    std::for_each(threads.begin(),threads.end(),
            std::mem_fn(&std::thread::join));

    return std::accumulate(results.begin(),results.end(),init);//使用 std::accumulate 将所有结果进行累加
}

此程序的一些要求和缺点:

  • 对于创建出results容器,需要保证T有默认构造函数。
  • 当线程运行时,所有必要的信息都需要传入到线程中去,包括存储计算结果的位置。不过,有时候显得太麻烦,可以用识别线程的来解决,可以传递一个标识数,例如 i来标记线程在容器中的位置。不过,当需要标识的函数在调用栈的深层,同时其他线程也可调用该函数,那么标识数就会变的捉襟见肘。因此,标准库为每个线程附加了唯一标识符。

二、识别线程

线程标识类型是 std::thread::id,有两种方式可以获取它:

  • 可以通过调用 std::thread 对象的成员函数 get_id() 获取线程标识。
  • 在当前线程中调用 std::this_thread::get_id() 获取线程标识。

std::thread::id 的对象可以自由的拷贝和对比:

  • 如果两个对象的 std::thread::id 相等,那它们就是同一个线程,或者都“没有线程”。
  • 如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有。

C++标准库允许程序员将 std::thread::id 的对象做为容器的键值,同时也提供std::hash<std::thread::id> 容器,所以 std::thread::id 也可以作为无序容器的键值。

三、共享数据带来的问题

当一个或多个线程要修改共享数据时,就会产生很多麻烦。
例子:
从一个列表中删除一个节点的步骤如下(不变量为此双链表)
a. 找到要删除的节点N
b. 更新前一个节点指向N的指针,让这个指针指向N的下一个节点
c. 更新后一个节点指向N的指针,让这个指正指向N的前一个节点
d. 删除节点N

image.png

b) 和 c) 在相同的方向上指向和原来已经不一致了,修改了共享数据,同时也破坏了不变量。在并发程序中,极有可能因为其他线程访问刚刚被删除的数据,而引发错误:条件竞争

四、条件竞争

条件竞争的定义

如上例子,并发中竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务,也被称为 “恶性条件竞争” 。

C++标准也定义了数据竞争这个术语:并发的去修改一个独立对象,数据竞争时可怕的未定义行为的起因。

如何避免恶性条件竞争?

1. 通过加锁或者无锁的方式来避免

  • 使用C++标准库提供的互斥量,确保只有进行修改的线程才能看到不变量被破坏时的中间状态。
  • 对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化,也就是 保证每个不变量保持稳定的状态, 这就是所谓的无锁编程,不过会使得程序变得很复杂。

2.使用事务的方式去处理数据结构的更新

  • 所需的一些数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交。 当数据结构被另一个线程修改后,或处理已经重启的情况下,提交就会无法进行 , 这称作为“软件事务内存”(STM)。不过,C++没有对STM进行直接支持。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 接着上节 mutex,本节主要介绍atomic的内容,练习代码地址。本文参考http://www.cplusplu...
    jorion阅读 73,603评论 1 14
  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,244评论 4 56
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,217评论 11 349
  • 离决心重新捡起写日记的习惯才刚刚过了十天,时间好像真的随愿变得慢下来了,也可能是无所事事的时间在这段日子里增多了。...
    蓝琥珀和红魔鬼阅读 307评论 0 2
  • 喜闹梅枝春意浓
    艳青阅读 220评论 2 3