✊✊✊🌈大家好!本篇文章是多线程系列第二篇文章😇。首先讲解了利用mutex解决多线程数据共享问题,举例更好理解lock
和unlock
的使用方法,以及错误操作造成的死锁问题,最后讲解了lock_guard
与unique_lock
使用的注意事项。
c++多线程系列目录:
c++多线程(一): 多进程和多线程并发**的区别以及各自优缺点,Thead线程库的基本使用。
对多线程其他内容感兴趣的同学可以点击上方目录链接跳转。
本专栏知识点是通过<零声教育>的音视频流媒体高级开发课程进行系统学习,梳理总结后写下文章,对音视频相关内容感兴趣的读者,可以点击观看课程网址:零声教育
🎡导航小助手🎡
- 一、互斥量(Mutex)
- 1.1 lock和unlock
- 1.2 死锁
- 1.3lock_guard与unique_lock
- 二、小结
一、互斥量(Mutex)
当多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。
为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量、条件变量、原子操作等。
互斥量(mutex)是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题。
1.1 lock和unlock
mutex常用操作:
lock()
:资源上锁unlock()
:解锁资源trylock()
:查看是否上锁,它有下列3种类情况:- (1)未上锁返回
false
,并锁住; - (2)其他线程已经上锁,返回
true
; - (3)同一个线程已经对它上锁,将会产生死锁。
- (1)未上锁返回
死锁:在两个或两个以上的进程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
下面举一个实例:
添加lock()和unlock():
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int shared_data = 0;
mutex mtx;
void func(int n) {
for (int i = 0; i < 10; ++i) {
mtx.lock();//添加lock锁
shared_data++;
cout << "Thread " << n
<< " increment shared_data to " << shared_data <<endl;
mtx.unlock();//解锁
}
}
int main() {
thread t1(func, 1);
thread t2(func, 2);
t1.join();
t2.join();
cout << "Final shared_data = " << shared_data <<endl;
return 0;
}
运行结果:
不添加:
结果就会很乱,因为两个线程都对shared_data
进行操作,发生了数据竞争现象。
补充:什么是线程安全?
如果多线程程序每次的运行结果和单线程运行的结果始终是一样的,那么线程是安全的。
1.2 死锁
假设存在两个线程 T1 和 T2,都要对两个互斥量 mtx1 和 mtx2 进行访问,且按照以下顺序获取互斥量的所有权:
- T1 先获取 mtx1 的所有权,再获取 mtx2 的所有权。
- T2 先获取 mtx2 的所有权,再获取 mtx1 的所有权。
如果两个线程同时执行,就会出现死锁问题。
因为 T1 获取了 mtx1 的所有权,但是无法获取 mtx2 的所有权,而 T2 获取了 mtx2 的所有权,但是无法获取 mtx1 的所有权,两个线程互相等待对方释放互斥量,导致死锁。
为了解决这一问题,就需要两个线程按照相同的顺序获取互斥量的所有权。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void func1(){
mtx2.lock();
std::cout << "Thread 1 locked mutex 2" << std::endl;
mtx1.lock();
std::cout << "Thread 1 locked mutex 1" << std::endl;
mtx1.unlock();
std::cout << "Thread 1 unlocked mutex 1" << std::endl;
mtx2.unlock();
std::cout << "Thread 1 unlocked mutex 2" << std::endl;
}
void func2() {
mtx2.lock();
std::cout << "Thread 2 locked mutex 2" << std::endl;
mtx1.lock();
std::cout << "Thread 2 locked mutex 1" << std::endl;
mtx1.unlock();
std::cout << "Thread 2 unlocked mutex 1" << std::endl;
mtx2.unlock();
std::cout << "Thread 2 unlocked mutex 2" << std::endl;
}
int main(){
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
return 0;
}
运行结果:
1.3lock_guard与unique_lock
lock_guard:
创建lock_guard
对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。
lock_guard
的特点:
- 当构造函数被调用时,该互斥量会被自动锁定。
- 当析构函数被调用时,该互斥量会被自动解锁。
- std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。
代码举例:
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex; // protects g_i,用来保护g_i
void safe_increment() {
const std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
// g_i_mutex自动解锁
}
int main() {
std::cout << "main id: " << std::this_thread::get_id() << std::endl;
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
运行结果:
最开始,主线程id
为17336
,g_i
为0
,每经过一个线程,g_i++
。
unique_lock:
简单地讲,unique_lock
是 lock_guard
的升级加强版,它具有 lock_guard
的所有功能,同时又具有其他很多方法,它可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。
unique_lock的特点:
- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
- 可以随时加锁解锁
- 作用域规则同 lock_grard,析构时自动释放锁
- 不可复制,可移动
- 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
std::unique_lock 提供了以下几个成员函数:
- lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
- try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回
false
,否则返回true
。 - try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
- try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
- unlock():对互斥量进行解锁操作
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex mtx;
void func() {
for (int i = 0; i < 10; i++) {
std::unique_lock<std::mutex> lg(mtx);
//知识点1.构造但不加锁,需要自己加锁
//std::unique_lock<std::mutex> lg(mtx,std::defer_lock);
g_i++;
}
}
//知识点2,延时加锁
std::timed_mutex mtx1; //需要使用时间锁
void func1(){
for (int i = 0; i < 2; i++) {
std::unique_lock<std::timed_mutex> lg(mtx1, std::defer_lock);
//知识点2,延时加锁
if (lg.try_lock_for(std::chrono::seconds(2))) {
std::this_thread::sleep_for(std::chrono::seconds(1));
g_i++;
}
}
}
int main() {
std::thread t1(func1);
std::thread t2(func1);
t1.join();
t2.join();
std::cout << g_i << '\n';
}
总之,一定要记住。unique_lock会在构建的时候可以选择是否进行加锁,析构的时候会解锁,并且可以选择延迟加锁。
二、小结
- 互斥量(mutex)是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题。
- 常常使用lock和unlock进行上锁和解锁,错误的行为有时会造成死锁,这就要要求两个线程按照相同的顺序获取互斥量的所有权。
- 创建lock_guard对象时,它会自动上锁,析构时自动解锁,比较方便。
- unique_lock**会在构建的时候可以选择是否进行加锁,析构的时候会解锁,并且可以选择延迟加锁。适用范围更广。
感谢大家阅读!
接下来还会继续更新多线程相关知识,感兴趣的可以看其他笔记!