C++ 多线程编程建议之 C++ 对多线程/并发的支持(下)

前言:

本文承接前文  C++ 对多线程/并发的支持(上) ,翻译自 C++ 之父 Bjarne Stroustrup 的 C++ 之旅(A Tour of C++)一书的第 13 章 Concurrency。本文将继续介绍 C++ 并发中的 future/promise,packaged_task 以及 async() 的用法。

1、通信任务

标准库还在头文件 <future> 中提供了一些机制,能够让编程人员基于更高的抽象层次任务来开发,而不是直接使用低层的线程、锁:

  • future promise:用于从任务(另一个线程)中返回一个值
  • packaged_task:帮助启动任务,封装了 future promise,并且建立两者之间的关联
  • async() :像调用一个函数那样启动一个任务。形式最简单,但也最强大!

1.1 future 和 promise

future promise 可以在两个任务之间传值,而无需显式地使用锁,实现了高效地数据传输。其基本想法很简单:当一个任务向另一个任务传值时,把值放入 promise,通过特定的实现,使得值可以通过与之关联的 future 读出(一般谁启动了任务,谁从 future 中取结果)。

假如有一个 future<X>fx,我们可以通过 get() 获取类型 X 的值:

X v = fx.get(); // if necessary, wait for the value to get computed

如果值还没有计算出,则调用 get() 的线程阻塞,直到有值返回。如果值无法计算出,get()可能抛出异常。

promise 的主要目的是提供一个简单的“put”的操作(set_value set_exception),和 future get() 相呼应。

如果你有一个 promise,需要发送一个类型为 X 的结果到一个 future,你要么传递一个值,要么传递一个异常。举个例子:

void f(promise<X>& px) // 一个任务:把结果放入 px
{
    try {
        X res;
        // 计算 res 的值
        px.set_value(res);
    }
    catch(...) { // 如果无法计算 res 的值
        px.set_exception(current_exception()); // 传异常到 future 的线程
    }
}

current_exception() 即捕获到的异常。

要处理通过 future 传递的异常,get() 的调用者必须在什么地方捕获,例如:

void g(future<X>& fx) // 一个任务;从 fx 提取结果
{
    try {
        X v = fx.get(); // 如有必要,等待值计算完成
        // 使用 v
    }
    catch(...){ // 无法计算 v
        // 错误处理
    }
}

如果 g() 不需要自己处理错误,代码可以进一步简化:

void g(future<X>& fx) // 一个任务;从 fx 提取结果
{
    X v = fx.get(); // 如有必要,等待值计算完成
    // 使用 v
}

思考:future 和 promise 是怎么关联起来的?

1.2 packaged_task

如何把 future 放入一个需要结果的任务,并且把与之关联的、产生结果的 promise 放入线程?packaged_task 可以简化任务的设置,关联 future/promisepackaged_task 封装了把返回值或异常放入 promise 的操作,并且调用 packaged_task get_future() 方法,可以得到一个与 promise 关联的 future。举个例子,我们可以设置两个任务,借助标准库的 accumulate() 分别累加 vector<double> 的前后部分:

double accum (double* beg, double* end, double init) // 计算以 init 为初值,[beg,end) 的和
{
    return accumulate(beg,end,init);
}

double comp2(vector<double>& v)
{
    using Task_type = double(double*,double*,double); // 任务的类型

    packaged_task<Task_type> pt0 {accum}; // 打包任务(即 accum)
    packaged_task<Task_type> pt1 {accum};

    future<double> f0 {pt0.get_future()}; // 取得 pt0 的 future
    future<double> f1 {pt1.get_future()}; // 取得 pt1 的 future

    double* first = &v[0];
    thread t1{move(pt0),first,first+v.size()/2,0};          // 为 pt0 启动线程
    thread t2{move(pt1),first+v.size()/2,first+v.size(),0}; // 为 pt1 启动线程

    return f0.get() + f1.get();
}

packaged_task 模板以任务的类型(Task_type,double(double*,double*,double) 的别名)作为其模板参数,以任务(accum)作为其构造函数的参数。move() 操作是必要的,因为 packaged_task 不可拷贝(只能移动)。packaged_task 不可拷贝是因为它是一个资源处理程序(resource handler),拥有 promise 的所有权,并且(间接地)负责与之关联的任务可能拥有的资源。

请注意,这里的代码没有显式地使用锁:我们能够专注于要完成的任务,而不是来管理它们通信的机制。这两个任务在不同的线程中执行,具有了潜在的并发性。

1.3 async()

我在本章所追求的思路,最简单,但也非常强大:把任务看成是一个恰巧可能和其他任务同时运行的函数。这并不是 C++ 标准库所支持的唯一模型,但它能很好地满足各类广泛的需求。其他更微妙、棘手的模型,如依赖于共享内存的编程风格也可以根据实际需要使用。

要启动潜在异步执行的任务,我们可以用 async():

