C++之并发编程


我们承担ROS,FastDDS等通信中间件,C++,cmake等技术的项目开发和专业指导和培训,有10年+相关工作经验,质量有保证,如有需要请私信联系。

不使用并发的场景

  • 启动线程时存在固有的开销,然后才能把新线程加入到调度器中,所有这一切需要时间,任务实际执行时间比启动线程的时间小则不适合使用线程
  • 线程是有限的资源,每个线程都需要独占的堆栈空间,运行太多线程会耗尽进程的可用内存或地址空间;同时运行太多线程会消耗很多操作系统资源
  • 运行越多的线程操作系统就需要做越多的上下文切换而耗费时间

概念理解

tearing(撕裂):撕裂是数据竞争的结果,有两种类型:

  • 撕裂读:如果线程将一部分数据写入内存,还有一部分数据没有写入,此时读取数据的其他线程将看到不一致的数据,发生撕裂读
  • 撕裂写:如果两个线程同时写入数据,两个线程各写入数据的一部分,发生撕裂写。

避免恶性条件的竞争的方法

  • 采取保护机制,C++中主要以这种形式
  • 无锁编程,不过这种方式很难得到正确的结果
  • 使用事务的方式去处理数据结构的更新,目前C++中没有对STM进行直接支持

死锁:多个线程因为等待另一个阻塞线程锁定的资源而造成无限阻塞。

嵌套死锁:设想有两个线程中的代码按如下顺序执行:
  线程1:加锁后获取A
  线程2:加锁后获取B
  线程1:获取B,但因为B被线程2独占,所以阻塞在这里
  线程2:获取A,但因为A被线程1独占,所以阻塞在这里

避免死锁的方法

  • 避免嵌套锁,一个线程已经获得一个锁时,别再去获取第二个,尽量保证一个线程只有一个锁
  • 当要求获取两个以上的锁时,以相同的顺序加锁
  • 避免在持有锁期间调用用户提供的代码

可重入函数:可重入是并发安全的强力保障,可以在多线程下放心使用

  • 一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。只有有两种情况:
    • 多个线程同时执行这个函数
    • 函数自身调用自身
  • 可重入表示该函数被重入之后不会产生任何不良后果,要成为可重入函数,必须具有以下几个特点:
    • 不使用任何静态或全局的非const变量
    • 不返回任何静态或全局的非const变量的指针
    • 仅依赖于调用方提供的参数——如果调用方参数是指针或引用,那是不是也是不安全的
    • 不依赖任何单个资源的锁(如mutex等)
    • 不调用任何不可重入的函数

过度优化:编译器为了提高变量x的访问速度将x放到某个寄存器中,不同线程中的寄存器是独立的,所以也有和不加锁的一样的情况出现的可能

  • volatile:确保本条指令不会因编译器的优化而被省略,即系统每次从变量所在内存读取数据而不是从寄存器读取备份。对于每一个希望在多线程中被共享的变量,都应该以volatile修饰,可以做到两件事:
    • 阻止编译器为了提高速度将一个变量缓存到寄存器而不写回
    • 阻止编译器调整操作volatile变量的指令顺序

使用C++标准库中的多线程带来的开销并不比手工编写等效函数的要高,而且编译器可能会很好的内联大部分代码

高级接口

async

#include<future>

  • 用法:async:尝试将其所获得的函数立刻异步启动于一个分离线程内,返回future对象,这个返回对象是必要的,因为允许你取得传给async的那个执行函数的未来结果,是个返回值或异常;future对象也是必须存在的,因为并不保证传入的函数一定会被启动和结束,需要future对象才能强迫启动,因此即使对启动于后台的那个函数的结果不感兴趣,还是需要掌握有这个

