C++11 多线程并发 互斥量、条件变量

互斥量

C++11提供4种互斥量语义:

 

(C++11)
provides basic mutual exclusion facility
(class)
(C++11)
provides mutual exclusion facility which implements locking with a timeout
(class)
(C++11)
provides mutual exclusion facility which can be locked recursively by the same thread
(class)
provides mutual exclusion facility which can be locked recursively
by the same thread and implements locking with a timeout
(class)

 

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

 独占互斥量std::mutex

mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
互斥量提供独占的、非递归的所有权语义:
调用线程从成功调用lock或try_lock直到调用unlock为止都拥有互斥体。
1.当一个线程拥有互斥锁时,如果所有其他线程尝试声明该互斥锁的所有权,则所有其他线程都将阻塞(对于 lock 调用)或收到错误的返回值(对于 try_lock)。
2.在调用 lock 或 try_lock 之前,调用线程不得拥有互斥量。
如果互斥锁在仍由任何线程拥有时被销毁,或者线程在拥有互斥锁时终止,则程序的行为是未定义的。 mutex 类满足 Mutex 和 StandardLayoutType 的所有要求。
std::mutex 既不可复制也不可移动。

加锁操作:

lock() 加锁,独占性,锁定互斥量,如果互斥量不可用则阻塞。

trylock() 尝试锁定互斥量,如果互斥量不可用则返回

unlock()  对互斥量解锁

 示例代码:

#include <chrono>
#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));
    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 [139774937843456]
leaving thread [139774937843456]
enter thread [139774929450752]
leaving thread [139774929450752]
enter thread [139774921058048]
leaving thread [139774921058048]

std::lock_guard

lock_guard 类是一个互斥锁包装器,它提供了一种方便的 RAII 样式机制,用于在作用域块的持续时间内拥有互斥锁。
当创建 lock_guard 对象时,它会尝试获取所给定互斥锁的所有权。当控制权离开创建 lock_guard 对象的范围时,lock_guard 将被破坏并释放互斥锁。
lock_guard 类是不可复制的。

注意:初学者常见的一个错误是“忘记”给 lock_guard 变量命名,例如std::lock_guard(mtx); (默认构造一个名为 mtx 的 lock_guard 变量)或 std::lock_guard{mtx}; (它构造了一个立即销毁的纯右值对象),因此实际上并没有构造一个为其余作用域保存互斥锁的锁。

std::scoped_lock 提供了 lock_guard 的替代方案,它提供了使用死锁避免算法锁定多个互斥体的能力。(C++17)

void func(){
    std::lock_guard<mutex> lck(mutex_);
    // mutex_.lock();
    cout << "enter thread [" << this_thread::get_id() << "]" << endl;
    this_thread::sleep_for(chrono::seconds(1));
    cout << "leaving thread [" << this_thread::get_id() << "]" << endl;
    // mutex_.unlock();
}

递归互斥量std::recursive_mutex

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

recursive_mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
recursive_mutex 提供独占的、递归的所有权语义:
调用线程在成功调用 lock 或 try_lock 时开始一段时间内拥有 recursive_mutex。在此期间,线程可能会额外调用lock或try_lock。当线程发出匹配数量的解锁调用时,所有权期限结束。
当一个线程拥有 recursive_mutex 时,如果所有其他线程尝试声明 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock)或收到错误的返回值(对于 try_lock)。
recursive_mutex 可以被锁定的最大次数是未指定的,但在达到该次数后,对 lock 的调用将抛出 std::system_error 并且对 try_lock 的调用将返回 false。
如果 recursive_mutex 被销毁但仍由某个线程拥有,则程序的行为是未定义的。 recursive_mutex 类满足 Mutex 和 StandardLayoutType 的所有要求。

#include <chrono>
#include<iostream>
#include<thread>
#include<mutex>

using namespace std;

struct Complex{
    // std::mutex mutex_;
    std::recursive_mutex mutex_;
    int val_;
    Complex() : val_(0){}

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

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

    void both(int x, int y){
        // std::lock_guard<std:: mutex> lock(mutex_);
        std::lock_guard<std::recursive_mutex> lock(mutex_);
        mul(x);
        div(y);
    }
};

int main(int argv, char *argc[]){
    Complex complex;
    complex.both(32, 23);
    return 0;
}

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

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

timed_mutex是超时的独占锁,在mutex基础上增加了超时等待功能。

timed_mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
与互斥量类似,timed_mutex 提供独占的、非递归的所有权语义。此外,timed_mutex 还提供了通过成员函数 try_lock_for() 和 try_lock_until() 尝试在超时时声明 timed_mutex 所有权的能力。
timed_mutex 类满足 TimedMutex 和 StandardLayoutType 的所有要求。

recursive_timed_mutex是超时递归锁,在recursive_mutex基础上增加了超时等待功能。

recursive_timed_mutex 类是一个同步原语,可用于保护共享数据不被多个线程同时访问。
以类似于 std::recursive_mutex 的方式,recursive_timed_mutex 提供独占的、递归的所有权语义。此外,recursive_timed_mutex 还提供了通过 try_lock_for 和 try_lock_until 成员函数尝试声明超时的 recursive_timed_mutex 所有权的能力。
recursive_timed_mutex 类满足 TimedMutex 和 StandardLayoutType 的所有要求。

超时等待功能是指,等待指定时间后,如果还未取得锁,不再阻塞。

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>

std::condition_variable

condition_variable 类是与 std::mutex 一起使用的同步原语,用于阻止一个或多个线程,直到另一个线程修改共享变量(条件)并通知 condition_variable。
打算修改共享变量的线程必须:
  1.获取 std::mutex(通常通过 std::lock_guard)。
  2.在拥有锁的情况下修改共享变量。
  3.对 std::condition_variable 调用notify_one或notify_all(可以在释放锁后完成)。