double comp4(vector<double>& v) // 如果 v 足够大,派生多个任务
{
    if(v.size()<10000) // 犯得着用并发吗?
        return accum(v.begin(),v.end(),0);

    auto v0 = &v[0];
    auto sz = v.size();

    auto f0 = async(accum,v0,v0+sz/4,0.0);
    auto f1 = async(accum,v0+sz/4,v0+sz/2,0.0);
    auto f2 = async(accum,v0+sz/2,v0+sz*3/4,0.0);
    auto f3 = async(accum,v0+sz*3/4,v0+sz,0.0);

    return f0.get()+f1.get()+f2.get()+f3.get(); // 收集 4 部分的结果,求和
}

大体上,async() 把“调用部分”和“获取结果部分“分离开来,并且将两者和实际执行的任务分离。使用 async() 你不需要考虑线程、锁;你只要从任务(潜在地、异步地计算结果)的角度去考虑就可以了。async() 也有明显的限制:使用了共享资源、需要上锁的任务无法使用 async() ,你甚至不知道会用到多少线程,这完全是由 async() 决定的,它会根据调用时系统可用资源的情况,决定使用多少线程。例如,async() 在决定使用几个线程前,会检查有多少核心(处理器)空闲。

示例代码中的猜测计算开销和启动线程的相对开销(v.size()<10000)只是一个很原始、粗略的性能估计。这里不适合展开讨论怎么去管理线程,但这个估计仅仅是一个简单(可能很烂)的猜测。

请注意,async()不仅仅是专门用于并行计算、提高性能的机制。例如,它也能用于派生任务,从用户获取输入,让“主程序”忙其他事情。

2、建议

