《深入应用C++11》笔记-互斥量std::mutex、锁std::lock_guard

上一篇:《深入应用C++11》笔记-线程std::thread

C++11中提供了std::mutex互斥量,共包含四种类型:

  • std::mutex:最基本的mutex类。
  • std::recursive_mutex:递归mutex类,能多次锁定而不死锁。
  • std::time_mutex:定时mutex类,可以锁定一定的时间。
  • std::recursive_timed_mutex:定时递归mutex类。

另外,还提供了两种锁类型:

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

    以及相关的函数:

  • std::try_lock:尝试同时对多个互斥量上锁。
  • std::lock:可以同时对多个互斥量上锁。
  • std::call_once:如果多个线程需要同时调用某个函数,call_once可以保证多个线程对该函数只调用一次。

std::mutex

std::mutex是C++中最基本的互斥量,提供了独占所有权的特性,std::mutex提供了以下成员函数

  • 构造函数:std::mutex不允许拷贝构造,也不允许move拷贝,最初产生的mutex对象是处于unlocked状态的。
  • lock():调用线程将锁住该互斥量,线程调用该函数会发生以下3种情况:
    (1)如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用unlock之前,该线程一直拥有该锁。
    (2)如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
    (3)如果当前互斥量被当前调用线程锁住,则会产生死锁,,也就是说同一个线程中不允许锁两次。
  • unlock():解锁,释放对互斥量的所有权。
  • try_lock():尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞,线程调用该函数会出现下面3种情况:
    (1)如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用unlock释放互斥量。
    (2)如果当前互斥量被其他线程锁住,则当前调用线程返回false,而并不会被阻塞掉。
    (3)如果当前互斥量被当前调用线程锁住,则会产生死锁。
int counter = 0;
std::mutex mtx;

void func() {
    for (int i = 0; i < 10000; ++i)
    {
        mtx.lock();
        ++counter;
        mtx.unlock();
    }
}

int main()
{
    std::thread threads[10];

    for (int i = 0; i < 10; ++i)
    {
        threads[i] = std::thread(func);
    }

    for (auto& th : threads)
    {
        th.join();
    }
    std::cout << counter << std::endl;   // 100000

    return 0;
}

以上代码实现了对counter的同步操作,同时只有一个线程能对counter操作。我们再看一下try_lock的结果:

void func() {
    for (int i = 0; i < 10000; ++i)
    {
        if (mtx.try_lock())
        {
            ++counter;
            mtx.unlock();
        }
    }
}

int main()
{
    std::thread threads[10];

    for (int i = 0; i < 10; ++i)
    {
        threads[i] = std::thread(func);
    }

    for (auto& th : threads)
    {
        th.join();
    }
    std::cout << counter << std::endl;   // 不固定

    return 0;
}

可以看到,使用try_lock对counter进行控制,最终结果是不固定的。因为try_lock在其他线程占有互斥量的情况下会返回false但是不阻塞,所以会跳过一部分的++counter操作。

std::recursive_mutex

std::recursive_mutex与std::mutex类似,但是它能够进行多次lock,这样能够规避一些死锁问题:

int counter = 0;
std::recursive_mutex mtx;

void func2() {
    mtx.lock();
    counter++;
    mtx.unlock();
}

void func1() {
    mtx.lock();
    func2();
    counter++;
    mtx.unlock();
}

int main()
{
    std::thread t(func1);

    t.join();
    std::cout << counter << std::endl;   // 2

    return 0;
}

如上面的代码所示,有时候会在两个函数中分别对数据进行lock,如果在一个函数中又调用了另一个函数,此时如果使用std::mutex将会死锁,而用std::recursive_mutex则不会。看起来std::recursive_mutex很不错,但是使用的时候也需要多注意,lock和unlock的数量必须相等,否则会出错。

另外还有性能的问题,std::recursive_mutex的性能会比较差一些,从下面的例子中可以看出来,性能上要差了1倍左右。

int counter = 0;
std::recursive_mutex rmtx;
std::mutex mtx;


int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 10000; ++i)
    {
        mtx.lock();
        counter++;
        mtx.unlock();
    }
    auto end = std::chrono::system_clock::now();
    std::cout << (end - begin).count() << std::endl;  // 330

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 10000; ++i)
    {
        rmtx.lock();
        counter++;
        rmtx.unlock();
    }
    end = std::chrono::system_clock::now();         
    std::cout << (end - begin).count() << std::endl; // 597

    return 0;
}

std::time_mutex和std::recursive_timed_mutex

这两种互斥量类型和不带time的相比,多了两个成员函数:

  • try_lock_for():函数参数表示一个时间范围,在这一段时间范围之内线程如果没有获得锁则保持阻塞;如果在此期间其他线程释放了锁,则该线程可获得该互斥锁;如果超时(指定时间范围内没有获得锁),则函数调用返回false。
  • try_lock_until():函数参数表示一个时刻,在这一时刻之前线程如果没有获得锁则保持阻塞;如果在此时刻前其他线程释放了锁,则该线程可获得该互斥锁;如果超过指定时刻没有获得锁,则函数调用返回false。

首先来看看try_lock_for的用法,下面的例子可以看出,try_lock_for等待指定时间后没有获取到锁,会返回false。

#include        // std::cout
#include          // std::chrono::milliseconds
#include          // std::thread
#include           // std::timed_mutex

std::timed_mutex mtx;

void fireworks(int n) {
    // 为这个锁等待200ms
    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
        std::string out = std::to_string(n);
        out += "wait\n";
        std::cout << out;
    }

    // 获取锁后等待700ms再解锁
    std::this_thread::sleep_for(std::chrono::milliseconds(700));
    std::cout << "end" << std::endl;
    mtx.unlock();
}

