c++ mutex

在c++等高级编程语言中,锁也是用来提供“访问保护”的,Mutex,互斥量,就是互斥访问的量。只在多线程编程中起作用。从c++11开始,c++提供了std::mutex类型,对于多线程的加锁操作提供了很好的支持。

无锁的情况

假定有一个全局变量counter,启动两个线程,每个都对该变量自增10000次,最后输出该变量的值。

#include 
#include 
#include 
#include 
#include 
#include 

int counter = 0;
void increase(int time) {
    for (int i = 0; i < time; i++) {
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000);
    std::thread t2(increase, 10000);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

下面是两次实际运行的结果:

counter:19997
counter:19996

出现上述情况的原因是:自增操作"counter++"不是原子操作,而是由多条汇编指令完成的。多个线程对同一个变量进行读写操作就会出现不可预期的操作。假定counter当前值为10,线程1读取到了10,线程2也读取到了10,分别执行自增操作,线程1和线程2分别将自增的结果写回counter,不管写入的顺序如何,counter都会是11,但是线程1和线程2分别执行了一次自增操作,我们期望的结果是12!!!!!

加锁的情况

定义一个std::mutex对象用于保护counter变量。对于任意一个线程,如果想访问counter,首先要进行"加锁"操作,如果加锁成功,则进行counter的读写,读写操作完成后释放锁; 如果“加锁”不成功,则线程阻塞,直到加锁成功。

#include 
#include 
#include 
#include 
#include 
#include 

int counter = 0;
std::mutex mtx; // 保护counter

void increase(int time) {
    for (int i = 0; i < time; i++) {
        mtx.lock();
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
        mtx.unlock();
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000);
    std::thread t2(increase, 10000);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

几次运行结果:

[root@2d129aac5cc5 demo]# ./mutex_demo2_with_mutex
counter:20000
[root@2d129aac5cc5 demo]# ./mutex_demo2_with_mutex
counter:20000
[root@2d129aac5cc5 demo]# ./mutex_demo2_with_mutex
counter:20000

这次运行结果和我们预想的一致,原因就是“利用锁来保护共享变量”,在这里共享变量就是counter(多个线程都能对其进行访问,所以就是共享变量啦)。

简单总结一些std::mutex:

  1. 对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁
  2. mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生
  3. mtx.unlock():释放锁
  4. std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。
  5. lock_guard
    虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的 :

死锁的情况

#include 
#include 
#include 
#include 
#include 
#include 

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        mtx.lock();
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
        mtx.unlock();
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

执行后,结果如下图所示:

id:1, throw excption....

程序并没有退出,而是永远的“卡”在那里了。

那么这种情况该怎么避免呢? 这个时候就需要std::lock_guard。std::lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数(这就是所谓的RAII)。

避免死锁,lock_guard

#include 
#include 
#include 
#include 
#include 
#include 

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        // std::lock_guard对象构造时,自动调用mtx.lock()进行上锁
        // std::lock_guard对象析构时,自动调用mtx.unlock()释放锁
        std::lock_guard<std::mutex> lk(mtx);
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

执行上述代码,结果为:

id:1, throw excption....
counter:10000

结果符合预期。所以,推荐使用std::mutex和std::lock_guard搭配使用,避免死锁的发生。std::lock_guard对象构造时,自动调用mtx.lock()进行上锁,写下这个语句的时候调用std::lock_guardstd::mutex lk(mtx);,那么调用析构函数应该是大括号{}结束的时候,也就是说定义lock_guard的时候调用构造函数加锁,大括号解锁的时候调用析构函数解锁。

实际上,std::lock_guard有两个构造函数,具体的(参考:cppreference):

explicit lock_guard( mutex_type& m );                   (1)      (since C++11)
lock_guard( mutex_type& m, std::adopt_lock_t t ); 	(2)      (since C++11)
lock_guard( const lock_guard& ) = delete;               (3)      (since C++11)

unique_lock

虽然lock_guard挺好用的,但是有个很大的缺陷,在定义lock_guard的地方会调用构造函数加锁,在离开定义域的话lock_guard就会被销毁,调用析构函数解锁。这就产生了一个问题,如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。
为了解决lock_guard锁的粒度过大的原因,可以使用unique_lock

unique_lock<mutex> unique(mtx);

这个会在构造函数加锁,然后可以利用unique.unlock()来解锁,所以当觉得锁的粒度太多的时候,可以利用这个来解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候一定会解锁,也没有中途解锁的功能。

但是unique_lock内部会维护一个锁的状态,所以在效率上肯定会比lock_guard慢。

你可能感兴趣的:(c++,c++,开发语言)