C++11 多线程与并发编程(2)
文章目录
线程安全
当多个线程共享数据时,需要注意线程安全问题(数据竞争,死锁等)。
如:假设多个线程同时访问一个全局变量,且其中至少一个线程对该变量进行了修改,就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。
为了保证线程安全,我们可以采用以下一些方式。
利用<mutex>库使用互斥量(C++11)
互斥量(mutex):是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。提供两个基本操作:
- lock() :线程调用lock时,若互斥量当前没有被其他线程占用,则可获得该互斥量的所有权;否则会阻塞该线程,直到其他线程释放此互斥量。
- unlock():释放该互斥量
C++ 11提供<mutex>库,提供互斥量及其基本操作可以在多线程中保护共享数据。
mutex类
1、创建互斥量:std::mutex mtxName;
2、mutex
互斥量的锁定与释放
成员函数名 | 作用 |
---|---|
lock() | 锁定mutex互斥量,若mutex互斥量被占用,则阻塞 |
unlock() | 解锁mutex互斥量 |
try_lock() | 尝试锁定互斥量。成功获得锁返回true,否则则返回false |
代码示例:
#include<thread>
#include<mutex>
#include<iostream>
#include<chrono>
// 定义一个时间变量
std::chrono::milliseconds interval(100);
std::mutex mtx;
int job_shared = 0; // 该变量将会被多个线程共享
int job_exclusive = 0; // 只有一个线程会修改此变量
void job_1(){
std::this_thread::sleep_for(interval); //休眠,使job_2一开始持锁
while(true){
// 不断尝试锁定mtx,获取job_shared的当前值,成功则返回
if(mtx.try_lock()){
std::cout<< "share mission: " << job_shared <<std::endl;
mtx.unlock();
return;
}else{
// 未获得锁时,线程还可以进行别的任务
++job_exclusive;
std::cout<< "running other mission: "<< job_exclusive <<std::endl;
std::this_thread::sleep_for(interval); // 休眠
}
}
}
void job_2(){
// 上锁
mtx.lock();
std::this_thread::sleep_for(5*interval);
++job_shared;
mtx.unlock(); // 释放
}
int main(){
std::thread thread_1(job_1);
std::thread thread_2(job_2);
thread_1.join();
thread_2.join();
return 0;
}
mutex的使用中避免死锁
假设有两个互斥量mtx1和mtx2,还有两个线程t1和t2,两个线程都需要获取mtx1和mtx2且上下锁的顺序刚好相反。
由于通常线程创建后就会开始执行,可能会出现一种意外,t1获取了其中一个互斥量,t2获取另一个互斥量,双方都需要再获得对方的互斥量才能运行下去,导致互相阻塞,产生死锁。
这种情况,我们需要让多个线程保持一致的取锁顺序,保证每次至少能有一个线程执行下去
关于互斥量的分配,有一些很经典的题目,可以自行查阅:
- 生产者—消费者问题
- 读者—写手问题
- 哲学家吃饭问题
- 吸烟者问题
针对死锁可以学习以下内容:
- 死锁避免——银行家算法
- 死锁检测——资源分配图
- 死锁接触:① 资源剥夺法 ②撤销进程法 ③ 进程回退法
由于笔者操作系统目前掌握不好,所以不在此班门弄斧,只能告诉大家可以去衍生学习这些内容
lock_guard类
std::lock_guard
是C++标准库中的一种互斥量封装类,用于管理互斥量的锁。
template< class Mutex >
class lock_guard; // 模板类,接受一个互斥量作为模板参数
// 初始化创建一个mutex的lock_guard对象
std::lock_guard<std::mutex> lg(mtx);
// 只要是互斥量对象都可以,如下
std::lock_guard<std::timed_mutex> tlg(tmtx);
lock_guard
类不可以复制(其拷贝构造已被删除)- 将一个互斥量对象传参给lock_guard后,lock_guard将会自动给其加锁
- 然后在
lock_guard
所属代码块运行结束,自动释放锁(调用其析构函数) - i.e. 特性是自动加解锁、不可复制
#include<iostream>
#include<thread>
#include<mutex>
std::mutex mtx; // 创建互斥量
void sharedFunction(int id) {
// 创建一个 lock_guard 对象,对 mtx 进行加锁
std::lock_guard<std::mutex> lg(mtx);
// 此时mtx已经被锁定
// 模拟访问共享资源
std::cout<<"Thread "<<id<<" is accessing shared resource."<< std::endl;
// 离开此作用域,lg会自动对mtx进行释放 ==> mtx.unlock();
}
int main(){
const int count = 5;
// 创建一个大小为5的线程池
std::thread threads[count];
// 给线程池中的每个线程初始化,并启动
for(int i = 0; i < count; i++){
threads[i] = std::thread(sharedFunction, i);
}
// 等待所有线程执行完毕
for(int i = 0; i < count; i++){
threads[i].join();
}
return 0;
}
unique_lock类
std::unique_lock
也是一种互斥量封装类, 它在lock_guard
的基础上提供更多功能,可以更灵活的管理互斥量,如:延迟加锁、条件变量、超时等。
构造函数 | 效果 |
---|---|
unique_lock() | 默认构造函数,一个未关联互斥量的类对象 |
unique_lock(mtx) | 和lock_guard()效果相同,根据互斥量初始化并加锁,作用域结束自动解锁。注意:该函数为explicit(即不支持类型隐式自动转换) |
unique_lock(mtx,可选参数t) | ① t为defer_lock: 利用mtx初始化,但不加锁 ② t为try_to_lock: 初始化并尝试加锁,若加锁失败则不关联任何互斥量 ③t为adopt_lock: 绑定一个mtx,并假定其已经被其他方式上锁(也就是可以接管已经被其他方式锁定的互斥量);对象被销毁时,也不会再次对互斥量进行解锁操作 |
unique_lock(mtx, 可选时间) | ①可选 若为chrono::duration :通过调用 m.try_lock_for(timeout_duration) 尝试锁定关联互斥体。阻塞到经过指定的 timeout_duration 或获得锁这两个事件的先达成者为止。②可选 若为 chrono::point : 通过调用 m.try_lock_until(timeout_time) 尝试锁定关联互斥体。阻塞到抵达指定的 timeout_time 或获得锁这两个事件的先达成者为止。 |
- 根据需求选用合适的构造函数
常用成员函数 | 用法 |
---|---|
lock() | 锁定互斥量,若互斥量被占用,则阻塞,直到成功加锁 |
unlock() | 释放互斥量 |
try_lock() | 尝试加锁,成功则返回true,否则返回false |
try_lock_for(rel_time) | 尝试加锁,若不成功,阻塞当前线程,直到成功加锁,或超过了rel_time参数(chrono::duration)指定的时间 |
try_lock_until(abs_time) | 尝试加锁,若不成功,阻塞当前线程,直到成功加锁,或超过了abs_time参数(chrono::point)指定的时间点 |
练习:实现一个线程安全的计数器类(Counter),其中包含两个方法:increment()
和 decrement()
,分别用于增加和减少计数器的值。要求在多线程环境下使用该类时,能够确保对计数器的操作是线程安全的。
#include <thread>
#include <mutex>
#include <iostream>
#include <chrono>
class Counter
{
public:
Counter() : count(0) {}
// 增加计数器的值
void increment()
{
// 先暂缓加锁,使用时间互斥量,因为后面需要判断是否超时
std::unique_lock<std::timed_mutex> ulg(tmtx, std::defer_lock);
// 最多尝试五次
int frequency = 5;
while (frequency--)
{
if (ulg.try_lock())
{
// 增加次数1
count += 1;
return;
}
// 未获锁,休眠0.5秒尝试
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "[Warning] Increment is failed" << std::endl;
}
// 减少计数器的值
void decrement()
{
std::unique_lock<std::timed_mutex> ulg(tmtx, std::defer_lock);
int frequency = 3;
// 最多尝试三次
while (frequency--)
{
// 未获得锁,阻塞0.5秒
if (ulg.try_lock_for(std::chrono::milliseconds(500)))
{
count -= 1;
return;
}
}
std::cout << "[Warning] Decrement is failed" << std::endl;
}
int get_value()
{
return count;
}
private:
int count;
std::timed_mutex tmtx; // 带有超时功能的互斥量
};
void increment_counter(Counter &counter, int n)
{
for (int i = 0; i < n; i++)
{
counter.increment();
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些工作时间
}
}
void decrement_counter(Counter &counter, int n)
{
for (int i = 0; i < n; ++i)
{
counter.decrement();
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些工作时间
}
}
int main()
{
Counter counter;
// 启动一个线程增加计数器五次
std::thread t1(increment_counter, std::ref(counter), 5);
// 启动一个线程减少计数器值3次
std::thread t2(decrement_counter, std::ref(counter), 3);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter.get_value() << std::endl;
return 0;
}
特殊互斥量类
std::recursive_mutex
:递归互斥量。允许同一个线程多次锁定同一个互斥量,而不会发生死锁。std::timed_mutex
:带有超时功能的互斥量。带有超时功能的互斥量允许线程尝试在一定时间内获取互斥量的锁,如果在指定时间内无法获取到锁,则可以进行其他处理或者放弃获取锁。(具有try_lock_for()
等成员函数)std::shared_mutex
(头文件<shared_mutex>,【since C++17】):共享互斥量。共享互斥量允许多个线程同时获取读取权限,但在写入时需要互斥。(配合shared_lock()
使用,则允许多个线程获取互斥量)
函数:call_once()
作用:确保函数或代码片段在多线程环境下,只需要执行一次。
语法:void call_once(flag, func, args);
- flag是
std::once_flag
,使call_once中的func
只调用一次。注意:如果在多线程中使用了call_once,想要只调用一次func,那么这多个线程的flag一定要是同一个flag。下面代码说明。 - func是可调用对象,通常为函数
- args是func中的参数,没有则不写
运用场景:call_once()多用于多线程需要只初始化一次资源的情景。比如在多线程操作时编写单例模式。以下是一个代码说明案例:
#include <iostream>
#include <thread>
#include <mutex>
// (1)使用call_once实现单例模式
class Singleton
{
private:
Singleton() {} // 构造函数私有化,禁止外部实例化
static Singleton *instance; // 单例实例指针
static std::once_flag flag; // 用于标记初始化是否完成的标志
static int instance_count; // 记录实例数量
// 通常根据需求应该是作为静态成员变量
// 但此处,我们只会实例化一次
// 为了更直观看到是否能成功加解锁,所以定义为普通成员变量
std::mutex mtx;
public:
// 获取单例实例的静态成员函数
static Singleton *getInstance()
{
// 创建一个instance实例
std::call_once(flag, []()
{
instance = new Singleton();
instance_count++; });
return instance;
}
// 返回实例总数
static int get_instance_count()
{
return instance_count;
}
// 示意性的单例方法
void doSomething()
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Singleton doing something. Instance count: "<< instance_count << std::endl;
}
};
// 静态成员初始化,静态成员被所有类对象(实例)共享
Singleton *Singleton::instance = nullptr;
std::once_flag Singleton::flag;
int Singleton::instance_count = 0;
// 测试函数,创建多个线程验证单例模式
void test_singleton()
{
Singleton *s = Singleton::getInstance();
s->doSomething();
}
// (2)call_once错误用法的测试函数
void init()
{
// 若下面的init_flag不同,这行代码会被调用多次。
std::cout << "data has inited!" << std::endl;
}
void failureCase()
{
// 注意,我们给call_once 传参flag时,不可以是一个局部变量
// 否则如果每次flag不同,call_once会根据不同flag调用函数
// 下面是错误用法
std::once_flag init_flag;
std::call_once(init_flag, init);
}
int main()
{
Singleton *s = Singleton::getInstance(); // 获取单例实例
// 创建多个线程来测试单例模式
std::thread t1(test_singleton);
std::thread t2(test_singleton);
std::thread t3(test_singleton);
// 等待线程结束
t1.join();
t2.join();
t3.join();
// 錯誤用法示例
std::cout << "[Warning] If two lines are printed below, it is wrong" << std::endl;
std::thread ft1(failureCase);
std::thread ft2(failureCase);
ft1.join();
ft2.join();
return 0;
<condition_variable>条件变量
头文件:<condition_variable>
std::condition_variable
是一个同步原语,用于多线程间的通信和同步。与std::mutex
一起使用,可以实现线程间的等待和通知机制。
使用的步骤如下:
- 创建一个
std::condition_variable
对象。 - 创建一个互斥量
std::mutex
对象,用于保护共享资源的访问。 - 在需要等待条件变量的地方,使用
std::unique_lock
或其他互斥封装类锁定互斥量,然后调用std::condition_variable
的等待原语,将线程阻塞,等待条件变量。 - 当其他线程利用
std::condition_variable
进行通知时,被阻塞的线程会被唤醒,继续运行。
特性:
- 不可复制(operator= (=delete))
【用于阻塞的成员函数】
1、void wait( std::unique_lock<std::mutex>& lock , Predicate pred)
- 会使当前线程任务阻塞,并释放lock所代表的互斥量;直到收到通知,再次锁定互斥量,继续当前线程任务
pred
(参数可选):是一个谓词,是一个可调用的对象,用于判断是否满足等待条件。如果pred返回false
会阻塞当前线程,等该线程收到通知,被唤醒后,依然会调用pred检查,需要pred返回true
,该线程才会继续执行。
2、void wait_for( std::unique_lock<std::mutex>& lock ,chrono::duration timeout_duration , Predicate pred)
- 在
wait()
基础上新增超时时间,若超过指定的timeout_duration
,该线程会被自动唤醒(但有pred时,仍然会检查是否为true
)。
3、void wait_until( std::unique_lock<std::mutex>& lock ,chrono::point abs_time, Predicate pred)
- 在
wait()
基础上新增超时时间,到达指定时间,该线程自动唤醒(但有pred时,仍然会检查是否为true
)。
【用于通知的成员函数】
1、void notify_one()
- 若有任何线程在 *this 上等待,则调用
notify_one
会除阻等待线程之一。
2、void notify_all()
- 解除所有在*this上等待的线程
代码说明:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
mutex mtx;
condition_variable cv;
bool ready = false;
void thread_function() {
unique_lock<mutex> lock(mtx);
while (!ready) {
// 阻塞当前线程,并释放lock拥有的线程锁
// 当运行这一行以后,这个线程就会停在在这里
// 不会进行while(!ready)的循环判断
// 直到下面cv.notify通知它,你可以继续运行
// 这个循环判断才会继续判断。
cv.wait(lock);
}
cout << "Thread is ready!" << endl;
}
int main() {
thread t(thread_function);
// 模拟主线程的一些工作
this_thread::sleep_for(chrono::seconds(2));
// 准备好了,通知线程
{
lock_guard<mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 唤醒使用cv阻塞的线程
t.join();
return 0;
}
参考: