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.
全部评论

相关推荐

点赞 评论 收藏
分享
评论
3
5
分享
牛客网
牛客企业服务