博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++并行编程
阅读量:326 次
发布时间: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会临时解锁}

这样使用起来就比lock_guard更加灵活!然后这也是有代价的,因为它内部需要维护锁的状态,所以效率要比lock_guard低一点,在lock_guard能解决问题的时候,就是用lock_guard,反之,使用unique_lock。

注意

unique_lock和lock_guard都不能复制,lock_guard不能移动,但是unique_lock可以。

// unique_lock 可以移动,不能复制std::unique_lock
guard1(_mu);std::unique_lock
guard2 = guard1; // errorstd::unique_lock
guard2 = std::move(guard1); // ok// lock_guard 不能移动,不能复制std::lock_guard
guard1(_mu);std::lock_guard
guard2 = guard1; // errorstd::lock_guard
guard2 = std::move(guard1); // error

多线程下生产者消费者模型

单生产者-单消费者模型

单生产者-单消费者模型中只有一个生产者和一个消费者,生产者不停地往产品库中放入产品,消费者则从产品库中取走产品,产品库容积有限制,只能容纳一定数目的产品,如果生产者生产产品的速度过快,则需要等待消费者取走产品之后,产品库不为空才能继续往产品库中放置新的产品,相反,如果消费者取走产品的速度过快,则可能面临产品库中没有产品可使用的情况,此时需要等待生产者放入一个产品后,消费者才能继续工作。

单生产者-多消费者模型

与单生产者和单消费者模型不同的是,单生产者-多消费者模型中可以允许多个消费者同时从产品库中取走产品。所以除了保护产品库在多个读写线程下互斥之外,还需要维护消费者取走产品的计数器。

多生产者-单消费者模型

与单生产者和单消费者模型不同的是,多生产者-单消费者模型中可以允许多个生产者同时向产品库中放入产品。所以除了保护产品库在多个读写线程下互斥之外,还需要维护生产者放入产品的计数器。

多生产者-多消费者模型

该模型可以说是前面两种模型的综合,程序需要维护两个计数器,分别是生产者已生产产品的数目和消费者已取走产品的数目。另外也需要保护产品库在多个生产者和多个消费者互斥地访问。

future

thread对象,它是C++11中提供异步创建多线程的工具。但是我们想要从线程中返回异步任务结果,一般需要依靠全局变量;从安全角度看,有些不妥;为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。

在这里插入图片描述

在C++标准库中,有两种“期望”,使用两种类型模板实现:

唯一期望(unique futures,std::future<>) std::future的实例只能与一个指定事件相关联。

共享期望(shared futures)(std::shared_future<>) std::shared_future的实例就能关联多个事件。

future 头文件中包含了以下几个类和函数:

Providers 类:std::promise, std::package_task

Futures 类:std::future, shared_future.
Providers 函数:std::async()
其他类型:std::future_error, std::future_errc, std::future_status, std::launch.

std::future

//通过async来获取异步操作结果std::future
result = std::async([](){ std::this_thread::sleep_for(std::chrono::milliseconds(500)); return 8; });std::cout << "the future result : " << result.get() << std::endl;std::cout << "the future status : " << result.valid() << std::endl;try{ result.wait(); //或者 result.get() ,会异常 //因此std::future只能用于单线程中调用 ,多线程调用使用std::share_future();}catch (...){ std::cout << "get error....\n ";}

转载地址:http://fnuh.baihongyu.com/

你可能感兴趣的文章