文章目录
- 前言
- 一、互斥锁
- 2.lock_guard
- 3.unique_lock
- 二、条件变量
- condition_variable
- 三、信号量
- semaphore
- 四、异步操作
- 构造方式
- 五、原子操作
- 备注
前言
关于C++多线程编程的几种实现方式(互斥锁、条件变量、信号量、异步操作、原子操作)小结
一、互斥锁
使用锁的方式对共享资源对象的访问进行控制,操作包括上锁lock()、解锁unlock()
- include <mutex>
- lock和unlock必须成对出现
- 构造出来的mutex对象都是unlock状态
- mutex不允许拷贝构造
- 当前线程lock了mutex,该线程在unlock之前一直拥有该锁,被锁期间阻塞。如果是自身线程调用了lock锁住mutex,可能会产生死锁
- try_lock(),尝试锁,即使被占有也不会阻塞(失败会返回false)
- mutex为最简单的互斥锁,不支持递归上锁,需要递归上锁要使用recursive_lock类
2.lock_guard
- 构造方式:lock_guard<mutex> lck(mtx);
- 相比较mutex最大特点是,构造时自动lock,离开作用域后调用析构函数unlock,可以与智能指针类比
- 采用资源请求初始化(RAII)编程技术,针对动态分配的资源(需要申请内存,非static对象)
- 不能手动解锁,不能拷贝
3.unique_lock
- 比lock_guard功能更加强大,但开销也大
- 构造时选择加锁方式:defer_lock延迟加锁;try_lock尝试加锁;adopt_lock立刻加锁
- release()函数,返回管理的mutex对象指针,释放所有权(不能再对该mutex进行控制),之后用户可以对mutex进行手动unlock操作,此功能给予更大的编程*度,从而完成相应功能
- 配合condition_variable使用,condition_variable wait()函数第一个参数为unique_lock类型
- 不允许复制,但允许"move",即对mutex的所有权进行传递,lck1对mymutex所有权转移至lck2:
unique_lock<mutex>lck1(mymutex);
unique_lock<mutex> lck2(mymutex);
二、条件变量
条件不满足,线程被阻塞。一个线程等待某一变量的成立而阻塞,另一线程使该变量变化而使上一线程成立,同时发送信号(notify)唤醒wait的线程
condition_variable
- 获取mutex,确切的说为mutex构造的unique_lock对象
- wait_for(),wait_until(),第二个参数为chrono(时间库)
- wait_for(),wait()条件成立 or 超过给定时间段的时间自动unlock
- wait_until(),同上,但时间段改为时刻
- notify_all(),notify_one(),全部唤醒 or 唤醒之一
三、信号量
提供原子操作,C语言提供
semaphore
- include <>
- 定义:sem_t mysem;
- 初始化:int sem_init(sem_t *sem,int pshared,unsigned int value); value 参数指定信号量的初始值。一般:sem_init(&mysem,0,0);需要首次执行的:sem_init(&mysem,0,1);
pshared 参数指明信号量是由进程内线程共享,还是由进程之间共享。如果 pshared 的值为 0,那么信号量将被进程内的线程共享,并且应该放置在这个进程的所有线程都可见的地址上(如全局变量,或者堆上动态分配的变量)。
- int sem_wait(sem_t *sem); //sem+1 >0时终端阻塞,继续执行
- int sem_post(sem_t *sem); //sem-1
- int sem_trywait(sem_t *sem);
- int sem_getvalue(sem_t *sem);
四、异步操作
在不需要等待被调用方返回之前,继续进行后续操作。c++11提供了异步接口std::async,std::async会自动创建一个线程去调用 线程函数,它返回一个std::future,这个future中存储了线程函数返回的结果,当我们需要线程函数的结果时,直接从future中获取
构造方式
-
template <class Fn, class… Args> future<typename
result_of<Fn(Args…)>::type>
async (Fn&& fn, Args&&… args); -
template <class Fn, class… Args> future<typename
result_of<Fn(Args…)>::type>
async (launch policy, Fn&& fn, Args&&… args); -
policy:
launch::async: 异步启动一个新线程调用fn
launch::deferred延迟:对 fn 的调用被延迟,直到返回的未来的共享状态被访问(等待或获取)。 此时,调用 fn 并且该函数不再被视为延迟。 当此调用返回时,返回的未来的共享状态已准备就绪。
launch::async|launch::deferred:该函数自动选择策略(在某一点)。这取决于系统和库实现,它们通常针对系统中当前的并发可用性进行优化。 -
fn 函数指针,可以为仿函数、lambda表达式,类的成员函数等
-
args 传递给fn的参数
- future是一个可以从某个提供者对象或函数中检索值的对象,如果在不同的线程中,可以正确地同步这种访问
- template future;
template <class R&> future<R&>;
template <> future<void>; - future是与共享状态相关联的未来对象,并且通过调用以下函数之一来构造:
async
promise::get_future
packaged_task::get_future - get()取值、valid()共享是否有效、wait()阻塞等待(由valid判断,false阻塞,true继续执行)、wait_for()、wait_until()同上
- promise是一个对象,它可以存储一个T类型的值,供将来的对象(可能在另一个线程中)检索,提供一个同步点。
- 这个共享状态可以通过调用成员get_future关联到一个未来对象。调用后,两个对象共享相同的共享状态: promise对象是异步提供者,应该在某个时候为共享状态设置一个值。 future对象是一个异步返回对象,它可以检索共享状态的值,并在必要时等待它准备就绪。
- set_value()设置value并通知future
五、原子操作
原子操作能够保证多个线程顺序访问,不会导致数据争用,执行时没有任何其它线程能够修改相同的原子对象。原子操作更加接近底层,因而效率更高。
- std::atomic<T>构造对象
- atomic_long total(0);
- atomic<bool> flag{ false }
- 底层原理:自旋锁 + CAS(乐观锁)
- 乐观锁:更新值时进行compareAndSwap操作,即当内存当前值等于内存预期值时,将要更新的值赋给内存,不等时不操作,之后可重试
- 自旋锁:尝试获取锁的所有权时会以忙等待的形式不断的循环检查锁是否可用
备注
以上为实际遇到的问题,待补充