系列文章目录
C++11多线程---互斥锁(二)
C++11多线程---原子操作变量(四)
C++11多线程---异步操作(五)
目录
3 带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
4 lock_guard和unique_lock的使用和区别
4.2 uniqh3ue_lock,lock_guard的区别
前言
mutex又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 #include <mutex>头文件中,所以如果你需要使用 std::mutex,就必须包含该头文件。
一、C++11中的互斥量
C++11提供如下4种语义的互斥量(mutex)
- std::mutex,独占的互斥量,不能递归使用。
- std::time_mutex,带超时的独占互斥量,不能递归使用。
- std::recursive_mutex,递归互斥量,不带超时功能。
- std::recursive_timed_mutex,带超时的递归互斥量。
1.独占互斥量std::mutex
std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。
1.1 std::mutex 的成员函数
- 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于unlocked 状态的。
- lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
- unlock(), 解锁,释放对互斥量的所有权。try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
1.2 代码示例如下
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
using namespace std;
volatile int counter(0); // non-atomic counter
mutex mtx; // locks access to counter
void increases_10k()
{
for (int i=0; i<10000; ++i) {
// 1. 使用try_lock的情况
// if (mtx.try_lock()) { // only increase if currently not locked:
// ++counter;
// mtx.unlock();
// }
// 2. 使用lock的情况
{
mtx.lock();
++counter;
mtx.unlock();
}
}
}
int main()
{
std::thread threads[10];
for (int i=0; i<10; ++i)
threads[i] = std::thread(increases_10k);
for (auto& th : threads)
th.join();
cout << " successful increases of the counter " << counter << endl;
return 0;
}
2.递归互斥量std::recursive_mutex
递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。
2.1 死锁范例
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
struct Complex
{
std::mutex mutex;
int i;
Complex() : i(0){}
void mul(int x)
{
lock_guard<mutex> lock(mutex);
i *= x;
}
void div(int x)
{
lock_guard<mutex> lock(mutex);
i /= x;
}
void both(int x, int y)
{
lock_guard<mutex> lock(mutex);
mul(x);
div(y);
}
};
int main(void)
{
Complex complex;
complex.both(32, 23);
return 0;
}
2.2 使用递归锁
//递归锁
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
struct Complex
{
std::recursive_mutex mutex;
int i;
Complex() : i(0){}
void mul(int x)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
i *= x;
}
void div(int x)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
i /= x;
}
void both(int x, int y)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
mul(x);
div(y);
}
};
int main(void)
{
Complex complex;
complex.both(32, 23); //因为同一线程可以多次获取同一互斥量,不会发生死锁
std::cout << "main finish\n";
return 0;
}
虽然递归锁能解决这种情况的死锁问题,但是尽量不要使用递归锁,主要原因如下:
- 需要用到递归锁的多线程互斥处理本身就是可以简化的,允许递归很容易放纵复杂逻辑的产生,并且产生晦涩,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁;
- 递归锁比起非递归锁,效率会低;
- 递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦超过一定的次数,再对lock进行调用就会抛出std::system错误。
3 带超时的互斥量std::timed_mutex和std::recursive_timed_mutex
std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;
timed_mutex mutex;
void func()
{
chrono::milliseconds timeout(100);
while (true)
{
if (mutex.try_lock_for(timeout))
{
cout << this_thread::get_id() << ": do func with the mutex" << endl;
chrono::milliseconds sleepDuration(250);
this_thread::sleep_for(sleepDuration);
mutex.unlock();
this_thread::sleep_for(sleepDuration);
}
else
{
cout << this_thread::get_id() << ": do work without the mutex" <<endl;
chrono::milliseconds sleepDuration(100);
this_thread::sleep_for(sleepDuration);
}
}
}
int main(void)
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << "exit\n";
return 0;
}
4 lock_guard和unique_lock的使用和区别
相对于手动lock和unlock,我们可以使用RAII(通过类的构造析构)来实现更好的编码方式。
RAII:也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。
4.1 unique_lock,lock_guard的使用
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_error
using namespace std;
mutex mtx;
void print_even (int x) {
if (x%2==0)
cout << x << " is even\n";
else
throw (logic_error("not even"));
}
void print_thread_id (int id) {
try {
// using a local lock_guard to lock mtx guarantees unlocking on
destruction / exception:
lock_guard<mutex> lck (mtx);
print_even(id);
}
catch (std::logic_error&) {
cout << "[exception caught]\n";
}
}
int main ()
{
thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = thread(print_thread_id,i+1);
for (auto& th : threads)
th.join();
return 0;
}
4.2 uniqh3ue_lock,lock_guard的区别
- unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。
- unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,lck.lock()进行上锁,而不必等到析构时自动解锁。
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <unistd.h>
using namespace std;
deque<int> q;
mutex mu;
condition_variable cond;
int count = 0;
void fun1() {
while (true) {
// {
unique_lock<mutex> locker(mu);
q.push_front(count++);
locker.unlock(); // 这里是不是必须的?
cond.notify_one();
// }
sleep(1);
}
}
void fun2() {
while (true) {
unique_lock<mutex> locker(mu);
cond.wait(locker, [](){return !q.empty();});
auto data = q.back();
q.pop_back();
// locker.unlock(); // 这里是不是必须的?
cout << "thread2 get value form thread1: " << data << std::endl;
}
}
int main() {
thread t1(fun1);
thread t2(fun2);
t1.join();
t2.join();
return 0;
}
总结
- std::lock_guard
- std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁。
- 2.锁在多线程编程中,使用较多,因此c++11提供了lock_guard模板类;在实际编程中,我们也可以根据自己的场景编写resource_guard RAII类,避免忘掉释放资源。
- std::unique_lock
- unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。
- unique_lock比lock_guard使用更加灵活,功能更加强大。
- 使用unique_lock需要付出更多的时间、性能成本。