C++ 多线程笔记2 线程同步
并发(Concurrency)和并行(Parallelism)
并发是指在单核CPU上,通过时间片轮转的方式,让多个任务看起来像是同时进行的。实际上,CPU在一个时间段内只会处理一个任务,但是由于切换时间非常快,用户感觉像是多个任务同时在进行。
这种方式的优点是可以充分利用CPU资源,提高系统的响应能力。然而,由于CPU需要频繁地切换任务,这会带来上下文切换的开销,可能会导致系统效率下降。
并行处理是指多核CPU在同一时刻同时处理多个任务。每个核心都有自己的独立寄存器和运算单元,可以独立地执行任务。这种方式的优点是可以显著提高系统的处理能力,因为多个任务可以真正的同时进行。
然而,并行处理也有其缺点。首先,不是所有的任务都可以并行化,有些任务可能更适合串行执行。其次,并行处理需要更多的硬件资源,如内存和总线带宽,这可能会增加系统的成本。
IO密集型程序和CPU密集型程序
IO密集型程序是那些在执行过程中大部分时间都花费在输入/输出操作上的程序,如文件读写、网络通信等。
CPU密集型程序指的是那些在执行过程中大部分时间都用于计算操作,如数学计算、逻辑运算、数据处理等。
因此IO密集型程序适合采用多线程的并行机制提高性能,而CPU密集型不一定,因为线程的上下文切换太过于耗费CPU时间,所以不是多线程就代表高性能程序。
当如果是多CPU多核的情况下,CPU密集型程序也适合采用多线程执行,充分利用性能。
多线程的线程数量怎么确定
为了完成任务,线程真的越多越好吗?
-
线程的创建和销毁都是“重操作”,需要与操作系统内核空间进行交互,是相对昂贵的操作。
在服务执行的过程去实时创建销毁线程。 -
线程栈本身也会占用大量内存。每一个线程都需要线程栈,栈都被占完了无法做事情。
-
线程上下文切换要占用大量时间,上下文切换花费的CPU时间也特别多,导致CPU利用率就不高了。
-
大量线程唤醒会使得系统出现锯齿状负载或者瞬时负载导致宕机。
一般会根据CPU的核心数量来确定线程。
线程池的优势
操作系统上创建线程和销毁线程都是很“重“的操作,耗时耗性能都比较多,那么在服务执行的过程中,如果业务量比较大,实时的去创
建线程、执行业务、业务完成后销毁线程,那么会号致系统的实时性能降低,业务的处理能力也会降低。
线程池的优势就是(每个池都有自己的优势),在服务进程启动之初,就事先创建好线程池里面的线程,当业务流量到来时需要分配线
程,直接从线程池中获取一个空闲线程执行tsk任务即可,task执行完成后,也不用释放线程,而是把线程归还到线程池中继续给后续
的task提供服务。
ixed模式线程池
线程池里面的线程个数是固定不变的,一般是ThreadPoolf创建时根据当前机器的CPU核心数量
进行指定。
cached模式线程池
线程池里面的线程个数是可动态增长的,根据任务的数量动态的增加线程的数量,但是会设置一个线程数量的阈值(线程过多的坏处上
面已经讲过了),任务处理完成,如果动态增长的线程空闲了60s还没有处理其它任务,那么关闭线程,保持池中最初数量的线程即可。
线程间的互斥
如果两个及多个线程访问同一个资源,根据CPU的调度,可能会出现不同的结果,为了让同一个时刻只有一个线程能访问资源,我们首先看看C++中的mutex。
传统的互斥锁 mutex
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥量
void print_block(int n, char c) {
mtx.lock(); // 请求互斥量
for (int i = 0; i < n; ++i) {
std::cout << c;
}
std::cout << '\n';
mtx.unlock(); // 释放互斥量
}
int main() {
std::thread th1(print_block, 50, '*');
std::thread th2(print_block, 50, '$');
th1.join();
th2.join();
return 0;
}
这样虽然可以保证同一时刻只有一个线程可以访问,但是
**mtx.lock(); // 请求互斥量 **
...
** mtx.unlock(); // 释放互斥量 **
如果中间发生了异常,unlock()就无法进行调用,就会陷入死锁机制。
C++中的对象lock_guard
std::lock_guard<std::mutex> lock(mtx);
std::lock_guard 是 C++11 引入的一个类模板,用于简化互斥锁(mutex)的管理。它提供了一种自动锁定和解锁互斥锁的机制,从而减少了由于忘记解锁或异常导致的死锁风险。
std::lock_guard 的使用非常直接。当你创建一个 std::lock_guard 对象时,它会尝试锁定关联的互斥锁。当 std::lock_guard 对象离开其作用域或被销毁时,它会自动解锁关联的互斥锁。
代码介绍:
#include <thread>
#include <atomic>
#include <iostream>
#include <list>
#include <mutex>
int ticketCount = 100;
std::mutex mtx; //创建全局互斥锁
void service(int index)
{
while (ticketCount > 0)
{
{
std::lock_guard<std::mutex> lock(mtx); //使用互斥锁
if (ticketCount > 0)
{
std::cout << "第" << index << "线程,卖出" << ticketCount << "张票\n";
ticketCount--;
}
}//出了这个作用域就会调用析构函数
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
std::list<std::thread> tlist;
for (int i = 1; i <= 10; ++i)
tlist.push_back(std::thread(service, i));
for (auto& t : tlist)
t.join();
}
线程之间的通信
std::lock_guardstd::mutex 虽然用起来很方便,但是无法解决线程通信的问题。
线程通信
是指在多线程编程中,不同的线程之间需要进行信息交换或同步协作的过程。由于每个线程都有自己的执行栈和局部变量,它们不能直接访问其他线程的内存空间,因此需要通过一些机制来实现线程之间的通信。
线程通信的主要目的包括:
-
数据共享:多个线程可能需要访问和修改共享的数据结构或资源。线程通信机制确保这些操作能够正确同步,避免数据竞争和不一致。
-
同步协作:线程之间可能需要按照一定的顺序执行操作,或者等待其他线程完成某个任务后再继续执行。同步机制(如互斥锁、条件变量、信号量等)可以帮助实现这种协作。
-
消息传递:一个线程可能需要向另一个线程发送消息或信号,以通知它进行某种操作或响应某个事件。消息队列、管道、套接字等机制可以用于线程间的消息传递。
-
任务划分与合并:线程可以将任务划分为更小的子任务,并在不同的线程上并行执行。完成后,这些线程需要合并结果或进行后续操作。
线程通信是多线程编程中的一个重要概念,它对于确保程序的正确性和性能至关重要。正确的线程通信可以避免竞态条件、死锁和其他并发问题,从而实现高效、可靠的并发执行。
举个例子:比如说经典的生产者消费者问题,在同一个资源中 ,如果该资源为空,生产者模块就会生成新的,当资源>0,消费者模块就会消费掉一个资源。
而不正确的线程通信中,资源为空的时候去一直消费,或者资源>0还在一直生产,就会导致死锁问题。
这个时候我们使用C++的新对象来解决问题,先看看代码:
#include <thread>
#include <atomic>
#include <iostream>
#include <list>
#include <mutex>
#include <queue>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
class Quque {
public:
void put(int val)
{
//std::lock_guard<std::mutex> lckg(mtx);
std::unique_lock<std::mutex> ulckg(mtx);
while (!que.empty()) //不为空就停止,等取出了再继续
{
cv.wait(ulckg);
}
que.push(val);
std::cout << "生产者 生产:" << val << "号物品" << std::endl;
cv.notify_all();
}
int get()
{
//std::lock_guard<std::mutex> lckg(mtx);
std::unique_lock<std::mutex> ulckg(mtx);
while (que.empty())
cv.wait(ulckg);
int val = que.front();
que.pop();
cv.notify_all();
std::cout << "生产者 消费:" << val << "号物品" << std::endl;
return val;
}
private:
std::queue<int> que;
};
//生产者
void producer(Quque* que)
{
for (int i = 1; i < 11; ++i)
{
que->put(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
//消费者
void consumer(Quque* que)
{
for (int i = 1; i < 11; ++i)
{
que->get();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
Quque q;
std::thread t1(producer, &q);
std::thread t2(consumer, &q);
t1.join();
t2.join();
return 0;
}
输出内容
生产者 生产:1号物品
生产者 消费:1号物品
生产者 生产:2号物品
生产者 消费:2号物品
生产者 生产:3号物品
生产者 消费:3号物品
生产者 生产:4号物品
生产者 消费:4号物品
生产者 生产:5号物品
生产者 消费:5号物品
生产者 生产:6号物品
生产者 消费:6号物品
生产者 生产:7号物品
生产者 消费:7号物品
生产者 生产:8号物品
生产者 消费:8号物品
生产者 生产:9号物品
生产者 消费:9号物品
生产者 生产:10号物品
生产者 消费:10号物品
首先是里面的std::unique_lockstd::mutex ulckg(mtx); 和std::condition_variable cv;
std::condition_variable
是 C++11 引入的一个类,用于支持线程间的条件同步。它常常与互斥锁(std::mutex
)一起使用,以实现一个或多个线程等待某个条件成立,而另一个线程在条件成立时通知等待的线程。
以下是 std::condition_variable
的主要作用和使用场景:
-
等待条件成立:
线程可以使用std::condition_variable
的wait()
方法进入等待状态,直到另一个线程通过notify_one()
或notify_all()
方法发出通知。wait()
方法会自动解锁关联的互斥锁,使等待的线程能够进入睡眠状态。当通知到来时,wait()
会重新锁定互斥锁并返回,这样线程可以检查条件是否已满足。 -
通知等待线程:
当某个条件满足时(例如,某个共享资源已经准备好或被修改),一个线程可以使用notify_one()
或notify_all()
方法来唤醒一个或所有等待在std::condition_variable
上的线程。 -
线程间协作:
std::condition_variable
常常用于生产者-消费者问题、多线程任务队列、线程池管理等场景中,以实现线程间的协作和同步。 -
避免虚假唤醒:
由于操作系统调度的原因,线程可能会被“虚假唤醒”(即在没有收到通知的情况下醒来)。std::condition_variable
的wait()
方法考虑到了这一点,因此通常与互斥锁和条件检查一起使用,以确保线程在继续执行前确实收到了通知,并且条件已经满足。
而在上面代码中cv.wait(ulckg);表示了在条件下进入等待状态,除非收到cv.notify_all();并且mutex已经被unlock,才会继续进行线程运行。
而std::unique_lock
相比std::lock_guard
提供了更多的灵活性
,因为它允许延迟锁定
、手动控制锁定
和解锁、条件等待以及所有权转移
。这使得std::unique_lock在需要更精细控制锁定时非常有用,比如在需要响应中断
或异常处理
时。
此外,std::unique_lock
还可以与std::defer_lock
、std::try_to_lock
和std::adopt_lock
标签配合使用,以在构造时指定不同的锁定行为。例如,使用std::defer_lock
标签可以在构造时不锁定互斥锁,稍后再通过调用lock()方法来锁定。