博客
关于我
C++并行编程
阅读量:328 次
发布时间:2019-03-04

本文共 7676 字,大约阅读时间需要 25 分钟。

parallel development

资料

参考资料

code

并发与并行

并发:同一时间段内可以交替处理多个操作

并行:同一时间段内同时处理多个操作
如果程序的结构设计为可以并发执行的,那么在支持并行的机器上,程序可以并行地执行。

多进程并发

多个进程独立地运行,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,文件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免一个进程去修改另一个进程,操作系统在进程间提供了一定的保护措施,当然,这也使得编写安全的并发代码更容易。运行多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。

多线程并发

在当个进程中运行多个线程也可以并发。线程就像轻量级的进程,每个线程相互独立运行,但它们共享地址空间,所有线程访问到的大部分数据如指针、对象引用或其他数据可以在线程之间进行传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建立且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是一致的。

C++中的并发与多线程

C++11标准提供了一个新的线程库,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作等各种类。

: 包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在该头文件中有声明;
:包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数;
:包含了与互斥量相关的类以及其他类型的函数;
: 包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数;
: 包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any

thread

std::thread

int n = 0;std::thread t1; // t1 is not a threadstd::thread t2(f1, n + 1); // pass by valuestd::thread t3(f2, std::ref(n)); // pass by referencestd::thread t4(std::move(t3)); // t4 is now running f2(). t3 is no longer a thread

std::thread::join

join 是让当前主线程等待所有的子线程执行完,才能退出。

std::thread::joinable

用于检测线程是否有效,true : 代表该线程是可执行线程

std::thread::detach

线程 detach 脱离主线程的绑定,主线程挂了,子线程不报错,子线程执行完自动退出。

线程 detach以后,子线程会成为孤儿线程,线程之间将无法通信。

~thread()分析

~thread() _NOEXCEPT {	    // 析构函数    if (joinable())         // 线程是可结合的(可执行线程),析构异常(也就是说只能析构不可结合的线程)        _XSTD terminate();  // terminate会调用abort()来终止程序}

joinable() = false :

空线程move后的线程,即move(t),则t是不可结合的join后的线程detach后的线程

Mutex

std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁

Mutex 系列类(四种)

std::mutex,最基本的 Mutex 类。std::recursive_mutex,递归 Mutex 类。std::time_mutex,定时 Mutex 类。std::recursive_timed_mutex,定时递归 Mutex 类。

Lock 类(两种)

std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

其他类型

std::once_flagstd::adopt_lock_tstd::defer_lock_tstd::try_to_lock_t

mutex函数

构造函数

std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。

std::try_lock

尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

std::lock

可以同时对多个互斥量上锁。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

std::unlock

解锁,释放对互斥量的所有权。

std::call_once

如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。

std::recursive_mutex

std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,std::recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

std::time_mutex

std::time_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()。

try_lock_for 函数

接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until 函数

接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

std::recursive_timed_mutex

和 std:recursive_mutex 与 std::mutex 的关系一样,std::recursive_timed_mutex 的特性也可以从 std::timed_mutex 推导出来。

lock_guard / unique_lock

std::lock_guard

与 Mutex RAII 相关,方便线程对互斥量上锁。

其原理是:声明一个局部的lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:在定义该局部对象的时候加锁(调用构造函数),出了该对象作用域的时候解锁(调用析构函数)。
使用方法:

  1. 首先需要包含mutex头文件
  2. 然后创建一个锁 std::mutex mutex
  3. 在需要被加锁的作用域内 将mutex传入到创建的std::lock_guard局部对象中
#include 
/*std::mutex、 std::lock_guard*/std::mutex mutex;void func(){ //lock_guard 互斥锁 作用域内不可拷贝构造 { std::lock_guard
lg(m_mutex); //函数内容 }}

std::unique_lock

与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度。

class LogFile {    std::mutex _mu;    ofstream f;public:    LogFile() {        f.open("log.txt");    }    ~LogFile() {        f.close();    }    void shared_print(string msg, int id) {        {            std::lock_guard
guard(_mu); //do something 1 } //do something 2 { std::lock_guard
guard(_mu); // do something 3 f << msg << id << endl; cout << msg << id << endl; } }};

上面的代码中,一个函数内部有两段代码需要进行保护,这个时候使用lock_guard就需要创建两个局部对象来管理同一个互斥锁(其实也可以只创建一个,但是锁的力度太大,效率不行),修改方法是使用unique_lock。它提供了lock()和unlock()接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard就一定会解锁)。上面的代码修改如下:

class LogFile {    std::mutex _mu;    ofstream f;public:    LogFile() {        f.open("log.txt");    }    ~LogFile() {        f.close();    }    void shared_print(string msg, int id) {        std::unique_lock
guard(_mu); //do something 1 guard.unlock(); //临时解锁 //do something 2 guard.lock(); //继续上锁 // do something 3 f << msg << id << endl; cout << msg << id << endl; // 结束时析构guard会临时解锁 // 这句话可要可不要,不写,析构的时候也会自动执行 // guard.ulock(); }};

上面的代码可以看到,在无需加锁的操作时,可以先临时释放锁,然后需要继续保护的时候,可以继续上锁,这样就无需重复的实例化lock_guard对象,还能减少锁的区域。同样,可以使用std::defer_lock设置初始化的时候不进行默认的上锁操作:

void shared_print(string msg, int id) {    std::unique_lock
guard(_mu, std::defer_lock); //do something 1 guard.lock(); // do something protected guard.unlock(); //临时解锁 //do something 2 guard.lock(); //继续上锁 // do something 3 f << msg << id << endl; cout << msg << id << endl; // 结束时析构guard会临时解锁}