即使共享变量是原子的,也必须在拥有互斥体的同时对其进行修改,才能正确地将修改发布到等待线程。
任何想要等待 std::condition_variable 的线程必须:
1.获取用于保护共享变量的互斥锁上的 std::unique_lock<std::mutex>。
2.执行以下操作之一:
  2.1检查情况,是否已更新并通知。
  2.2对 std::condition_variable 调用 wait、wait_for 或 wait_until(以原子方式释放互斥体并挂起线程执行,直到通知条件变量、超时到期或发生虚假唤醒,然后在返回之前以原子方式获取互斥体)。
  2.3检查条件,如果不满足则继续等待。
  或者:
  使用 wait、wait_for 和 wait_until 的谓词重载,它执行相同的三个步骤。
std::condition_variable 仅适用于 std::unique_lock<std::mutex>,它允许在某些平台上实现最大效率。 std::condition_variable_any 提供可与任何 BasicLockable 对象一起使用的条件变量,例如 std::shared_lock。
条件变量允许同时调用 wait、wait_for、wait_until、notify_one 和 notify_all 成员函数
类 std::condition_variable 是一个 StandardLayoutType。它不是可复制构造、可移动构造、可复制分配或可移动分配。

std::condition_variable 仅适用于 std::unique_lock<std::mutex> 

示例

 

#include<condition_variable>
#include<iostream>
#include<mutex>
#include<string>
#include<thread>

using namespace std;

std::mutex m;
std::condition_variable cv;
string data;
bool re = false;
bool pro = false;

void worker_thread(){
    std::unique_lock<std::mutex> lock(m);
    cv.wait(lock, []{return re;});

    cout << "Worker thread is processing data\n";
    data += " after processing";

    pro = true;
    cout << "Worker thread signals data processing completed\n";

    lock.unlock();
    cv.notify_one();
}

int main()
{
    std::thread worker(worker_thread);
    data = "Data";
    {
        std::lock_guard<std::mutex> lock(m);
        re = true;
        cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
    {
        std::unique_lock<std::mutex> lock(m);
        cv.wait(lock, []{return pro;});
    }
    cout << "back in main(), data = " << data << '\n';

    worker.join();
}

运行结果:

main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
back in main(), data = Data after processing

 

std::condition_variable_any

condition_variable_any 类是 std::condition_variable 的泛化。 std::condition_variable 仅适用于 std::unique_lock<std::mutex>,而 condition_variable_any 可以对满足 BasicLockable 要求的任何锁进行操作。
有关条件变量语义的描述,请参见 std::condition_variable。
类 std::condition_variable_any 是一个 StandardLayoutType。它不是可复制构造、可移动构造、可复制分配或可移动分配。
如果锁是 std::unique_lock<std::mutex>,则 std::condition_variable 可能会提供更好的性能。

注意:

std::condition_variable_any 可以与 std::shared_lock 一起使用,以便在共享所有权模式下等待 std::shared_mutex。
std::condition_variable_any 与自定义 Lockable 类型的可能用途是提供方便的可中断等待:自定义锁定操作将按预期锁定关联的互斥锁,并在收到中断信号时执行必要的设置以通知此条件变量。 

示例代码:

#include<condition_variable>
#include <cstddef>
#include<iostream>
#include<mutex>
#include<string>
#include<thread>
#include<list>
using namespace std;

template<typename T>
class SyncQueue{
private:
    std::list<T> queue_;
    std::mutex mutex_;
    std::condition_variable_any not_empty_;
    std::condition_variable_any not_full_;
    int max_size_;

    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> lock(mutex_);
        not_full_.wait(mutex_, [this]{return !this->IsFull();});

        queue_.push_back(x);
        not_empty_.notify_one();
    }

    void Take(T& x){
        std::lock_guard<std::mutex> lock(mutex_);
        not_empty_.wait(mutex_, [this]{return !this->isEmpty();});

        x = queue_.front();
        queue_.pop_front();
        not_full_.notify_one();
    }

    bool Empty() {
        std::lock_guard<std::mutex> lock(mutex_);
        return queue_.empty();
    }

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

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

SyncQueue<int> myQue(5);

void Producer(){
    for(int i = 0; i < 10; ++i){
        myQue.Put(i);
        cout << "Produced: " << i << endl;
    }
}

void Consumer(){
    for(int i = 0; i < 10; ++i){
        myQue.Take(i);
        cout << "Consumed: " << i << endl;
    }
}

int main(){
    std::thread P(Producer);
    std::thread C(Consumer);

    P.join();
    C.join();
    return 0;
}

运行结果:

Produced: 0
Produced: 1
Produced: 2
Produced: 3
Produced: 4
Consumed: 0
Consumed: 1
Consumed: 2
Consumed: 3
Consumed: 4
Produced: 5
Produced: 6
Produced: 7
Produced: 8
Produced: 9
Consumed: 5
Consumed: 6
Consumed: 7
Consumed: 8
Consumed: 9

  

call_once/once_flag

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

示例代码:

#include<condition_variable>
#include<iostream>
#include<mutex>
#include<string>
#include<thread>

using namespace std;

std::once_flag g_flag;
void work() {
	call_once(g_flag, []() { cout << "Called once" << endl; }); 
}

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

 

运行结果:

Called once

不管开多少个线程,只会调用一次函数。

posted @ 2023-11-24 21:45  dustemoff  阅读(93)  评论(0编辑  收藏  举报