• 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
    
  • 相关阅读:
    高斯模糊原理,算法
    SIFT算法详解
    第五章:状态图
    ANTLR4权威指南
    第八章:包图,组件图,部署图
    棋盘n皇后问题-递归
    普通页面引入React(使用和不使用JSX)
    浏览器环境
    DevTool-Network
    优化浏览器渲染
  • 原文地址:https://www.cnblogs.com/fortunely/p/15811707.html
Copyright © 2020-2023  润新知