使用并发改善响应性和吞吐量
尽可能在最高级别的抽象上工作(比如优先考虑 asyncpackaged_task 而不是 threadmutex
考虑使用进程作为线程的替代方案
标准库的并发支持是类型安全的
内存模型把多数程序员从考虑机器架构的工作中解放出来
内存模型使得内存的表现和我们的预期基本一致
原子操作为无锁编程提供了可能性
把无锁编程留给专家
有时顺序操作比起并发更简单、更快
避免数据竞争(不受控地同时访问可变数据)
std::thread 是类型安全的系统线程接口
join() 等待一个线程结束
尽量避免显式共享数据
unique_lock 管理 mutexes
lock() 一次性获取多个锁
condition_variable 管理线程之间的通信
从(可以并行执行的)任务的角度思考,而非线程
不要低估“简单性”的价值
选择 packaged_task future,而不是直接使用 thread mutex
promise 返回结果,从 future 获取结果
packaged_task 处理任务抛出的异常或返回值
packaged_task future 来表示对外部服务的请求,以及等待其回复
async() 启动简单的任务

到此这篇关于 C++ 多线程编程建议之 C++ 对多线程/并发的支持的文章就介绍到这了,更多相关C++ 对多线程/并发的支持内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

上一篇:C++ 对多线程/并发的支持(上)

(0)

相关推荐

  • C++11并发编程:多线程std::thread

    一:概述 C++11引入了thread类,大大降低了多线程使用的复杂度,原先使用多线程只能用系统的API,无法解决跨平台问题,一套代码平台移植,对应多线程代码也必须要修改.现在在C++11中只需使用语言层面的thread可以解决这个问题. 所需头文件<thread> 二:构造函数 1.默认构造函数 thread() noexcept 一个空的std::thread执行对象 2.初始化构造函数 template<class Fn, class... Args> explicit th

  • C++11 并发指南之多线程初探

    C++11 自2011年发布以来已经快两年了,之前一直没怎么关注,直到最近几个月才看了一些 C++11 的新特性,今后几篇博客我都会写一些关于 C++11 的特性,算是记录一下自己学到的东西吧,和大家共勉. 相信 Linux 程序员都用过 Pthread, 但有了 C++11 的 std::thread 以后,你可以在语言层面编写多线程程序了,直接的好处就是多线程程序的可移植性得到了很大的提高,所以作为一名 C++ 程序员,熟悉 C++11 的多线程编程方式还是很有益处的. 如果你对 C++11

  • C++ 对多线程/并发的支持(上)

    目录 1. 并发介绍 2. 任务和线程 3.传递参数 4.返回结果 5.共享数据 6.等待事件 7.通信任务 前言: 本文翻译自 C++ 之父 Bjarne Stroustrup 的 C++ 之旅( A Tour of C++ )一书的第 13 章 Concurrency.作者用短短数十页,带你一窥现代 C++ 对并发/多线程的支持.原文地址:现代 C++ 对多线程/并发的支持(上) -- 节选自 C++ 之父的 < A Tour of C++ > 水平有限,有条件的建议直接阅读原版书籍. 1

  • C++ 多线程编程建议之 C++ 对多线程/并发的支持(下)

    前言: 本文承接前文  C++ 对多线程/并发的支持(上) ,翻译自 C++ 之父 Bjarne Stroustrup 的 C++ 之旅(A Tour of C++)一书的第 13 章 Concurrency.本文将继续介绍 C++ 并发中的 future/promise,packaged_task 以及 async() 的用法. 1.通信任务 标准库还在头文件 <future> 中提供了一些机制,能够让编程人员基于更高的抽象层次任务来开发,而不是直接使用低层的线程.锁: future 和 p

  • Java多线程编程中的两种常用并发容器讲解

    ConcurrentHashMap并发容器 ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁. ConcurrentHashMap的内部结构 ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下Con

  • Linux多线程编程快速入门

    本文主要对Linux下的多线程进行一个入门的介绍,虽然是入门,但是十分详细,希望大家通过本文所述,对Linux多线程编程的概念有一定的了解.具体如下. 1 线程基本知识 进程是资源管理的基本单元,而线程是系统调度的基本单元,线程是操作系统能够进行调度运算的最小单位,它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务. 一个进程在某一个时刻只能做一件事情,有了多个控制线程以后,在程序的设计成在某一个时刻能够做

  • 详解Python中的多线程编程

    一.简介 多线程编程技术可以实现代码并行性,优化处理能力,同时功能的更小划分可以使代码的可重用性更好.Python中threading和Queue模块可以用来实现多线程编程. 二.详解 1.线程和进程        进程(有时被称为重量级进程)是程序的一次执行.每个进程都有自己的地址空间.内存.数据栈以及其它记录其运行轨迹的辅助数据.操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间.进程也可以通过fork和spawn操作来完成其它的任务,不过各个进程有自己的内存空间.数据栈等,所以只

  • VC多线程编程详解

    本文实例讲述了VC多线程编程概念与技巧,分享给大家供大家参考.具体分析如下: 一.多线程编程要点 线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件.信号标识及动态分配的内存等.一个进程内的所有线程使用同一个地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行以及什么时候执行线程.线程有优先级别,优先权较低的线程必须等到优先权较高的线程执行完后再执行.在多处理器的机器上,调度程序可将多个线程放到不同的处理器上去运行,这样可

  • Python多线程编程(一):threading模块综述

    Python这门解释性语言也有专门的线程模型,Python虚拟机使用GIL(Global Interpreter Lock,全局解释器锁)来互斥线程对共享资源的访问,但暂时无法利用多处理器的优势.在Python中我们主要是通过thread和 threading这两个模块来实现的,其中Python的threading模块是对thread做了一些包装的,可以更加方便的被使用,所以我们使用 threading模块实现多线程编程.这篇文章我们主要来看看Python对多线程编程的支持. 在语言层面,Pyt

  • Java多线程编程实战之模拟大量数据同步

    背景 最近对于 Java 多线程做了一段时间的学习,笔者一直认为,学习东西就是要应用到实际的业务需求中的.否则要么无法深入理解,要么硬生生地套用技术只是达到炫技的效果. 不过笔者仍旧认为自己对于多线程掌握不够熟练,不敢轻易应用到生产代码中.这就按照平时工作中遇到的实际问题,脑补了一个很可能存在的业务场景: 已知某公司管理着 1000 个微信服务号,每个服务号有 1w ~ 50w 粉丝不等.假设该公司每天都需要将所有微信服务号的粉丝数据通过调用微信 API 的方式更新到本地数据库. 需求分析 对此

  • C# 并行和多线程编程——并行集合和PLinq

    在上一篇博客,我们学习了Parallel的用法.并行编程,本质上是多线程的编程,那么当多个线程同时处理一个任务的时候,必然会出现资源访问问题,及所谓的线程安全.就像现实中,我们开发项目,就是一个并行的例子,把不同的模块分给不同的人,同时进行,才能在短的时间内做出大的项目.如果大家都只管自己写自己的代码,写完后发现合并不到一起,那么这种并行就没有了意义. 并行算法的出现,随之而产生的也就有了并行集合,及线程安全集合:微软向的也算周到,没有忘记linq,也推出了linq的并行版本,plinq - P

  • Java学习随记之多线程编程

    Process和Thread 程序是指令和数据的有序集合, 本身没有运行的含义,是一个静态的概念. 进程是执行程序的一次执行过程,他是一个动态的概念,是系统资源分配的单位 一个进程中可以包含若干个线程,线程是CPU调度和执行的单位 线程创建 三种创建方法 继承Thread类 //创建线程方法一:继承Thread类,重写run() 方法,调用start开启主线程 public class TestThread01 extends Thread{ @Override public void run(

  • linux下c语言的多线程编程

    我们在写linux的服务的时候,经常会用到linux的多线程技术以提高程序性能 多线程的一些小知识: 一个应用程序可以启动若干个线程. 线程(Lightweight Process,LWP),是程序执行的最小单元. 一般一个最简单的程序最少会有一个线程,就是程序本身,也就是主函数(单线程的进程可以简单的认为只有一个线程的进程) 一个线程阻塞并不会影响到另外一个线程. 多线程的进程可以尽可能的利用系统CPU资源. 1创建线程 先上一段在一个进程中创建一个线程的简单的代码,然后慢慢深入. #incl

随机推荐