C++语言级别的多线程最大的好处:
- 代码可以跨平台
在不支持语言级别的多线程编程的时候,只能是在代码中调用系统提供的API,程序就不支持在多平台编译运行
实际上,C++语言级别的多线程thread只是在系统的API上提供了一个封装,根据不同的平台实际上底层会调用不同的API:
windows —> createThread
linux----->pthread_create
-
线程内容:
一、怎么创建启动一个线程
- std::thread定义一个线程对象,传入线程所需要的线程函数和参数,线程就自动开始运行了
二、子线程如何结束
- 子线程函数运行完成,线程就结束了
三、主线程如何处理子线程?
- t.join():等待t线程结束,当前线程继续向下运行
- t.detach():把t线程设置为分离线程,主线程结束,整个进程结束,看不到分离线程的输出结果了
-
通过thread编写C++多线程程序
#include <iostream>
#include <thread>
//using namespace std; //公司里面一般不用,避免命名冲突
void threadHandle1(int time)
{
// 让子线程睡眠2秒
// thi_thread是一个命名空间, chrono也是一个命名空间
std::this_thread::sleep_for(std::chrono::seconds(time));
std::cout << "hello thread1" << std::endl;
}
int main()
{
// 传入线程函数创建一个线程对象,新线程就开始运行了
std::thread t1(threadHandle1, 2);
// 主线程等待子线程运行结束,主线程继续往下运行
// 主线程运行完成,如果当前进程还有未完成的子线程,进程就会异常终止(C++语言级别的多线程的情况)
//t1.join();
// 把子线程设置为分离线程
// 子线程分离出去,和主线程没有任何的关系
// 我们也看不到分离出去的线程的输出结果
t1.detach();
std::cout << "main thread done" << std::endl;
}
说明:
- 主线程运行完成,如果当前进程还有未完成的子线程,进程就会异常终止(C++语言级别的多线程的情况)
- 通过设置分离线程,把子线程分离出去,这样子线程就和主线程无任何关系,主线程正常运行结束,但是我们也看不到分离出去的线程的输出结果
线程间互斥-mutex互斥锁和lock_guard
- 竞态条件:多线程程序执行的结果是一致的,不会随着CPU对线程不同的调用顺序,而产生不同的运行结果。如果每次运行后的结果不一样,那么说明程序中存在竞态条件
#include <iostream>
#include <thread>
#include <mutex> // 包含互斥锁的头文件
#include <list>
//using namespace std;
int ticketcount = 100;// 总票数
没有加互斥锁,存在竞态条件的线程函数
void seilTicket(int index)
{
while (ticketcount > 0)
{
cout << "窗口:" << index << " 卖出第" << ticketcount << "张票" << endl;
ticketcount--;
/*
mov eax, ticketcount
sub eax, 1
mov ticketcount, eax
*/
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
模拟3个窗口进行卖票
int main()
{
std::list<std::thread> tlist;
// 模拟3个窗口进行卖票
for (int i = 0; i < 3; ++i)
{
tlist.push_back(std::thread(seilTicket,i));
}
// 不允许拷贝构造线程,这里使用了引用
for (std::thread& t:tlist)
{
// 主线程运行完成,如果当前进程还有未完成的子线程,进程就会异常终止
// 等待所有子线程都运行结束后,主线程继续运行
t.join();
}
return 0;
}
从运行结果的一部分截图可以看到,存在多次卖出同一张票的情况,这显然是错误的!!原因也很简单:–操作并不是原子操作,当一个线程准备执行–操作时,另一个线程获取了还未被处理的票数(可以举个例子)
添加互斥锁,来管理临界资源
- 锁+双重判断
#include <mutex> // 包含互斥锁头文件
int ticketcount = 100;
std::mutex mtx;// 锁变量
void seilTicket(int index)
{
// 锁+双重判断
while (ticketcount > 0)
{
mtx.lock();
if (ticketcount > 0) // 这里再加一个判断,避免输出ticketcount为负的情况
{
// 临界区代码段
std::cout << "窗口:" << index << " 卖出第" << ticketcount << "张票" << std::endl;
/*
mov eax, ticketcount
sub eax, 1
mov ticketcount, eax
*/
ticketcount--;
}
mtx.unlock();
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
手动添加互斥锁还存在一个问题,如果执行临界区代码时,线程提前(比如条件成立后return)结束了,那么由于还没有执行unlock(),其他线程就无法获取锁了;
所以,我们要使用智能指针,确保无论什么情况,线程结束都能把锁释放掉:
-
lock_guard
-
接收锁类型变量,调用构造函数,自动加锁
-
与scoped_ptr类似,不允许使用拷贝构造和赋值运算符重载函数(删除了拷贝构造和赋值运算符重载函数),无法作为参数传递,无法作为函数返回值
-
出作用域,析构,自动解锁
-
-
unique_lock
- 支持自动加锁,释放锁
- 与unique_ptr类似,不允许使用左值引用的拷贝构造和赋值,定义了带右值引用的拷贝构造和赋值,可以使用lock()和unlock()加锁和解锁
- 在线程通信中会比较常用到
// lock_guard和unique_guard
void seilTicket(int index)
{
// 锁+双重判断
while (ticketcount > 0)
{
{
// 这里建议使用
std::lock_guard<std::mutex> lock(mtx);
/*
unique_lock<std::mutex> lck(mtx);
lck.lock();
*/
if (ticketcount > 0)
{
// 临界区代码段
std::cout << "窗口:" << index << " 卖出第" << ticketcount << "张票" << std::endl;
/*
mov eax, ticketcount
sub eax, 1
mov ticketcount, eax
*/
ticketcount--;
}
/*lck.unlock();*/
}// 出作用域就会把自动调用析构把锁释放
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
int main()
{
std::list<std::thread> tlist;
// 模拟3个窗口进行卖票
for (int i = 0; i < 3; ++i)
{
tlist.push_back(std::thread(seilTicket,i));
}
// 不允许拷贝构造线程,这里使用了引用
for (std::thread& t:tlist)
{
// 主线程运行完成,如果当前进程还有未完成的子线程,进程就会异常终止
// 等待所有子线程都运行结束后,主线程继续运行
t.join();
}
return 0;
}
线程间的同步通信
多线程编程的两个问题:
-
线程间互斥
临界区代码段存在竞态条件,需要保证对临界资源的操作是原子操作,所以给临界区代码段加上互斥锁,或者是轻量级的无锁实现CAS
(Linux下执行 strace ./a.out )
-
线程间同步
- 生产者和消费者线程模型
(注意:C++STL提供的容器都不是线程安全的,需要我们自己去保证)
#include <iostream>
#include <thread>
#include <condition_variable> // 包含条件变量的头文件
#include <queue>
using namespace std;
/*
生产者,消费者模型
*/
// 生产者生产一个物品,通知消费者消费一个,消费完了,消费者再通知生产者继续生产物品
std::mutex mtx; // 定义互斥锁,做线程间的互斥操作
std::condition_variable cv; // 定义条件变量,做线程间的通信操作
class Queue
{
public:
void put(int val) //生产物品
{
// 配合cv.wait()
unique_lock<std::mutex> lck(mtx);
while (!que.empty())
{
// que不为空,生产者应通知消费者去消费,消费完了,再继续生产
// 生产者应该进入等待状态,并且把互斥锁mtx释放
cv.wait(lck);
}// 获得其他线程的通知后,由等待状态==》阻塞
que.push(val);
/*
notify_one:通知另外的一个线程
notify_all:通知其他所有线程
*/
// 通知其他线程我生产完了,快开始消费
cv.notify_all();
cout << "生产者 生产:" << val << "号物品" << endl;
// 互斥锁mtx释放
}
int get() // 消费物品
{
unique_lock<std::mutex> lck(mtx);
while (que.empty()) // 注意得是循环等待
{
// 消费者发现que是空的
// 1.进入等待状态; 2.把mtx释放
cv.wait(lck);
}// 获得其他线程的通知后,由等待状态==》阻塞
// 阻塞状态下获得其他线程释放的锁后,开始运行
int val = que.front();
que.pop();
// 通知其他线程,我已经消费了,快开始生产
cv.notify_all();
cout << "消费者 消费:" << val << "号物品" << endl;
return val;
}
private:
queue<int> que;
};
// 生产者线程函数
void producer(Queue* que)
{
for (int i=1;i<=10;++i)
{
que->put(i);
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
// 消费者线程函数
void consumer(Queue* que)
{
for (int i = 1; i <= 10; ++i)
{
que->get();
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
- 注意cv.wait(lck)
- 线程进入等待状态,并且释放锁
- 接收到其他线程后的通知后,由等待状态变为阻塞状态,只有获取其他线程释放的锁后才会继续运行
int main()
{
Queue que;// 共享队列
std::thread thread1(producer, &que);// 创建生产者线程
std::thread thread2(consumer, &que);// 创建消费者线程
thread1.join();
thread2.join();
return 0;
}
再谈unique_lock和lock_guard
-
lock_guard
- lock_guard对象不能用在函数参数传递或者返回过程中,只能用在简单的临界区代码段的互斥操作中
-
unique_lock
- 不仅能用在简单的临界区代码段的互斥操作中,还能用在函数调用过程中
- 线程通信中比较常用
-
条件变量condition_variable
cv.notify_all()
- 通知在cv上等待的线程,条件成立了,起来干活了
- 其他在cv上等待的线程,收到通知,从等待状态–》阻塞状态–》 获取互斥锁后,线程继续运行
基于CAS操作的atomic原子类型
互斥锁是比较重的,如果临界区代码要做的事情比较复杂,那么一般使用互斥锁;
如果临界区代码只涉及到一些比较简单的操作,比如count++,count–,那么就没必要去使用互斥锁,而是从系统理论角度出发:用CAS来保证++,–操作的原子特性,CAS称为无锁操作,但是并不是不加锁,而是不在软件层面加/解锁,而是相当于给总线加锁(硬件层面,CPU必须支持CAS操作)
#include <iostream>
#include <atomic>
#include <thread>
#include <list>
using namespace std;
// isReady和mycount都是在数据段上存储的全局变量,作为多个线程的共享资源
/*
cpu执行线程指令时通过优化,将共享资源的值都拷贝到线程的缓存里(cpu的缓存);
为了保证一个线程对共享变量的改变,能够马上反映到另一个线程上,所以使用volatile修饰共享变量
*/
volatile std::atomic_bool isReady = false;
volatile std::atomic_int mycount = 0;
void task()
{
while (!isReady)
{
std::this_thread::yield(); // 线程让出时间片,等待下一次调度
}
for (int i=0;i<100;++i)
{
// mycount是原子类型,在硬件层面上保证了mycount++是原子操作
mycount++;
}
}
int main()
{
list<std::thread> lis;
for (int i = 0; i < 10; ++i)
{
lis.push_back(thread(task));
}
std::this_thread::sleep_for(std::chrono::seconds(3));
isReady = true;
for (thread &t:lis)
{
t.join();
}
cout << "mycount=" << mycount;
return 0;
}
如果临界区代码段的操作比较简单,那么可以将共享变量设置为原子类型,提高程序执行的效率!!