C++11 多线程之互斥量、条件变量、call_once使用简介

互斥量

C++11提供4种互斥量(mutex)语义,对于4个类:

  • std::mutex 独占互斥量,不能递归加锁;
  • std::timed_mutex 带超时的独占互斥量,超时自动解锁,不能递归加锁;
  • std::recursive_mutex 递归互斥量,不带超时解锁功能;
  • std::recursive_timed_mutex 带超时功能的递归互斥量,超时自动解锁,能递归加锁;

头文件:

独占互斥量std::mutex

独占互斥量又称互斥量,互斥锁,独占锁,顾名思义,同一时刻只能有一个线程取得该锁,其他试图取得该锁的线程阻塞,待持有锁的线程释放独占锁时,才能唤醒取得独占锁后继续运行。
互斥量不允copy操作(copy构造、copy assignment),不允许move操作(move构造、move assignment),最初参数的mutex对象是unlocked(未加锁)状态。

mutex的同样操作
1)lock(),加锁,独占性占用互斥量资源;
2)unlock(),解锁,解除对互斥量的占用,必须和lock成对出现;
3)try_lock(),尝试锁定互斥量,成功返回true;失败返回false,非阻塞;

示例:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

std::mutex mutex_; // 独占互斥量 

void func()
{
	mutex_.lock();
	cout << "enter thread [" << this_thread::get_id() << "]" << endl;
	this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒
	cout << "leaving thread [" << this_thread::get_id() << "]" << endl;

	mutex_.unlock();
}
int main(int argc, char *argv[])
{
	thread t1(func);
	thread t2(func);
	thread t3(func);

	t1.join();
	t2.join();
	t3.join();
	return 0;
}

运行结果:

enter thread [13660]
leaving thread [13660]
enter thread [13372]
leaving thread [13372]
enter thread [13368]
leaving thread [13368]

lock_guard与mutex
lock_guard类可以简化mutex的lock/unlock写法,利用loak_guard对象的构造对mutex加锁,对象的析构对mutex解锁。即所谓RAII技术。这样,可以保证在资源除了作用域后就释放,即使中间发生异常,也能正常解锁。缺点是,会带来额外的对象构造和析构性能消耗。
将上面的例子,改造成利用lock_guard lock/unlock:

void func()
{
	lock_guard<mutex> lck(mutex_); // 自动对mutex_加锁(lock)
	cout << "enter thread [" << this_thread::get_id() << "]" << endl;
	this_thread::sleep_for(chrono::seconds(1)); // 休眠1秒
	cout << "leaving thread [" << this_thread::get_id() << "]" << endl;
	// 退出函数作用域时,析构loak_guard对象,自动释放mutex_锁(unlock)
}

递归互斥量 std::recursive_mutex

递归互斥量又称递归锁,可以解决同一个线程多次获取同一个互斥量导致死锁问题。不过,要求解锁次数 等于 加锁次数,否则不能正常解锁。

示例:

// 同一线程多次获取同一个互斥量导致死锁问题的例子
struct Complex {
	std::mutex mutex_;
	int val_;
	Complex() : val_(0) {}

	void mul(int x) {
		std::lock_guard<std::mutex> lock(mutex_);
		val_ *= x;
	}

	void div(int x) {
		std::lock_guard<std::mutex> lock(mutex_);
		val_ /= x;
	}

	void both(int x, int y) {
		std::lock_guard<std::mutex> lock(mutex_);
		mul(x);  // 同一线程多次对mutex_加锁,会导致死锁
		div(y);  // 同一线程多次对mutex_加锁,会导致死锁
	}
};
int main(int argc, char *argv[])
{
	Complex complex;
	complex.both(32, 23);
	return 0;
}

将例子改造成使用递归锁recursive_mutex

// 使用递归锁recursive_mutex
struct Complex {
	std::recursive_mutex mutex_;
	int val_;
	Complex() : val_(0) {}

	void mul(int x) {
		std::lock_guard<std::recursive_mutex> lock(mutex_);
		val_ *= x;
	}