int main ()
{
    std::thread threads[3];
    for (int i = 0; i < 3; ++i)
    {
       threads[i] = std::thread(fireworks, i);
       // 为了保证线程按照顺序开始,保证输出一致
       std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }

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

    return 0;
}

// 输出结果
1wait
2wait
1wait
2wait
1wait
2wait
end
2wait
2wait
2wait
end
end

try_lock_until的作用类似,不同的是传入参数是时间点:

void fireworks(int n) {
    auto now  = std::chrono::steady_clock::now();
    while (!mtx.try_lock_until(now +  std::chrono::milliseconds(200))) {
        std::string out = std::to_string(n);
        out += "wait\n";
        std::cout << out;
        now  = std::chrono::steady_clock::now();
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(700));
    std::cout << "end" << std::endl;
    mtx.unlock();
}

std::recursive_timed_mutex和std::recursive_mutex的区别也是一样的,多了两个成员函数。

std::lock_guard和std::unique_lock

std::lock_guard是一个模板类,模板类型可以是以上的四种锁,用于自动锁定解锁,直到对象作用域结束。

std::mutex mtx;

int main ()
{
    {
        std::lock_guard::mutex> guard(mtx);
        std::cout << mtx.try_lock() << std::endl;  // 0,互斥量被占用
    }

    std::cout << mtx.try_lock() << std::endl;     // 0,互斥量释放 
    mtx.unlock();

    return 0;
}

std::unique_lock也支持std::lock_guard的功能,但是区别在于它提供跟多的成员函数使用更加灵活,并且能够和condition_variable一起使用来控制线程同步,内容较多以后有机会在做介绍,有兴趣的朋友可以自己了解。

std::try_lock、std::lock、std::call_once

std::try_lock支持尝试对多个互斥量进行锁定,尝试锁定成功返回-1,否则返回锁定失败的互斥量的位置,例如第一个锁定失败返回0、第二个失败返回1。

int main ()
{
    std::mutex mtx1;
    std::mutex mtx2;

    if (-1 == std::try_lock(mtx1, mtx2))
    {
        std::cout << "locked" << std::endl;
        mtx1.unlock();
        mtx2.unlock();
    }

    return 0;
}

std::lock支持对多个锁锁定,并且避免死锁的出现,以下代码运行时有可能出现死锁的情况:

void func(std::mutex* mtx1, std::mutex* mtx2, int index)
{
    std::lock_guard::mutex> lock1(std::adopt_lock);
    std::lock_guard::mutex> lock2(std::adopt_lock);

    std::cout << index << "out\n";
}

int main ()
{
    std::mutex mtx1;
    std::mutex mtx2;

    // 两个线程的互斥量锁定顺序不同,可能造成死锁
    std::thread t1(func, &mtx1, &mtx2, 1);
    std::thread t2(func, &mtx2, &mtx1, 2);

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

    return 0;
}

而用std::lock能避免多个锁出现死锁:

void func(std::mutex* mtx1, std::mutex* mtx2, int index)
{
    std::lock(*mtx1, *mtx2); // 同时锁定

    // std::adopt_lock作用是声明互斥量已在本线程锁定,std::lock_guard只是保证互斥量在作用域结束时被释放
    std::lock_guard<std::mutex> lock1(*mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(*mtx2, std::adopt_lock);

    // 等价方法:
    //std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
    //std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
    //std::lock(lock1, lock2);

    std::cout << index << "out\n";
}

int main ()
{
    std::mutex mtx1;
    std::mutex mtx2;

    std::thread t1(func, &mtx1, &mtx2, 1);
    std::thread t2(func, &mtx2, &mtx1, 2);

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

    return 0;
}

std::call_once的作用是即使在多线程的情况下,也只执行一次指定的可调用对象(可以是函数、成员函数、函数对象、lambda函数),需要通过配合std::once_flag实现。具体的细节如下:

  • 若在调用 call_once 的时刻, flag 指示已经调用了f指定的可调用对象,则 call_once 立即返回,就是说不再执行可调用对象。
  • 否则,call_once 会调用指定的可调用对象。若该调用对象抛异常,则传播异常给 call_once 的调用方,并且不翻转 flag ,让下一次调用仍然执行。若该调用正常返回,则翻转 flag ,并保证同一 flag不在执行可调用对象。
std::once_flag flag1, flag2;

void simple_do_once()
{
    std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}

void may_throw_function(bool do_throw)
{
    if (do_throw) {
        std::cout << "throw: call_once will retry\n"; // this may appear more than once
        throw std::exception();
    }
    std::cout << "Didn't throw, call_once will not attempt again\n"; // guaranteed once
}

void do_once(bool do_throw)
{
    try {
        std::call_once(flag2, may_throw_function, do_throw);
    }
    catch (...) {
    }
}

int main()
{
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    std::thread st3(simple_do_once);
    std::thread st4(simple_do_once);
    st1.join();
    st2.join();
    st3.join();
    st4.join();

    std::thread t1(do_once, true);
    std::thread t2(do_once, true);
    std::thread t3(do_once, false);
    std::thread t4(do_once, true);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

// 输出结果:
Simple example: called once
throw: call_once will retry
throw: call_once will retry
Didn't throw, call_once will not attempt again

下一篇:《深入应用C++11》笔记-异步线程操作std::async、std::future、std::promise、std::packaged_task

你可能感兴趣的:(C-C++,深入理解/应用C++11)