当前位置: 首页 > news >正文

深入了解C++11第一讲 -- thread和mutex

深入了解C++11第一讲 -- thread和mutex

  • 1.thread
  • 2.this_thread
    • 2.1 std::chrono
      • 2.1.1 Clocks
    • 2.2 sleep_for && sleep_until
    • 2.3 std::this_thread::yield
  • 3. std::mutex
    • 3.1 回顾不加锁,导致的错误
    • 3.1 std::ref
    • 3.2 std::mutex::try_lock
  • 4. std::lock_guard
  • 5. std::unique_lock
    • 5.1 自动加锁和解锁
    • 5.2 延迟加锁(手动控制加锁时机)
    • 5.3 尝试加锁与超时加锁
  • 6.std::lock
    • 6.1 多锁获取顺序与死锁
    • 6.2 std::lock的解决方案
    • 6.3 std::try_lock

1.thread

我们之前在Linux课程中,已经学习过线程,所以这里我们讲的,只会讲解C++提供的线程接口的使用,如果要了解线程,先看这个博客: 线程理论知识

#include <iostream>
#include <thread> // 线程使用,头文件
#include <unistd.h> // sleepvoid ThreadFunc(int id)
{for(int i = 0; i<10; i++){std::cout << "线程入口函数调用: " << id << std::endl;sleep(1);}
}int main()
{// 1. 线程的创建,传入的参数有:1.线程的入口函数  2.函数所需要的参数// 1.1 注意: thread的构造函数会复制参数,对于类,调用拷贝构造std::thread t1(ThreadFunc, 1);std::thread t2(ThreadFunc, 2);// 2. 可以将线程设置为分离状态,不需要手动join了t2.detach();int x = 3;// 3. 线程的入口函数,可以是lambda表达式std::thread t3([&x](){ThreadFunc(x);       });// 4. 线程结束了之后,一定要进行joint1.join();t3.join();return 0;
}

更多的使用,比如查看线程ID,可以查找CPlusPlus官网
对于线程,支持移动构造,但是不支持拷贝构造,所以我们可以这样使用:

int main()
{std::vector<std::thread> threads(2);// thread不支持拷贝构造,但是支持移动构造for(int i = 0; i<2; i++) threads[i] = std::thread(ThreadFunc);for(int i = 0; i<2; i++) threads[i].join();return 0;
}

2.this_thread

this_thread是头文件中的命名空间,封装了线程的四个操作接口,不需要创建thread对象就可以直接使用:
在这里插入图片描述

void ThreadFunc()
{for(int i = 0; i<10; i++){// 1. get_id的使用,返回的id为一个类对象// id可以进行输出,可以进行比较,因为已经进行重载std::thread::id id = std::this_thread::get_id();std::cout << "我的线程ID为:" << id << std::endl;sleep(1);}
}int main()
{std::thread t1(ThreadFunc);t1.join();return 0;
}

2.1 std::chrono

std::chrono是C++11引入的标准时间库,位于头文件中
提供了多种时间的类型:
在这里插入图片描述
支持加减、比较、缩放:

int main()
{std::chrono::seconds second(1); // 1秒std::chrono::milliseconds millsecond(500); // 500ms// 1. 加减操作auto total = second + millsecond;std::cout << total.count() << std::endl; // 如果要cout,需要先转换// 2. 比较操作if(second > millsecond) std::cout << "1s > 500ms" << std::endl;// 3. 缩放操作auto ret = second * 2;std::cout << ret.count() << std::endl;return 0;
}

2.1.1 Clocks

标准库中,提供了三种常用时钟:

  1. std::chrono::system_clock
    系统时钟,与系统时间相关联,需要与ctime同时使用
int main()
{// 1. system_clock,获取当前系统时间点std::chrono::system_clock::time_point ntime = std::chrono::system_clock::now();// 2. 转化为时间戳time_t timestamp = std::chrono::system_clock::to_time_t(ntime);// 3. 转化为年月日进行输出 -- 当前系统时间: Mon Oct 27 19:46:10 2025std::cout << "当前系统时间: " << std::ctime(&timestamp);return 0;
}
  1. std::chrono::steady_clock
    稳定时钟,单调递增,不受系统时间影响,可以用于程序运行耗时
int main()
{// 1. 记录开始的时间auto start = std::chrono::steady_clock::now();// 2. 模拟程序运行,休眠1sstd::this_thread::sleep_for(std::chrono::seconds(1));// 3. 记录结束时间auto end = std::chrono::steady_clock::now();// 4. 记录耗时auto time = end - start;std::cout << time.count() << std::endl;return 0;
}
  1. std::chrono::high_resolution_clock
    高精度时钟,在对精度要求比较高的情景下使用

2.2 sleep_for && sleep_until

  1. sleep_for – 让线程休眠指定的时长(基于std::chrono的时间单位)
  2. sleep_until – 让线程休眠,直到某个指定的时间点(std::chrono::time_point)
// 1. sleep_for的测试
void ThreadSleepForTest()
{std::cout << "sleep_for -- 线程开始休眠\n";// 1. 直接选择休眠的时长,就休眠多长时间std::this_thread::sleep_for(std::chrono::seconds(5));std::cout << "sleep_for -- 线程休眠5s,休眠结束\n";
}
// 2. sleep_until的测试
void ThreadSleepUntilTest()
{std::cout << "sleep_until -- 线程开始休眠\n";// 2. 需要传入休眠到多长时间,就休眠到多长时间auto now = std::chrono::system_clock::now();auto wakeuptime = now + std::chrono::seconds(7);std::this_thread::sleep_until(wakeuptime);std::cout << "sleep_until -- 线程休眠7s结束\n";
}
int main()
{std::thread testsleepfor(ThreadSleepForTest);std::thread testsleepuntil(ThreadSleepUntilTest);testsleepfor.join();testsleepuntil.join();return 0;
}

2.3 std::this_thread::yield

线程的运行,不就是一个队列,按照时间片进行轮转吗,yield这个接口的作用是让当前线程主动放弃CPU时间片,允许其它就绪状态的线程优先执行。在一个死循环中,如果不使用yield,可能会造成一个进程疯狂占用CPU,进程饥饿的问题,此时就可以调用yield,建议/允许其它进程优先调度,但是实际调度肯定还是OS说了算

3. std::mutex

3.1 回顾不加锁,导致的错误

// 1. 模拟不加锁的错误场景
void UnLockedError(int& rx, int n)
{for(int i = 0; i<n; i++){rx++;}
}int main()
{int rx = 0;// 1. 线程1将rx+十万次std::thread thread1(UnLockedError, std::ref(rx), 100000);// 2. 线程1将rx+二十万次std::thread thread2(UnLockedError, std::ref(rx), 200000);thread1.join();thread2.join();// 3. 等线程运行结束之后,打印出rx1的实际加的次数std::cout << "rx实际被加的次数为:" << rx << std::endl;return 0;
}

在这里插入图片描述
每一次输出的结果都不一样,我们会觉得只有一个rx++操作呀,但是因为rx++不是原子操作!:
rx在汇编中需要执行的操作有三步:1. 将rx放到寄存器中 2. 寄存器++ 3. 将寄存器中的值再写入到rx中
如果线程1开始时,取出rx=0,放到寄存器中,此时线程的时间片到了,线程2取出rx,还是=0!这里就会少加一次,所以这里要加锁!

// 1. 模拟不加锁的错误场景
void UnLockedError(int& rx, int n, std::mutex& mutex)
{// 2. 加锁和解锁mutex.lock();for(int i = 0; i<n; i++){rx++;}mutex.unlock();
}int main()
{int rx = 0;// 1. 创建锁std::mutex mutex;std::thread thread1(UnLockedError, std::ref(rx), 100000, std::ref(mutex));std::thread thread2(UnLockedError, std::ref(rx), 200000, std::ref(mutex));thread1.join();thread2.join();std::cout << "rx实际被加的次数为:" << rx << std::endl;return 0;
}

3.1 std::ref

上面的场景中,如果不使用std::ref,甚至连编译都编译不过去,我来讲讲为什么:
在这里插入图片描述

3.2 std::mutex::try_lock

尝试加锁,如果得到锁,返回true。失败,返回false。但是不是报错,可以通过if判断,如果没有得到锁,仍然可以执行判断为false的代码

4. std::lock_guard

lock_guard是C++11提供的支持RAII(翻译为资源获取初始化,就是将资源的生命周期与对象的声明周期绑定,就像智能指针,如果构造的对象生命周期结束,会自动释放锁/空间)方式管理的类
lock_guard其实就是这样管理的:

template<class Mutex>
class LockGuard
{
public:LockGuard(Mutex& mtx):_mtx(mtx){_mtx.lock();}~LockGuard(){_mtx.unlock();}
private:Mutex& _mtx;
};

构造是上锁,析构时自动释放锁

// 1. lock_guard的测试
void LockGuardTest(int &rx, int n, std::mutex &mutex)
{// 2. 如果只有一部分需要锁进行管理,可以使用{}// 当lock_guard出了}之后,就会自动释放锁{// 1. 此时就不需要加锁和解锁了,直接使用lock_guard进行管理std::lock_guard<std::mutex> lock(mutex);for (int i = 0; i < n; i++){rx++;}}// 2. 如果只有一部分需要锁进行管理,可以使用{}// 当lock_guard出了}之后,就会自动释放锁for (int i = 0; i < 10; i++){std::cout << "不需要锁管理的资源\n";}
}int main()
{int rx = 0;// 1. 创建锁std::mutex mutex;std::thread thread1(LockGuardTest, std::ref(rx), 100000, std::ref(mutex));std::thread thread2(LockGuardTest, std::ref(rx), 200000, std::ref(mutex));thread1.join();thread2.join();std::cout << "rx实际被加的次数为:" << rx << std::endl;return 0;
}

5. std::unique_lock

std::unique_lock,同样基于RAII思想,但是比lock_guard提供了更多控制能力:

  1. 支持手动加锁和解锁(lock和unlock),可以在作用域更加灵活地控制锁
  2. 支持超时加锁(try_lock_for和try_lock_until),避免无限阻塞
  3. 支持延迟加锁,一开始不加锁,当遇到临界区再lock加锁

5.1 自动加锁和解锁

// 1. unique_lock的测试
void UniqueLockTest(int &rx, int n, std::mutex &mutex)
{{// 1. 构造时自动加锁,析构自动解锁,支持RAII思想,与lock_guard相同std::unique_lock<std::mutex> lock(mutex);for (int i = 0; i < n; i++){rx++;}}for (int i = 0; i < 5; i++){std::cout << "不需要锁管理的资源\n";}
}int main()
{int rx = 0;// 1. 创建锁std::mutex mutex;std::thread thread1(UniqueLockTest, std::ref(rx), 100000, std::ref(mutex));std::thread thread2(UniqueLockTest, std::ref(rx), 200000, std::ref(mutex));thread1.join();thread2.join();std::cout << "rx实际被加的次数为:" << rx << std::endl;return 0;
}

5.2 延迟加锁(手动控制加锁时机)

// 1. unique_lock的测试
void UniqueLockTest(int &rx, int n, std::mutex &mutex)
{// 1. 仅关联锁,构造时不锁,标注defer_lockstd::unique_lock<std::mutex> lock(mutex, std::defer_lock);for (int i = 0; i < 5; i++){std::cout << "不需要锁管理的资源\n";}{// 2. 手动加锁lock.lock();for (int i = 0; i < n; i++){rx++;}// 3. 可提前释放锁,也可等结束生命周期lock.unlock();}
}int main()
{int rx = 0;// 1. 创建锁std::mutex mutex;std::thread thread1(UniqueLockTest, std::ref(rx), 100000, std::ref(mutex));std::thread thread2(UniqueLockTest, std::ref(rx), 200000, std::ref(mutex));thread1.join();thread2.join();std::cout << "rx实际被加的次数为:" << rx << std::endl;return 0;
}

5.3 尝试加锁与超时加锁

在这里插入图片描述
尝试加锁,成功返回true,失败返回false
超时加锁,for和until,和上面说的那个时间一样

6.std::lock

std::lock,用于同时对多个互斥锁进行加锁

6.1 多锁获取顺序与死锁

当线程1申请锁1,然后切换到线程2,线程2获取锁2,此时线程1无法获取锁2,阻塞。线程2无法获取锁1,阻塞。死锁!

// 线程1:先锁 mtx1,再锁 mtx2
void thread1() {mtx1.lock();mtx2.lock();  // 若线程2已锁定 mtx2,线程1会阻塞等待mtx2.unlock();mtx1.unlock();
}// 线程2:先锁 mtx2,再锁 mtx1
void thread2() {mtx2.lock();mtx1.lock();  // 若线程1已锁定 mtx1,线程2会阻塞等待mtx1.unlock();mtx2.unlock();
}

6.2 std::lock的解决方案

std::lock可以同时锁定多个锁:

std::mutex mtx1, mtx2;void safe_operation()
{// 同时锁定 mtx1 和 mtx2(原子操作,无死锁风险)// 如果获取锁1,没有获取锁2,会释放锁1,再次尝试申请两锁std::lock(mtx1, mtx2);// 用 std::adopt_lock 标记锁已锁定,避免重复加锁// 告诉unique_lock/lock_guard,我已经lock了,你只需要unlock就行了std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);// 临界区操作(同时使用两个锁保护的资源)std::cout << "安全操作:已同时获取两个锁" << std::endl;
}int main()
{std::thread t1(safe_operation);std::thread t2(safe_operation);t1.join();t2.join();return 0;
}

6.3 std::try_lock

尝试同时锁定多个锁,返回-1,说明所有锁都获取成功,返回0,第0个锁没有获取成功,返回1,第1个锁没有获取成功
锁的顺序就是传入参数的顺序,传入的第一个参数就是第0个锁
如果锁0没有获取成功,就不管锁1了,直接返回0

std::try_lock(mtx0, mtx1, mtx2);
http://www.dtcms.com/a/537353.html

相关文章:

  • 航电系统动力模块技术解析
  • 数据结构(11)
  • 什么网站好哪里公司建设网站好
  • 通过python脚本判断两个多语言properties的差异,并生成缺失的文件
  • python ThreadPoolExecutor基础
  • 昆山网站建设方案优化公司线下推广的方式有哪些
  • 基于微信公众号开发网站开发上海网络推广培训学校
  • 我的全栈学习之旅:Celery(持续更新!!!)
  • 【Linux】xargs命令
  • CCUT应用OJ题解——贪吃的松鼠
  • [已解决]Python将COCO格式实例分割数据集转换为YOLO格式
  • CSS Backgrounds (背景)
  • Blender入门学习08 - 骨骼绑定
  • 家装设计网站开发企业做网站大概多少钱
  • TCP/UDP端口、IP协议号与路由协议 强行记忆点总结
  • (一)React面试()
  • 配置文件加载顺序与优先级规则
  • 数字化转型迫在眉睫,企业应该如何面对?
  • 做网站百度云网站登录不了
  • HF4054H-B 50V耐压 集成6.1V过压保护和1.3A过流保护 42V热拔插性能的500mA锂电池线性充电芯片
  • 网站可以做音频线吗网站如何安装源码
  • 小学校园文化建设网站网站打不开显示asp
  • 142.DDR报错bank32,33,34
  • Android性能优化深度解析与实际案例
  • 网站素材网站建设的目标和需求
  • 与您探讨电子元器件结构陶瓷(陶瓷基板)的分类及结构陶瓷的应用
  • 模板建站自适应互联网网站分了
  • 苹果ios安卓apk应用APP文件怎么修改手机APP显示的名称
  • 网站界面用什么做的网站创建方法
  • 《自动控制原理》第 3 章 线性控制系统的运动分析:3.6、3.7