【C++】锁

news/2025/2/22 10:32:50/

目录

  • 一、简介
  • 二、互斥锁
  • 三、条件锁 和 (unique_lock && lock_guard)
  • 四、递归锁
  • 五、读写锁
  • 六、自旋锁

一、简介

  线程之间的锁有:互斥锁、条件锁、递归锁、读写锁、自旋锁
  其中自旋锁比较特殊,它是一种busy-waiting锁。如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2不但得不到这个自旋锁,而且与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。
  除自旋锁外,其他的锁都是sleep-waiting锁。即假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。

二、互斥锁

互斥量(std::mutex)的成员函数:

  1. 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
  2. lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
    • 如果该互斥量当前没 有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
    • 如果当 前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
    • 如果当前互斥量被当前调用线程锁 住,则会产生死锁(deadlock)。
  3. unlock(), 解锁,释放对互斥量的所有权。
  4. try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该 函数也会出现下面 3 种情况:
    • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直 到该线程调用 unlock 释放互斥量。
    • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
    • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
#include <mutex>
#include <iostream>
#include <thread>#define TRY_MUTEX       0
#define MY_MUTEX        1volatile int counter(0); // non-atomic counterstd::mutex mtx;void increases10k()
{for(int i=0;i<10000;i++){
#if TRY_MUTEXif(mtx.try_lock()){++counter;mtx.unlock();}
#elif MY_MUTEXmtx.lock();++counter;mtx.unlock();
#endif}}int main(int argc,char **argv)
{std::thread threads[10];for(int i=0;i<10;i++){threads[i]=std::thread(increases10k);}for(auto& th:threads)th.join();std::cout << " successful increases of the counter " << counter <<std::endl;return 0;}

参考链接:C++11互斥量mutex使用详解

三、条件锁 和 (unique_lock && lock_guard)

  条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。

头文件:< condition_variable >
类型:std::condition_variable(只和std::mutex一起工作) 和 std::condition_variable_any
(符合类似互斥元的最低标准的任何东西一起工作)。

  相对于手动lock和unlock,我们还可以使用RAII(通过类的构造析构)来实现更好的编码方式。 RAII:也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证 在任何情况下,使用对象时先构造对象,最后析构对象。

#include <mutex>
#include <unistd.h>
#include <deque>
#include <condition_variable>std::mutex mutex;
std::condition_variable cond;
std::deque<int> q;
int count = 0;void producter(){while(true){std::unique_lock<std::mutex> locker(mutex);q.push_back(++count);std::cout << "producter:" << count << std::endl;locker.unlock(); //手动解锁cond.notify_one();sleep(1);}
}void consumer(){while(true){std::unique_lock<std::mutex> locker(mutex);cond.wait(locker, [](){return !q.empty();});auto data = q.back();q.pop_back();std::cout << "consumer:" << data << std::endl;}
}int main(void)
{std::thread t1(producter);std::thread t2(consumer);t1.join();t2.join();std::cout << "main finish\n";return 0;
}

在此段代码中条件变量的目的是为了,在没有获得某种提醒时长时间休眠; 如果正常情况下, 我们需要一直循环 (+sleep), 这样的问题就是CPU消耗+时延问题(忙等待),条件变量的意思是在cond.wait这里一直休眠直到 cond.notify_one唤醒才开始执行下一句; 还有cond.notify_all()接口用于唤醒所有等待的线程。
那么这里为什么必须使用unique_lock呢? 原因是条件变量在wait时会进行unlock再进入休眠, lock_guard并无该操作接口。

  1. wait: 如果线程被唤醒或者超时那么会先进行lock获取锁, 再判断条件(传入的参数)是否成立, 如果成立则 wait函数返回否则释放锁继续休眠。
  2. notify: 进行notify动作并不需要获取锁。
  3. 使用场景:需要结合notify+wait的场景使用unique_lock; 如果只是单纯的互斥使用lock_guard。

unique_lock && lock_guard 的异同:
4. unique_lock与lock_guard都能实现自动加锁和解锁
5. unique_lock更加灵活,可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,lck.lock()进行上锁,而不必等到析构时自动解锁,而lock_guard是不支持手动释放的。在notify+wait结合的场景使用unique_lock;
6. lock_guard效率更高,单纯的互斥场景下使用lock_guard。

四、递归锁

递归锁允许同一个线程多次获取该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。

#include <iostream>
#include <thread>
#include <mutex>
struct Complex
{std::recursive_mutex mutex;int i;Complex() : i(0){}void mul(int x){std::lock_guard<std::recursive_mutex> lock(mutex);i *= x;}void div(int x){std::lock_guard<std::recursive_mutex> lock(mutex);i /= x;}void both(int x, int y){std::lock_guard<std::recursive_mutex> lock(mutex);mul(x);div(y);}
};
int main(void)
{Complex complex;complex.both(32, 23); //因为同一线程可以多次获取同一互斥量,不会发生死锁std::cout << "main finish\n";return 0;
}

五、读写锁

说到读写锁我们可以借助于“读者-写者”问题进行理解。首先我们简单说下“读者-写者”问题。

计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。这就是一个简单的读者-写者模型。

头文件:boost/thread/shared_mutex.cpp
类型:boost::shared_lock

用法:你可以使用boost::shared_ mutex的实例来实现同步,而不是使用std::mutex的实例。对于更新操作,std::lock_guard< boost::shared _mutex>和 std::unique _lock< boost::shared _mutex>可用于锁定,以取代相应的std::mutex特化。这确保了独占访问,就像std::mutex那样。那些不需要更新数据结构的线程能够转而使用 boost::shared _lock< boost::shared _mutex>来获得共享访问。这与std::unique _lock用起来正是相同的,除了多个线程在同一时间,同一boost::shared _mutex上可能会具有共享锁。
  唯一的限制是,如果任意一个线程拥有一个共享锁,试图获取独占锁的线程会被阻塞,直到其他线程全都撤回它们的锁。同样的,如果一个线程具有独占锁,其他线程都不能获取共享锁或独占锁,直到第一个线程撤回它的锁。

#include <iostream>
#include <mutex>    //unique_lock
#include <shared_mutex> //shared_mutex shared_lock
#include <thread>std::mutex mtx;class ThreadSaferCounter
{
private:mutable std::shared_mutex mutex_;unsigned int value_ = 0;
public:ThreadSaferCounter(/* args */) {};~ThreadSaferCounter() {};unsigned int get() const {//读者, 获取共享锁, 使用shared_lockstd::shared_lock<std::shared_mutex> lck(mutex_);//执行mutex_.lock_shared();return value_;  //lck 析构, 执行mutex_.unlock_shared();}unsigned int increment() {//写者, 获取独占锁, 使用unique_lockstd::unique_lock<std::shared_mutex> lck(mutex_);//执行mutex_.lock();value_++;   //lck 析构, 执行mutex_.unlock();return value_;}void reset() {//写者, 获取独占锁, 使用unique_lockstd::unique_lock<std::shared_mutex> lck(mutex_);//执行mutex_.lock();value_ = 0;   //lck 析构, 执行mutex_.unlock();}
};
ThreadSaferCounter counter;
void reader(int id){while (true){std::this_thread::sleep_for(std::chrono::seconds(1));std::unique_lock<std::mutex> ulck(mtx);//cout也需要锁去保护, 否则输出乱序std::cout << "reader #" << id << " get value " << counter.get() << "\n";}    
}void writer(int id){while (true){std::this_thread::sleep_for(std::chrono::seconds(1));std::unique_lock<std::mutex> ulck(mtx);//cout也需要锁去保护, 否则输出乱序std::cout << "writer #" << id << " write value " << counter.increment() << "\n";}
}int main()
{std::thread rth[10];std::thread wth[10];for(int i=0; i<10; i++){rth[i] = std::thread(reader, i+1);}for(int i=0; i<10; i++){wth[i] = std::thread(writer, i+1);}for(int i=0; i<10; i++){rth[i].join();}for(int i=0; i<10; i++){wth[i].join();}return 0;
}

六、自旋锁

原子变量

参考链接:C++原子变量atomic详解

#include<iostream>
#include<atomic>
#include<thread>
using namespace std;class CAS
{
private:std::atomic<bool> flag;
public:CAS():flag(false) {}~CAS() {}CAS(const CAS& s) = delete;CAS& operator=(const CAS&) = delete;void lock(){bool expect = false;while (!flag.compare_exchange_strong(expect, true)){expect = false;}}void unlock(){flag.store(false);}};int num = 0;
CAS sm;void fun()
{for (int i = 0;i < 1000;i++){sm.lock();cout << "num:" << ++num << endl;sm.unlock();}
}int main()
{thread t1(fun);thread t2(fun);t1.join();t2.join();return 0;
}

参考链接:

  1. c++之理解自旋锁
  2. C++11线程中的几种锁
  3. c++之多线程中“锁”的基本用法
  4. C++11 锁机制简单对比 - atomic, mutex, spinlock
  5. C++原子变量atomic详解

http://www.ppmy.cn/news/88101.html

相关文章

基于欧式距离的聚类算法的Kmeans作业

访问【WRITE-BUG数字空间】_[内附完整源码和文档] 基于欧式距离的聚类算法&#xff0c;其认为两个目标的距离越近&#xff0c;相似度越大。 该实验产生的点为二维空间中的点。 环境配置 java环境&#xff0c;使用原生的Java UI组件JPanel和JFrame 算法原理 基于欧式距离的聚…

《汇编语言》- 读书笔记 - 第3章-寄存器(内存访问)包含:栈

《汇编语言》- 读书笔记 - 第3章-寄存器&#xff08;内存访问&#xff09; 3.1 内存中字的存储问题 3.1 3.2 DS 和 [address]问题 3.2 3.3 字的传送问题 3.3问题 3.4 3.4 mov、add、sub 指令3.5 数据段问题 3.53.1~3.5 小结检测点 3.1 3.6 栈3.7 CPU 提供的栈机制问题 3.6 3.8 …

格式化输入

C语言中常用的输入可以有多种方式&#xff0c;如scanf(),getchar(),gets()。它们的功能都类似&#xff08;获取键盘输入的数据&#xff09;&#xff0c;但scanf()因功能使用、函数简单&#xff0c;而深受编程者喜爱。 scanf()函数功能: 执行格式化输入&#xff08;用户可以按照…

阿里巴巴今年计划招聘15000人

据媒体报道&#xff0c;阿里巴巴集团宣布将在2023年招聘15000名新员工&#xff0c;其中包括3000名校招人才。 这一消息的发布&#xff0c;也为最近关于淘宝天猫、阿里云、菜鸟、本地生活等业务裁员的谣言敲响了谣言的警钟。 阿里巴巴官微表示&#xff0c;“近日&#xff0c;关于…

安装 Jenkins 2.346.3 LTS

环境&#xff1a; apache-tomcat-8.5.88 Jenkins 2.346.3 LTS Running Jenkins system Jenkins requires Java 11 or 17 since Jenkins 2.357 and LTS 2.361.1. Read more about this in the announcement blog post. https://www.jenkins.io/doc/administration/requirement…

【CSAPP】虚拟内存 | 地址空间 | 页表内存保护 | 页错误引发异常逐出 (evicted)

&#x1f4ad; 写在前面&#xff1a;本文将学习《深入理解计算机系统》虚拟内存部分&#xff0c;CSAPP 是计算机科学经典教材《Computer Systems: A Programmers Perspective》的缩写&#xff0c;该教材由Randal E. Bryant和David R. OHallaron 合著。 &#x1f4dc; 本章目录…

【毕业季征文】你们的未来可期

还有大约一个月即将又有一批大学的学弟学妹们离开学校,走向社会,走上工作岗位。你们是否是既兴奋又迷茫呢?,我相信更多的是憧憬吧。回想我大学毕业那是10年前的事情了,现在的感受是那么遥远,仿佛自己从来都没上过大学。可能是在大学期间过的太过艰苦,以至于不想保留那一…

【自然语言处理】【ChatGPT系列】大模型的涌现能力

大语言模型的涌现能力 《Emergent Abilities of Large Language Models》 论文地址&#xff1a;https://arxiv.org/pdf/2206.07682.pdf 相关博客 【自然语言处理】【ChatGPT系列】WebGPT&#xff1a;基于人类反馈的浏览器辅助问答 【自然语言处理】【ChatGPT系列】ChatGPT的智能…