线程执行顺序
在启动多个线程时线程的执行顺序完全却决于CPU的线程调度机制有关,因此可能存在线程运行期间是无法保证顺序的,而且随时会被切换到另一个线程,线程的调度不可控因此也就产生了多个线程访问同一个数据时,造成数据脏读
这一类问题,其中最经典的例子就是多个线程买票问题
,简单示例代码如下:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int ticket = 500;
class Wicket
{
public:
Wicket(const string & _wicket_name) {
wicket_name_ = _wicket_name;
}
void operator() () {
while (ticket > 0) {
ticket -= 1;
cout << wicket_name_ << " 售出1张票,剩余 " << ticket <<" 张票." << endl;
}
}
private:
string wicket_name_;
};
int main()
{
vector<thread> thread_arrays;
thread_arrays.push_back(thread(Wicket("售票窗口A")));
thread_arrays.push_back(thread(Wicket("售票窗口B")));
for (int index = 0; index < 2; ++index) {
thread_arrays.at(index).join();
}
system("pause");
return 0;
}
互斥量(mutex)
lock和unlock
在多线程访问同一个数据变量时常常会提到互斥量
这个概念,即在线程读写一个共享数据变量时,将数据锁住,其他线程一看被锁住就不再操作,而是等到该变量解锁的时候由当前获得执行权限的线程访问该变量,示例代码如下:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
using namespace std;
int ticket = 500;
// 定义一个锁对象变量
mutex sell_ticket_mutex_;
class Wicket
{
public:
Wicket(const string & _wicket_name) {
wicket_name_ = _wicket_name;
}
void operator() () {
while (ticket > 0) {
// 在减票前加上锁
sell_ticket_mutex_.lock();
ticket -= 1;
cout << wicket_name_ << " 售出1张票,剩余 " << ticket <<" 张票." << endl;
// 在减票后解开锁
sell_ticket_mutex_.unlock();
this_thread::sleep_for(chrono::milliseconds(1));
}
}
private:
string wicket_name_;
};
int main()
{
vector<thread> thread_arrays;
thread_arrays.push_back(thread(Wicket("售票窗口A")));
thread_arrays.push_back(thread(Wicket("售票窗口B")));
for (int index = 0; index < 2; ++index) {
thread_arrays.at(index).join();
}
system("pause");
return 0;
}
上面代码使用lock()
和unlock()
对某一段数据进行锁定,注意这两个函数一定要成对出现,否则将会导致其他线程停止在lock()处卡死
。
lock_guard
可以使用lock_guard
替换lock()
和unlock()
,示例代码如下:
void operator() () {
while (ticket > 0) {
{
// 声明一个lock_guard对象,将互斥量作为构造参数传入
lock_guard<mutex> guard_thread_(sell_ticket_mutex_);
ticket -= 1;
cout << wicket_name_ << " 卖出1张票,剩余 " << ticket << " 张票." << endl;
}
this_thread::sleep_for(chrono::milliseconds(100));
}
}
使用lock_guard
时要注意不能再使用lock()
和unlock()
两个函数,同时,要将锁定的范围进行局部化
。这个lock_guard
会在声明的时候锁住,然后出了声明的作用域后就会自动解锁,因此不能在一个全局的地方声明它。例如,上面的代码中使用{...}
将减票操作和打印操作括起来,而将耗时等待放到外部,进入花括号的时候声明lock_guard
上锁,出了花括号就自动解锁,进入睡眠状态。其他线程就可以执行了。
lock_guard
的机制其实就是在构造的时候用传进去的互斥量参数调用一次lock()
,当出了作用域后声明的对象可能会析构,在析构函数中调用一次unlock()
。
如果需要在构造时不调用lock()
可以在构造的时候增加std::adop_lock
参数。
unique_lock
unique_lock
与lock_guard
相比可以使用的方式更加丰富,常见参数如下:
- adop_lock
如果使用这个参数那么在构造对象前需要手动进行加锁处理。
- defer_lock_t
与adop_lock
正好相反,在构造前不能上锁。
- try_to_lock
这个标志表示在构造的时候会使用try_lock()
进行尝试上锁。注意使用try_lock()
并不会阻塞
后边程序的运行,通常要与if...else
配合使用否则起不到互斥的作用,作为unique_lock
构造参数时,使用方式如下:
class Wicket
{
public:
Wicket(const string & _wicket_name) {
wicket_name_ = _wicket_name;
}
void operator() () {
while (ticket > 0) {
{
unique_lock<mutex> guard_thread_(sell_ticket_mutex_, try_to_lock);
// 判断是否拥有锁的占用权
if (guard_thread_.owns_lock()) {
ticket -= 1;
cout << wicket_name_ << " 卖出1张票,剩余 " << ticket << " 张票." << endl;
} else {
cout << wicket_name_ << " 不可买票" << endl;
}
}
this_thread::sleep_for(chrono::milliseconds(100));
}
}
private:
string wicket_name_;
};
等待函数
现在有一种情况,当A线程需要等待B线程提供的数据,因此启动线A程并开始执行前先判断一下是否存在共享的数据是否存在,如果不存在则睡眠一段时间,这是一种定时器唤醒模式
它的执行效率并不高,这就需要使用条件变量(condition_variable)配合互斥量可以实现
等待模式唤醒模式`,示例代码:
int goods_count = 0;
mutex sell_goods_mutex_;
condition_variable condition_lock;
class ReplenishStock
{
public:
ReplenishStock() {}
void operator() () {
while (true) {
unique_lock<mutex> guard_thread_(sell_goods_mutex_);
condition_lock.wait(guard_thread_, [this] {
return goods_count == 0;
});
cout << "正在进货 " << endl;
goods_count += 5;
this_thread::sleep_for(chrono::seconds(1));
condition_lock.notify_one();
}
}
};
class Sales
{
public:
Sales() {}
void operator() () {
while (true) {
unique_lock<mutex> guard_thread_(sell_goods_mutex_);
condition_lock.wait(guard_thread_, [this] {
return goods_count != 0;
});
int need_sales_count = goods_count;
for (int index = 0; index < need_sales_count; ++index) {
;
cout << "卖出货物, 剩余 " << --goods_count << " 件货" << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
condition_lock.notify_one();
}
}
};
等待函数解析
上面代码中主最主要的就是wait()
函数,
unique_lock<mutex> guard_thread_(sell_goods_mutex_);
condition_lock.wait(guard_thread_, [this] {
return goods_count != 0;
});
该函数第一个参数是锁对象
这里使用的是unique_lock
,第二个参数是一个函数,该函数的返回值必须要是bool类型
。
当前程序进入while后首先创建unique_lock
对象此时互斥量被锁主,执行wait()
函数,该函数的执行过程机器原理如下:
- 如果没有设置函数(即,没有第二个参数)则阻塞在此处。
- 如果设置了函数,则根据函数的返回值再决定是否阻塞在此处,其中如果返回的是
true则不阻塞
,返回的是false则阻塞
。 - 当处于
阻塞状态时释放锁资源
。 - 在阻塞状态下只有在其他线程调用了
notify_one
之后被唤醒。 - 被唤醒后首先是获取
互斥量
的锁,如果获取失败则继续等待,成功后如果有第二个参数则根据返回值决定是执行还是阻塞。