c++之多线程中“锁”(mutex)的用法

1. 锁:mutex

锁,是生活中应用十分广泛的一种工具。锁的本质属性是为事物提供“访问保护”,例如:大门上的锁,是为了保护房子免于不速之客的到访;自行车的锁,是为了保护自行车只有owner才可以使用;保险柜上的锁,是为了保护里面的合同和金钱等重要东西……

在c++等高级编程语言中,锁也是用来提供“访问保护”的,不过被保护的东西不再是房子、自行车、金钱,而是内存中的各种变量。此外,计算机领域对于“锁”有个响亮的名字——mutex(互斥量),学过操作系统的同学对这个名字肯定很熟悉。

Mutex,互斥量,就是互斥访问的量。这种东东只在多线程编程中起作用,在单线程程序中是没有什么用处的。从c++11开始,c++提供了std::mutex类型,对于多线程的加锁操作提供了很好的支持。下面看一个简单的例子,对于mutex形成一个直观的认识。

Demo1——无锁的情况

假定有一个全局变量counter,启动两个线程,每个都对该变量自增10000次,最后输出该变量的值。在第一个demo中,我们不加锁,代码文件保存为:mutex_demo1_no_mutex.cpp

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

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;
}

为了显示多线程竞争导致结果不正确的现象,在每次自增操作的时候都让当前线程休眠1毫秒

如果没有多线程编程的相关经验,我们可能想当然的认为最后的counter为20000,如果这样想的话,那就大错特错了。下面是两次实际运行的结果:

[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19997
[root@2d129aac5cc5 demo]# ./mutex_demo1_no_mutex
counter:19996

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

轮到mutex上场。

Demo2——加锁的情况

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

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

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;
}

上述代码保存文件为:mutex_demo2_with_mutex.cpp。先来看几次运行结果:

[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对象,任意时刻最多允许一个线程对其进行上锁
    1. mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生
    1. mtx.unlock():释放锁
    1. std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。

2. lock_guard

虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的 :

Demo3——死锁的情况(仅仅为了演示,不要这么写代码哦)

为了捕捉抛出的异常,我们重新组织一下代码,代码保存为:mutex_demo3_dead_lock.cpp。

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

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;
}

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

[root@2d129aac5cc5 demo]# ./mutex_demo3_dead_lock
id:1, throw excption....

程序并没有退出,而是永远的“卡”在那里了,也就是发生了死锁。

那么这种情况该怎么避免呢? 这个时候就需要std::lock_guard登场了。std::lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数(这就是所谓的RAII,读者可自行搜索)。我们修改一下demo3。

Demo4——避免死锁,lock_guard

demo4保存为:mutex_demo4_lock_guard.cpp

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

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;
}

执行上述代码,结果为:

[root@2d129aac5cc5 demo]# ./mutex_demo4_lock_guard
id:1, throw excption....
counter:10000

结果符合预期。所以,推荐使用std::mutex和std::lock_guard搭配使用,避免死锁的发生

3. std::lock_guard的第二个构造函数

实际上,std::lock_guard有两个构造函数,具体的

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)

在demo4中我们使用了第1个构造函数,第3个为拷贝构造函数,定义为删除函数。这里我们来重点说一下第2个构造函数。

第2个构造函数有两个参数,其中第二个参数类型为:std::adopt_lock_t。这个构造函数假定:当前线程已经上锁成功,所以不再调用lock()函数。
参考两个参数

  • 36
    点赞
  • 128
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《Linux多线程服务端编程:使用muduo C++网络库》主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread。 目 录 第1部分C++ 多线程系统编程 第1章线程安全的对象生命期管理3 1.1当析构函数遇到多线程. . . . . . . . . . . . . . . . .. . . . . . . . . . . 3 1.1.1线程安全的定义. . . . . . . . . . . . . . . . .. . . . . . . . . . . 4 1.1.2MutexLock 与MutexLockGuard. . . . . . . . . . . . . . . . . . . . 4 1.1.3一个线程安全的Counter 示例.. . . . . . . . . . . . . . . . . . . 4 1.2对象的创建很简单. . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 5 1.3销毁太难. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . 7 1.3.1mutex 不是办法. . . . . . . . . . . . . . . . . . . .. . . . . . . . 7 1.3.2作为数据成员的mutex 不能保护析构.. . . . . . . . . . . . . . 8 1.4线程安全的Observer 有多难.. . . . . . . . . . . . . . . . . . . . . . . . 8 1.5原始指针有何不妥. . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 11 1.6神器shared_ptr/weak_ptr . . . . . . . . . .. . . . . . . . . . . . . . . . 13 1.7插曲:系统地避免各种指针错误. . . . . . . . . . . . . . . . .. . . . . . 14 1.8应用到Observer 上.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 1.9再论shared_ptr 的线程安全.. . . . . . . . . . . . . . . . . . . . . . . . 17 1.10shared_ptr 技术与陷阱. . . .. . . . . . . . . . . . . . . . . . . . . . . . 19 1.11对象池. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . 21 1.11.1enable_shared_from_this . . . . . . . . . . . . . . . . . . . . . . 23 1.11.2弱回调. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 24 1.12替代方案. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . 26 1.13心得与小结. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . 26 1.14Observer 之谬. . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 第2章线程同步精要 2.1互斥器(mutex). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 2.1.1只使用非递归的mutex . . . . . . . . . . . . . .. . . . . . . . . . 33 2.1.2死. . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 35 2.2条件变量(condition variable). . . . . . . . . .
互斥mutex lock)是一种常用的线程同步机制,用于保护共享资源的访问。在多线程环境下,当多个线程同时访问某个共享资源时,互斥可以确保同一时间只有一个线程可以访问该资源,避免数据竞争和不一致性的问题。 互斥有两种状态:定(locked)和未定(unlocked)。当线程想要访问共享资源时,它首先尝试获取互斥。如果状态是未定,线程可以获取,并将状态设置为定;如果状态是定,线程就会被阻塞,直到被释放。一旦线程完成对共享资源的访问,它会释放互斥,使得其他线程可以获取并访问资源。 以下是使用C/C++语言pthread库提供的互斥的示例代码: ```c #include <pthread.h> // 定义互斥 pthread_mutex_t mutex; // 共享资源 int shared_resource = 0; void* thread_function(void* arg) { // 获取互斥 pthread_mutex_lock(&mutex); // 访问共享资源 shared_resource++; // 释放互斥 pthread_mutex_unlock(&mutex); return NULL; } int main() { // 初始化互斥 pthread_mutex_init(&mutex, NULL); // 创建多个线程 pthread_t thread1, thread2; pthread_create(&thread1, NULL, thread_function, NULL); pthread_create(&thread2, NULL, thread_function, NULL); // 等待线程结束 pthread_join(thread1, NULL); pthread_join(thread2, NULL); // 销毁互斥 pthread_mutex_destroy(&mutex); return 0; } ``` 在上述示例,两个线程同时访问 `shared_resource` 变量,并使用互斥确保了对该变量的原子操作,避免了竞态条件的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值