C11线程管理:互斥锁

1、概述

锁类型

c11提供了跨平台的线程同步手段,用来保护多线程同时访问的共享数据。
std::mutex,最基本的 Mutex 类,独占的互斥量,不能递归使用。
std::time_mutex,带超时的独占互斥量,不能递归使用。
std::recursive_mutex,递归互斥量,不带超时功能。
std::recursive_timed_mutex,带超时的递归互斥量。

lock类型

std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

锁函数

std::try_lock,尝试同时对多个互斥量上锁。
std::lock,可以同时对多个互斥量上锁。
std::unlock,解锁。

2、独占互斥量std::mutex

互斥量使用都很简单,接口用法也很简单。一般是通过lock()来阻塞线程,直到获取到互斥量为止。在获取互斥量完成之后,使用unlock()来解除互斥量的占用。lock()和unlock()必须成对出现。

std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。

lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

unlock(), 解锁,释放对互斥量的所有权。

try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

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

std::mutex g_lock;

void vFunc()
{
    g_lock.lock();

    std::cout << "entered thread:" << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "leave thread:" << std::this_thread::get_id() << std::endl;

    g_lock.unlock();
}

int main()
{
    std::thread t(vFunc);
    std::thread t1(vFunc);
    std::thread t2(vFunc);

    t.join();
    t1.join();
    t2.join();

    return 0;
}
//输出结果
entered thread:4420
leave thread:4420
entered thread:5288
leave thread:5288
entered thread:1432
leave thread:1432

3、std::lock_guard和std::unique_lock

使用lock_guard和std::unique_lock可以简化lock/unlock的写法,同时也更安全,因为lock_guard使用了RAII技术,在构造分配资源,在析构释放资源,会在构造的时候自动锁定互斥量,在退出作用域之后进行析构自动解锁。所以不用担心没有解锁的情况,更加安全。

std::lock_guard与 Mutex RAII 相关,方便线程对互斥量上锁,std::unique_lock与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制,它可以自由的释放mutex,而std::lock_guard需要等生命周期结束才能释放。

void vFunc()
{
    std::lock_guard<std::mutex> locker(g_lock);//出作用域自动解锁
    std::cout << "entered thread:" << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds());
    std::cout << "leave thread:" << std::this_thread::get_id() << std::endl;
}

3、递归互斥锁std::recuisive_mutex

同一个线程不能多次获取同一个独占互斥锁,一个线程多次获取同一个互斥锁发生死锁。

//std::mutex mutex;

void funcA()
{
    std::lock_guard<std::mutex> lock(mutex);
    //do something
}

void funcB()
{
    std::lock_guard<std::mutex> lock(mutex);
    //do something
}

void funcC()
{
    std::lock_guard<std::mutex> lock(mutex);
    funcA();
    funcB();
}

int main()
{
    //发生死锁,funcC已经获取,无法释放,funcA无法获取
    funcC(); 
    
    return ;
}

为了解决死锁的问题,C11有了递归锁std::recursive_mutex,递归锁允许一个线程多次获取该锁。

//std::recursive_mutex mutex;

void funcA()
{
    std::lock_guard<std::recursive_mutex> lock(mutex);
    //do something
}

void funcB()
{
    std::lock_guard<std::recursive_mutex> lock(mutex);
    //do something
}

void funcC()
{
    std::lock_guard<std::recursive_mutex> lock(mutex);
    funcA();
    funcB();
}

int main()
{
    //同一个线程可以多次获取同一互斥量,不会发生死锁
    funcC(); 
    
    return ;
}

需要注意的是,尽量少用递归锁,原因如下:

允许递归互斥容易放纵复杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题;
递归锁比非递归锁效率低;
递归锁虽然可以在同一线程多次获取,但是获取次数过多容易发生问题,引发std::system错误。

4、超时锁

std::time_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用于在获取锁时增加超时等待功能,设置一个等待获取锁的时间,超时后做其他的事情。超时锁多了两个获取锁的接口,try_lock_for和try_lock_until,这两个接口用来获取互斥量的超时时间。

try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

#include <iostream>       // std::cout
#include <chrono>         // std::chrono::milliseconds
#include <thread>         // std::thread
#include <mutex>          // std::timed_mutex

std::timed_mutex mtx;

void fireworks() {
    // waiting to get a lock: each thread prints "-" every 200ms:
    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
        std::cout << "-";
    }
    // got a lock! - wait for 1s, then this thread prints "*"
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::cout << "*\n";
    mtx.unlock();
}

int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i<10; ++i)
        threads[i] = std::thread(fireworks);

    for (auto& th : threads) th.join();

    return 0;
}

相关推荐