	void div(int x) {
		std::lock_guard<std::recursive_mutex> lock(mutex_);
		val_ /= x;
	}

	void both(int x, int y) {
		std::lock_guard<std::recursive_mutex> lock(mutex_);
		mul(x); // 不会产生死锁
		div(y); // 不会产生死锁
	}
};
...

TIP:能不使用递归锁,尽量不用。原因在于:
1)需要用到递归锁的多线程互斥处理的情况,本身往往可以简化,而允许递归互斥很容易导致复杂逻辑的产生,从而导致多线程同步引起的晦涩难懂的问题;
2)递归锁比非递归锁,效率更低;
3)递归锁虽然允许同一线程多次获得同一个互斥量,可重复获得的最大次数并未具体说明,但一旦超过一定次数,再调用lock会抛出std::system错误。

带超时的互斥量std::timed_mutex及std::recursive_timed_mutex

timed_mutex是超时的独占锁,在mutex基础上增加了超时等待功能。
recursive_timed_mutex是超时递归锁,在recursive_mutex基础上增加了超时等待功能。
超时等待功能是指,等待指定时间后,如果还未取得锁,不再阻塞。

timed_mutex 示例
recursive_timed_mutex类似

timed_mutex mutex_; // 超时独占锁

void work() {
	chrono::microseconds timeout(100); // 100 ms
	while (true) {
		// try to wait the lock
		if (mutex_.try_lock_for(timeout)) { // success to get the lock
			cout << this_thread::get_id() << ": do work with the mutex" << endl;

			chrono::milliseconds sleepDuration(250);
			this_thread::sleep_for(sleepDuration);

			mutex_.unlock();
		}
		else { // timed out, fail to get the lock
			cout << this_thread::get_id() << ": do work without the mutex" << endl;

			chrono::milliseconds sleepDuration(100);
			this_thread::sleep_for(sleepDuration);
		}
	}
}

int main(int argc, char *argv[])
{
	thread t1(work);
	thread t2(work);
	t1.join();
	t2.join();
	return 0;
}

条件变量

条件变量,是一种用于多线程等待的同步机制。条件变量能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。
条件变量需要和互斥量搭配使用。

C++11提供2种条件变量:

  • condition_variable 搭配std::unique_lock<std::mutex> 进行wait操作;
  • condition_variable_any 搭配任意带有lock/unlock语义的mutex使用,较灵活,但效率比condition_variable 更低;

头文件:<condition_variable>

condition_variable

condition_variable 的5个函数:

  • wait 阻塞当前线程,等待唤醒;
  • wait_for 阻塞当前线程,等待唤醒,最多等待一段时间;
  • wait_until 阻塞当前线程,等待唤醒,最多等待到某个时间点;
  • notify_one 唤醒一个等待在这个条件变量上的线程;
  • notify_all 唤醒所有等待在这个条件变量上的线程;
    condition_variable_any 也拥有这5个函数。

使用condition_variable_any搭配lock_guard示例
同步队列:当队列满时,阻塞插入线程,无法再往内部插入数据,直到另一个线程取走数据满足队列非满条件;
当队列空时,阻塞取数据线程,无法再从内部取走数据,直到另一个线程插入数据满足队列非空条件。

/**
同步队列类
*/
template<typename T>
class SyncQueue {
private:
	// 内部使用, 非线程安全
	bool IsFull() const {
		return queue_.size() == max_size_;
	}
	// 内部使用, 非线程安全
	bool IsEmpty() const {
		return queue_.empty();
	}

public:
	SyncQueue(int max_size) : max_size_(max_size) {

	}
	// 插入数据
	void Put(const T& x) {
		std::lock_guard<std::mutex> lck(mutex_);
		/* while语句 <=>
		not_full_.wait(mutex_, [this] { return !this->IsFull(); }) 
		*/
		while (IsFull()) {
			cout << "缓冲区满了,需要等待..." << endl;
			not_full_.wait(mutex_); // 等待条件not_full_
		}

		queue_.push_back(x);
		not_empty_.notify_one(); // 随机唤醒一个等待在条件变量not_empty_上的线程
	}
	// 取出数据
	void Take(T& x) {
		std::lock_guard<std::mutex> lck(mutex_);
		while (IsEmpty()) {
			cout << "缓冲区空了,需要等待..." << endl;
			not_empty_.wait(mutex_); // 等待条件not_empty_
		}

		x = queue_.front();
		queue_.pop_front();
		not_full_.notify_one(); // 随机唤醒一个等待在条件变量not_full_上的线程
	}