future

  • 唯一期望future<>:允许等待线程结束并获取结果,结果是一个返回值,或一个异常
    • 用法说明:
      • 使用future等待一次性事件(如果多次推荐使用条件变量),用来表现某一操作的成果:async,pakcaged_task, promise; 可能是返回值或异常,但不会二者都是,这份成果被管理于一个shared state内,可以被async或packaged_task或一个promise创建出来,有两种期望:唯一期望future和共享期望shared_future;可以移动但不能拷贝
      • future既不提供拷贝构造也不提供拷贝赋值运算符,确保不会有两个对象共享同一后台操作状态。将某个future状态搬移至另一个的唯一方法是调用移动构造或移动赋值运算符
  • future对象的方法:
    • 构造/析构
      • operator=:
      • share:转移共享状态从this到shared_future并返回,产生一个shared_future带有当前状态,并令当前future对象的状态失效
      • get:返回执行函数的结果。一个future只能被调用get()一次,在那之后future就处于无效状态,而这种状态只能由对future调用valid()来检测
      • valid:检查是否future对象有一个共享的状态,如果还有效就返回true,然后才可以调用以下/上函数
      • wait:强制启动该future象征的线程并等待这一后台操作终止,这个接口一个调用一次以上
      • wait_for/wait_until:不强制启动线程(如果线程尚未启动的话),wait_for的参数指定一个时间段,就可以让异步运行中的操作等待一段有限时间;wait_until等待直到到达某特定时间点
    • 返回值:
      • std::future_status::deferred:如果async延缓了操作而程序中又完全没有调用wait或get,这种情况夏两个函数都会立刻返回
      • std::future_status::timeout:如果某个操作被异步启动但尚未结束,而waiting又已经逾期
      • std::future_status::ready:操作已完成
        • 对get的调用的分析:以下三件事之一会发生:
        ○ 如果func1被async启动于一个分离线程中并已经结束,会立刻获得其结果
        ○ 如果func1被启动但尚未结束,get会引发停滞,待func1结束后获得结果
        ○ 如果func1尚未被启动,会被强迫启动如同一个同步调用,get会引发停滞直至产生结果
        • 遵循:早调用,晚返回的原则,即将调用async()和调用get()之间的距离最大化

共享期望shared_future

与future的差异:

  • 允许多次调用get,因此get不会令其状态失效
    • 支持copy(拷贝构造和拷贝赋值运算符)
    • 不提供share()
    • get()是个const成员函数,返回一个const reference指向存储于shared state的值——?
      std::shared_future:可以寻常的future为初值
      • 方法:
      ○ share()
      Launch(发射)策略:
    1. future async(std::launch::async, F func, args …):异步
      • 尝试启动func并给予实参args,形成一个异步任务,如果办不到就抛出std::system_error异常,带有差错码std::errc::resource_unavailable_try_again
      • 有了这个发射策略,就不必非得调用get()了,如果不将结果赋值出去,调用者会在此停滞到目标函数结束,就相当于是同步调用
      • 以下情况会结束线程:
      ○ 对返回的future调用get()或wait()
      ○ 如果最后一个指向返回之future所代表的shared state的object被销毁
      ○ 这意味着对async()的调用会造成停滞直到func完成
    2. future async(std::launch::deferred, F func, args …)::传递func并带实参args,形成一个推迟任务,当我们对返回的future调用wait或get时那个推迟的任务才被调用
      • 如果没有调用wait或get,这个任务不会启动
      • 这个策略允许你写出lazy evaluation(缓式求值)
      3.future async(F func, args …):相当于async携带std::launch::async和std::launch::deferred组合而来的launch发射策略,如果当前不能立即发射,会造成func被推迟调用
      • 这个调用的唯一保证是对返回的future对象调用get或wait方法
      • 如果没有调用get或wait方法,func有可能永远不会被调用
      • 如果无法异步调用func,这个形式的async不会抛出system_error异常

低层接口

