1 背景
在多线程编程中,大家经常会遇到一个问题,每个线程都会对同一个对象(比如自动驾驶中的轨迹信息)进行读写的操作,那么如何保护该对象在各自的线程内是安全的呢?
因此就引入了本篇博客要讨论的一个话题:std::lock_guard。因为在量产中会经常使用,所以在此介绍给各位读者朋友们,希望有所帮助。
2 std::lock_guard 简介
std::lock_guard 是 C++ 标准库中的一个模板类,它用于简化互斥体(mutex)的管理,确保在 std::lock_guard 的生命周期内,互斥体被正确锁定和解锁。这有助于防止由于程序员忘记解锁互斥体而导致的死锁和其他并发问题。
以下是 std::lock_guard 的详细介绍:
-
模板参数
std::lock_guard 是一个模板类,它接受一个互斥体类型作为模板参数。通常,这个互斥体类型会是 std::mutex,但也可以是其他满足互斥体要求的类型。
-
构造函数
std::lock_guard有两个构造函数:
(1)第一个构造函数接受一个互斥体引用作为参数,并在构造时锁定该互斥体。
(2)第二个构造函数是默认构造函数,它不接受任何参数,也不锁定任何互斥体。但是,使用默认构造函数的 std::lock_guard对象不能用于锁定或解锁互斥体。
-
析构函数
当 std::lock_guard对象离开其作用域(即被销毁)时,其析构函数会自动解锁关联的互斥体。这是 std::lock_guard的主要优点之一,因为它确保了在所有情况下,互斥体都会被正确解锁,即使发生异常也是如此。
- 注意事项
(1)std::lock_guard 不可复制或移动,因此不能将其传递给其他函数或返回给调用者。
(2)由于 std::lock_guard 在其析构时自动解锁互斥体,因此不应在 std::lock_guard 对象的作用域内手动解锁互斥体,否则会导致未定义的行为。
(3)如果需要更复杂的锁定逻辑(例如,同时锁定多个互斥体),则可能需要使用 std::unique_lock 而不是 std::lock_guard。
3 std::lock_guard 使用
这个类是一个互斥量的包装类,用来提供自动为互斥量上锁和解锁的功能,简化了多线程编程,用法如下:
#include <mutex>
std::mutex mtx;
void function() {
// 构造时自动加锁
std::lock_guard<std::mutex> (mtx);
// 离开局部作用域,析构函数自动完成解锁功能
}
用法非常简单,只需在保证线程安全的函数开始处加上一行代码即可,其他的都在这个类的构造函数和析构函数中自动完成。
3.1 什么是 std::mutex ?
还有一个疑问,std::lock_guard都会使用了一个 std::mutex 作为构造函数的参数,这是因为std::lock_guard只是一个包装类,而实际的加锁和解锁的操作都还是 std::mutex 完成的,那什么是 std::mutex 呢?
std::mutex 其实是一个用于保护共享数据不会同时被多个线程访问的类,它叫做互斥量,可以把它看作一把锁,它的基本使用方法如下:
#include <mutex>
std::mutex mtx;
void function() {
//加锁
mtx.lock();
// task
// 离开作用域解锁
mtx.unlock();
}
3.2 什么是锁?
前面的问题介绍中都提到了锁这个概念,那么什么是锁,又有什么用处呢?锁是用来保护共享资源(变量或者代码)不被并发访问的一种方法,它只是方法,实际的实现就是 std::mutex 等等的类了。可以简单的理解为:
(1)当前线程访问一个变量之前,将这个变量放到盒子里锁住,并且当前线程拿着钥匙。这样一来,如果有其他的线程也要访问这个变量,则必须等待当前线程将盒子解锁之后才能访问,之后其他线程在访问这个变量之前也将会再次锁住这个变量。
(2)当前线程执行完后,就将该盒子解锁,这样其他的线程就可以拿到盒子的钥匙,并再次加锁访问这个变量了。
这样就保证了同一时刻只有一个线程可以访问共享资源,解决了简单的线程安全问题。
3.3 举例分析
这个例子中,主线程开启了 2 个子线程,每个子线程都修改共享的全局变量 kTestIntData
,如果没有增加必要的锁机制,那么每个子线程打印出的 kTestIntData
就可能会出错。这里使用了 2 种不同的加锁方法来解决:
- 使用 std::lock_guard
- 使用 std::mutex 实现原生的加锁
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; //全局互斥体
int kTestIntData = 0; //两个子线程共享的全局变量
void print_data() {
// 1.创建一个互斥量的包装类,用来自动管理互斥量的获取和释放
// std::lock_guard<std::mutex> lock(mtx);// 锁定互斥体
// 2.原生加锁
// mtx.lock();
for (int i = 0; i < 10; i++) {
// 打印当前线程的 id : kTestIntData
std::cout << std::this_thread::get_id()
<< ":" << kTestIntData++ << std::endl;
}
// 2. 原生解锁
//mtx.unlock();
// 离开局部作用域,局部锁解锁,释放互斥量
}
int main() {
// 开启两个线程
std::thread th1(print_data);
std::thread th2(print_data);
// 主线程等待上面创建的两个线程完成操作之后再退出
th1.join();
th2.join();
return 0;
}
多线程是一种重要的并行处理技术,它通过允许程序在同一时间内执行多个线程来提高程序的性能和响应速度。但也需要注意线程安全、线程管理和同步机制等问题,多线程编程时需要注意线程安全,避免多个线程同时访问和修改共享资源导致的数据不一致问题。
为什么不加锁的结果会出错?
首先线程是一种轻量级的进程,也存在调度,假设当前 CPU
使用的是基于时间片的轮转调度算法,为每个进程分配一段可执行的时间片,因此每个线程都得到一段可以执行的时间(更详细的信息可参考《计算机组成原理》,在这里就不多说了)。当子线程 thread1在修改并打印变量 kTestIntData
的时候,子线程 thread1 的时间片就用完了,那么CPU切换到子线程 thread2 去修改并打印 kTestIntData
,下一周期同理。