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 虽然用起来很方便,但是无法解决线程通信的问题

线程通信

是指在多线程编程中,不同的线程之间需要进行信息交换同步协作的过程。由于每个线程都有自己的执行栈和局部变量,它们不能直接访问其他线程的内存空间,因此需要通过一些机制来实现线程之间的通信。

线程通信的主要目的包括:

  1. 数据共享:多个线程可能需要访问和修改共享的数据结构或资源。线程通信机制确保这些操作能够正确同步,避免数据竞争和不一致。

  2. 同步协作:线程之间可能需要按照一定的顺序执行操作,或者等待其他线程完成某个任务后再继续执行。同步机制(如互斥锁、条件变量、信号量等)可以帮助实现这种协作。

  3. 消息传递:一个线程可能需要向另一个线程发送消息或信号,以通知它进行某种操作或响应某个事件。消息队列、管道、套接字等机制可以用于线程间的消息传递。

  4. 任务划分与合并:线程可以将任务划分为更小的子任务,并在不同的线程上并行执行。完成后,这些线程需要合并结果或进行后续操作。

线程通信是多线程编程中的一个重要概念,它对于确保程序的正确性和性能至关重要。正确的线程通信可以避免竞态条件、死锁和其他并发问题,从而实现高效、可靠的并发执行。

举个例子:比如说经典的生产者消费者问题,在同一个资源中 ,如果该资源为空,生产者模块就会生成新的,当资源>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 的主要作用和使用场景:

  1. 等待条件成立
    线程可以使用 std::condition_variable 的 wait() 方法进入等待状态,直到另一个线程通过 notify_one() 或 notify_all() 方法发出通知。wait() 方法会自动解锁关联的互斥锁,使等待的线程能够进入睡眠状态。当通知到来时,wait() 会重新锁定互斥锁并返回,这样线程可以检查条件是否已满足。

  2. 通知等待线程
    当某个条件满足时(例如,某个共享资源已经准备好或被修改),一个线程可以使用 notify_one() 或 notify_all() 方法来唤醒一个或所有等待在 std::condition_variable 上的线程。

  3. 线程间协作
    std::condition_variable 常常用于生产者-消费者问题、多线程任务队列、线程池管理等场景中,以实现线程间的协作和同步。

  4. 避免虚假唤醒
    由于操作系统调度的原因,线程可能会被“虚假唤醒”(即在没有收到通知的情况下醒来)。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_lockstd::try_to_lockstd::adopt_lock标签配合使用,以在构造时指定不同的锁定行为。例如,使用std::defer_lock标签可以在构造时不锁定互斥锁,稍后再通过调用lock()方法来锁定。

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值