thread

  • thread类的构造函数是一个可变参数模板,可接受任意数目的参数,第一个参数是新线程要执行的函数的名称。
    如果一个线程对象表示系统当前或过去的某个活动线程,则认为他是可结合的,即使这个线程执行完毕,该线程对象也仍然处于可结合状态。在销毁一个可结合的线程对象之前,必须调用其join()或detach()方法,线程将会成为不可结合的线程。如果一个仍可结合的线程对象被销毁,析构函数会调用terminate()。
  • 线程函数的参数总是被复制到线程的某个内部存储中。可通过<functional>中的std::ref()std::cref()按引用传递参数。

创建线程

  • 通过函数指针创建线程

  • 通过函数对象创建线程

  • 通过lambda创建线程

  • 通过成员函数创建线程

  • 可以移动但不能拷贝

    • 不可复制保证了在同一时间点一个thread实例只能关联一个执行线程;
    • 可移动性使得程序员可以自己决定哪个实例拥有实际线程的的所有权
  • 特点:

    • 没有所谓的发射策略,永远试着将目标函数启动于一个新线程中,如果无法做到就会抛出std::system_error并带差错码resource_unavailable_try_again
    • 没有接口可获得线程处理结果,唯一可以获得的是一个独一无二的线程ID
    • 如果发生异常,但未被捕捉到线程内,程序会立刻终止并调用std::terminate();若想将异常传递至线程外的某个context,必须使用exception_ptr——?
    • 必须声明是否等待线程结束(调用join),或打算将它自母体卸离(调用detach)使它运行于后台不受任何控制。如果不这么做,或它发生一次移动赋值(move assignment),程序会中止并调用std::terminate
    • 如果让线程运行于后台而main()结束了,所有的线程被硬性终止
  • Detached Thread需要注意的问题:

    • 绝不要让detached线程访问任何寿命已经结束的对象,所以建议以by value的方式传递
    • 如果detached thread使用了全局或静态变量:
      • 确保在使用全局或静态变量的的detached thread结束之前,这些变量不被销毁,一种做法就是使用条件变量。它让detached thread用来发信号说它们已经结束。离开main或调用exit之前你必须先设置妥这些条件变量,然后发信号说可以进行析构了
      • 以调用quick_exit的方式结束程序。这个函数之所以存在完全是为了以不调用全局和静态对象的析构函数的方式结束程序
    • 牢记一个经验法则:终止detached thread的唯一安全方法就是搭配"…at_thread_exit"函数群中的某一个,这会强制主线程等待detached thread真正结束——这个函数群中的函数有哪些?
  • 倾向于在无异常的情况下使用join,需要在异常处理过程中调用join从而避免生命周期的问题

  • 向线程函数传递参数,默认参数要拷贝到线程独立内存中,即使参数是引用形式——?使用std::ref将参数转化成引用形式。C++并发编程实战2.2

  • 成员:

    • join():等待线程完成,只能对一个线程使用一次join,会阻塞当前线程,一直等到线程完成工作;调用join还清理了线程相关的存储部分;当线程运行之后产生异常,在join调用之前抛出,就意味着这次调用很可能会被跳过;通常倾向于在无异常的情况下调用join,需要在异常处理过程中调用join,从而避免生命周期问题
    • joinable():如果使用过join,对其使用joinable时将返回false;同样,joinable返回true才可以使用detach,所以join和detach需要joinable做检查
    • detach():会将线程对象和底层OS线程分离,此时OS线程将继续独立运行,让线程在后台运行
    • std::thread::id / std::this_thread::get_id():获取当前线程ID,唯一可对thread ID执行的操作是对他们进行比较,或是将他们写到一个输出流中。此外有个哈希函数用来在非定序容器中管理thread ID。你不该有任何进一步假设像是no thread拥有ID 0或者main thread拥有ID 1之类的想象。事实上实现有可能在被申请时才动态生成这些ID,而不是在thread被启动时就生成
    • unsigned int std::thread::hardware_concurrency():static成员函数,用来查询并行线程的可能数量,该数量只是个参考值,不保证正确;如果数量不可计算或不明确返回值是0
    • native_handle_type native_handle():返回句柄
  • 疑惑点:

    • thread是不可拷贝的,为啥能push_back到容器中

