C++高级——多线程编程
目录
线程
线程是操作系统能够进行运算调度的最小单位。被包含在进程之中,是进程的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程可以并发执行多个线程,每个线程会执行不同的任务。对应在现实生活中,进程是组长,线程是小组成员。
怎么创建启动一个线程
在语言级别,一般调用std名称空间的thread类来启动一个线程。
其对应操作系统层次的一下系统调用:
windows: createThread
linux:pthread_create
以下是thread类的一个构造函数:
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
我们可以看到,其需要一个线程函数(也可以是类对象和lambda表达式)以及这个函数所需要传入的参数。
所以我们便可以这样来创建线程:
void threadHandle1(int time)
{
// 让子线程睡眠time秒
std::this_thread::sleep_for(std::chrono::seconds(time));
cout << "hello thread1!" << endl;
}
void threadHandle2(int time)
{
// 让子线程睡眠time秒
std::this_thread::sleep_for(std::chrono::seconds(time));
cout << "hello thread2!" << endl;
}
// 创建了一个线程对象,传入一个线程函数,新线程就开始运行了
std::thread t1(threadHandle1, 2);
std::thread t2(threadHandle2, 3);
这样,t1就会执行threadHandle1函数,t2执行threadHandle2函数。
线程如何区分
线程除了站在我们角度上的以名字区分,它还有一个属于自己的id!
通过std::thread::get_id()
便可以获取到该成员对象线程的id。
std::cout << "t1 thread :: ID = " << t1.get_id() << std::endl;
而在线程函数中通过std::this_thread::get_id()
获取线程id。
std::cout << "inside thread :: ID = " << std::this_thread::get_id() << std::endl;
线程如何结束
线程结束主要分为以下四种方式:
线程函数返回(推荐)
调用ExitThraed函数,线程自行撤销
同一进程或者另一个进程中调用TerminateThread函数
ExitProcess和TerminateProcess函数也可以用来终止线程进行
除了第一种,其他都不推荐使用,那我就不把它们写进博客了。
主线程如何处理子线程
主要用到的就是join和detach,其他的百度去吧。
t1.join();
这个方法让主函数等待子线程结束,主线程才继续往下继续运行。
t1.detach();
这个方法把子线程设置为分离线程。也就是主线程和子线程断绝父子关系了。
在一般情况下,如果主线程结束,就代表整个进程结束,如果这时有子线程还未结束就会出现运行错误。
当设置子线程为分离线程,主线程结束,子线程也自动结束。
多线程编程
总结了线程的基本知识,我们现在就来看一下多线程编程。
假设有车站的三个买票窗口来卖100张票。
int ticketCount = 100; // 车站有100张车票,由三个窗口一起卖票
int main()
{
list<std::thread> tlist;
for (int i = 1; i <= 3; ++i)
{
tlist.push_back(std::thread(sellTicket, i));
}
for (std::thread &t : tlist)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
现在我们看看具体是怎样卖票的:
我们知道对车票(count)进行卖(--操作
)时,会分为3步:
mov eax, count
sub eax,1
mov count,eax
这不是原子性的!
CPU可能刚执行完sub操作的时候,该线程(t1)时间片到了执行到其他线程(t2),这样其他卖票窗口拿到的count也是100,然后这个线程(t2)执行完count = 99
,CPU又回去执行t1,这时你就会白给一张票。
所以就必须引入锁操作。
锁
在多线程程序,需满足竞态条件:多线程程序执行的结果是一致的,不会随着CPU对线程不同的调用顺序,而产生不同的结果。
所以我们就需要定义一把锁。
std::mutex mtx; // 全局的一把互斥锁
有了这把锁,我们就可以实现一个没什么大错的卖票程序了:
// 模拟卖票的线程函数 lock_guard unique_lock
void sellTicket(int index)
{
while (ticketCount > 0) // ticketCount=1 锁+双重判断
{
// 保证所有线程都能释放锁,防止死锁问题的发生 scoped_ptr
lock_guard<std::mutex> lock(mtx);
if (ticketCount > 0)
{
// 临界区代码段 =》 原子操作 =》 线程间互斥操作了 =》 mutex
cout << "窗口:" << index << "卖出第:" << ticketCount << "张票!" << endl;
//cout << ticketCount << endl;
ticketCount--;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
这里用了lock_guard<std::mutex> lock(mtx);
把锁包装成了一个类,保证能出函数一定会释放锁。
CAS原子操作
因为锁的操作是比较重,而且在临界区代码做的事情比较复杂,比较多。所以引入了CAS来保证上面的--操作
的原子特性。同时这也是无锁操作。
首先定义一下原子的类型:
volatile std::atomic_bool isReady = false;
volatile std::atomic_int mycount = 0;
这里的volatile保证了每次数据都是从内存拿,而不是有一定安全性风险的寄存器。
void task()
{
while (!isReady)
{
std::this_thread::yield(); // 线程出让当前的CPU时间片,等待下一次调度
}
for (int i = 0; i < 100; ++i)
{
mycount++;
}
}
int main()
{
list<std::thread> tlist;
for (int i = 0; i < 10; ++i)
{
tlist.push_back(std::thread(task));
}
std::this_thread::sleep_for(std::chrono::seconds(3));
isReady = true;
for (std::thread &t : tlist)
{
t.join();
}
cout << "mycount:" << mycount << endl;
return 0;
}
这明显就比锁轻便了很多!
lock_guard和unique_lock
这两个其实可以类比智能指针来记:
lock_gurad类比于scoped_ptr,它的拷贝构造和复制构造都被删除了,不可用在函数参数传递或者返回过程中,只能用在简单的临界区代码段的互斥操作中。
lock_ guard(const lock_ guard&)=delete;
lock_ guard& operator= (const lock_ guard&)=delete;
而unique_lock可以类比于unique_ptr,它不仅可以用在简单的临界代码段的互斥操作中,还能用在函数调用过程中。
总的来说,建议使用unique_lock
.
线程通信——生产者消费者模型
现在就用一个比较常用的模型来认识一下线程通信。
首先先定义一下互斥锁mtx和条件变量cv:
std::mutex mtx; // 定义互斥锁,做线程间的互斥操作
std::condition_variable cv; // 定义条件变量,做线程间的同步通信操作
然后定义出生产者和消费者的类queue:
// 生产者生产一个物品,通知消费者消费一个;消费完了,消费者再通知生产者继续生产物品
class Queue
{
public:
void put(int val) // 生产物品
{
//lock_guard<std::mutex> guard(mtx); // scoped_ptr
unique_lock<std::mutex> lck(mtx); // unique_ptr
while (!que.empty())
{
// que不为空,生产者应该通知消费者去消费,消费完了,再继续生产
// 生产者线程进入#1等待状态,并且#2把mtx互斥锁释放掉
cv.wait(lck); // lck.lock() lck.unlock
}
que.push(val);
/* notify_one:通知另外的一个线程的 notify_all:通知其它所有线程的 通知其它所有的线程,我生产了一个物品,你们赶紧消费吧 其它线程得到该通知,就会从等待状态 =》 阻塞状态 =》 获取互斥锁才能继续执行 */
cv.notify_all();
cout << "生产者 生产:" << val << "号物品" << endl;
}
int get() // 消费物品
{
//lock_guard<std::mutex> guard(mtx); // scoped_ptr
unique_lock<std::mutex> lck(mtx); // unique_ptr
while (que.empty())
{
// 消费者线程发现que是空的,通知生产者线程先生产物品
// #1 进入等待状态 # 把互斥锁mutex释放
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::milliseconds(100));
}
}
void consumer(Queue *que) // 消费者线程
{
for (int i = 1; i <= 10; ++i)
{
que->get();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
创建两个线程:
int main()
{
Queue que; // 两个线程共享的队列
std::thread t1(producer, &que);
std::thread t2(consumer, &que);
t1.join();
t2.join();
return 0;
}
参考文献
[1] 施磊.腾讯课堂——C++高级.图论科技,2020.7.
[2] DoubleLi.如何终止线程运行.博客园,2012.8.15.