	// 公共接口,线程安全,注意mutex_是独占锁(下面3个函数同)
	bool Empty() {
		std::lock_guard<std::mutex> lck(mutex_);
		return queue_.empty();
	}

	bool Full() {
		std::lock_guard<std::mutex> lck(mutex_);
		return queue_.size() == max_size_;
	}

	size_t Size() {
		std::lock_guard<std::mutex> lck(mutex_);
		return queue_.size();
	}

private:
	std::list<T> queue_;
	std::mutex mutex_;
	std::condition_variable_any not_empty_;
	std::condition_variable_any not_full_;
	int max_size_;
};

注意:Put中的wait代码可以改写成lambda形式

std::lock_guard<std::mutex> lck(mutex_);
while (IsFull()) {
	cout << "缓冲区满了,需要等待..." << endl;
	not_full_.wait(mutex_); // 等待条件not_full_
}
// 可以改写成
not_full_.wait(mutex_, [this] { return !this->IsFull(); }) 

wait的第二个参数判别式为true时,线程不会放弃锁,会继续执行;当判别式为false时,线程放弃锁,阻塞。

unique_lock与lock_guard

由于condition_variable_any 只能搭配unique_lock使用,我们研究下unique_lock与lock_guard有何区别?
最大区别在于,unique_lock不像lock_guard只能在析构时才释放锁,而是可以随时调用unlock释放锁。另外,可以构造一个空的unique_lock,却无法构造一个空的lock_guard,也就是说,lock_guard必须绑定一个mutex。

使用condition_variable搭配unique_lock示例
我们将上面condition_variable_any + lock_guard的同步队列示例,修改为condition_variable + unique_lock

/**
同步队列类
*/
template<typename T>
class SyncQueue {
	...
	// 插入数据
	void Put(const T& x) {
		std::unique_lock<std::mutex> lck(mutex_);
		not_full_.wait(mutex_, [this] { return !this->IsFull(); })

		queue_.push_back(x);
		not_empty_.notify_one(); // 随机唤醒一个等待在条件变量not_empty_上的线程
	}
	// 取出数据
	void Take(T& x) {
		std::unique_lock<std::mutex> lck(mutex_);
		not_full_.wait(mutex_, [this] { return !this->IsEmpty(); });

		x = queue_.front();
		queue_.pop_front();
		not_full_.notify_one(); // 随机唤醒一个等待在条件变量not_full_上的线程
	}
	...
private:
	...
	std::mutex mutex_;
	std::condition_variable not_empty_;
	std::condition_variable not_full_;
	...
};

call_once/once_flag

Linux有pthread_once可以确保函数只被调用一次,C++有没有类似技术?
答案是有的,那就是使用call_once/once_flag。在多线程环境中,如需要某个对象只初始化一次,可以用std::call_once。用std::call_once时,需要提供一个once_flag参数。

头文件:

示例

once_flag g_flag;
void work() {
	call_once(g_flag, []() { cout << "Called once" << endl; }); // call_once确保传入的可调用对象(lambda表达式)只会被调用一次
}

int main(int argc, char *argv[])
{
	thread t1(work);
	thread t2(work);
	t1.join();
	t2.join();
	return 0;
}

输出:

Called once

原文链接: https://www.cnblogs.com/fortunely/p/15811707.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    C++11 多线程之互斥量、条件变量、call_once使用简介

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/185895

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 上午11:01
下一篇 2023年2月12日 上午11:01

相关推荐