Promise

  • 使用场景:问题:如何在线程之间传递参数和异常(也就是高级接口如async如何实现)?。欲传递数值给线程,可以仅仅把他们当作实参来传递,如果需要线程的运行结果可用by reference方式传递。另一个传递运行结果和异常的一般性机制就是std::promise。promise object和future object是配对兄弟,二者都能暂时持有一个shared state,但future允许取回数据,promise object却是让你提供数据。promise内部会建立一个shared state,在这里用来存放一个相应类型的值或异常,并可被future object取其数据当作线程结果,一旦shared state存有某个值或某个异常,其状态就会变成ready,于是可以在其他地方取出其内容,但是取出动作需要借助一个共享相同shared state的future object;可以移动但不能拷贝。
  • 方法:
    • swap(p1, p2)/p1.swap(p2):互换p1和p2的状态
    • p.get_future():产生一个future对象用以取回shared state,只能调用get_future()一次,第二次调用会抛出std::future_error的异常并带有差错码std::future_already_retrieved
    • set_value()
    • set_exception()
    • set_value_at_thread_exit()
    • set_exception_at_thread_exit
  • 注意:
    • 通过调用shared state的future object的get会停滞直到shared state成为ready,当promise的set_value或set_exception()执行后便是如此
    • 不能既存储值又存储异常,企图这么做会导致std::future_error并夹带错误码

package_task

#include<future> 对一个函数或可调用对象绑定一个期望。当std::packaged_task<>对象被调用,它就会调用相关函数或可调用对象,将期望状态置为就绪,返回值也会被存储为相关数据。被用来同时持有目标函数及其结果,结果是某个返回值或目标函数触发的异常;可以移动但不能拷贝——干啥的还没有搞懂

  • 方法:
    • swap(p1, p2)/p1.swap(p2):两个packaged task互换
    • p.valid():如果p有一个shared state就产生true
    • p.get_future():产出一个future对象,用来取回shared_state
    • p.make_ready_at_thread_exit(args):调用task并在线程退离时令shared state称为ready
    • p.reset():为p建立一个新的shared state

namespace this_thread

  • this_thread: #include, 针对任何线程,用以提供线程专属的global函数,有以下方法:
  • this_thread::get_id():当前线程的ID,属于特殊类型std::thread::id,其值独一无二,唯一允许对thread ID进行的操作是比较。
  • this_thread::sleep_for(dur):将线程阻塞dur时间段: std::this_thread::sleep_for(2000ms);
  • this_thread::sleep_until(tp): 将某个线程阻塞直到时间点tp
  • this_thread::yield():建议系统释放控制以便重新调度让下一个线程能够执行,在什么情况下使用?

线程本地变量

thread_local关键字声明,命名空间内的变量,静态成员变量,本地变量等任何变量都可以声明成线程本地变量,即每个线程都有这个变量的独立副本;声明一个变量为thread_local,这个变量将在线程的整个声明周期中持续存在。其值将在线程开始时初始化,而在线程结束时将不再有效。以下代码验证每个线程都有一份县城变量的独立副本

int k;
thread_local int n;
void threadFunction(int id) {
    ++n;
    ++k;
    cout << n << ' ' << k << '\n';
}
int main() {
    thread t1{ threadFunction, 1 }; t1.join();
    thread t2{ threadFunction, 2 }; t2.join();
}

输出:在这里插入图片描述

线程池

  • 线程池的七大核心参数:
    • corePoolSize:核心线程最大数量
    • maxNumPoolSize:线程总数量最大值
    • keepAliveTime:非核心线程的闲置超时时间
    • workQueue:任务队列
    • threadFactory:线程工厂
    • rejectHandler:拒绝策略
  • 常用的四种线程池
    • 可缓存线程池
    • 指定工作线程数量的线程池
    • 单线程化的线程池
    • 定长线程池
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值