C++重要知识清单:多线程基础

0.前言

并发编程的两种模型:

1.多进程

每个进程只是一个线程,进程间可以相互通信;进程通信的方式有:文件、管道、消息队列

2.多线程

一个进程有多个线程,线程间通过共享内存的方式进行通信。

3.优缺点:

  • 相对于进程来说,线程启动速度很快,因为系统会分配一系列的内部资源来管理进程,所以线程更轻量级。
  • 线程开销更低,进程开销更大;操作系统要对进程提供很多的保护,而且通过共享内存的通信方式要比通过文件、管道、消息队列的方式速度快很多。所以多线程要比多进程性能优越
  • 但是多线程很难管理,有很多多线程的特定问题需要处理
  • 多线程不能在分布式系统下运行,而多进程则没有这个问题

由于并发编程是一个很深的问题,这篇博客先记录一些多线程基础的内容,后面会针对每一个核心的子内存单独更新学习笔记

1.核心对象和函数

1.1thread

创建一个线程有多种方式

方式1:通过函数构造

void function_1() {
	cout << "www.lalal.com" << endl;
}

int main() {
	thread t1(function_1);
	//主线程会等待t1线程的执行
	t1.join();
	//各自不关联的执行
	t1.detach();
	//detach后进行join是会报错的,可以进行判断能否join来做安全保护所以要判断
	if (t1.joinable()) {
		t1.join();
	}
	return 0;
}

方式2:通过任何可被调用的对象构造

class Factor {
public:
	void operator()() {
		for (int i = 0; i > -10; i--) {
			cout << "from t1:" << i << endl;
		}
	}
	void operator()(string& msg) {
		for (int i = 0; i > -10; i--) {
			cout << "from t1:" << i << endl;
		}
	}
};

int main() {
	
	Factor fct;
	thread t1(fct);
	//注意括号;两种方式等效
	thread t2((Factor()));
	string s = "asd";
	thread t3((Factor()),move(s));

	//如果不做异常处理,当主线程中发生异常时,t1线程会被直接销毁
	try
	{
		for (int i = 0; i < 100; i++) {
			cout << "from main:" << i << endl;
		}
	}
	catch (...)
	{
		t1.join();
		throw;
	}

	t1.join();
	return 0;
}

1.2 线程id

异步时不一定准确

	//输出当前线程(主线程)id
	cout << this_thread::get_id() << endl;
	//输出t2线程id
	cout << t2.get_id() << endl;

在实际应用中,对于一个复杂的问题,我们要创建多少个线程来解决呢?

这需要由CPU的核心数来决定,如果超出了CPU线程得限制反而会导致很多转换操作影响性能。

	//支持得并发线程数
	thread::hardware_concurrency();

1.3线程阻塞相关

yield :把CPU当前所有权返回给系统

sleep_for:阻塞至少时间,期间会让开执行权,时间到后会进入调度队列。所以等待时间为阻塞时间+等待时间

sleep_until:阻塞当前线程,直至抵达指定时间。期间不会让开执行权,一直忙等。

 

2.数据竞争与互斥对象

进行以下代码测试:

我们在主线程和t1线程中同时打印信息

void function_1() {
	for (int i = 0; i > -10; i--) {
		cout << "From t1:" << i << endl;
	}
}

int main() {
	thread t1(function_1);
	for (int i = 0; i < 10; i++) {
		cout << "From main:" << i << endl;
	}
	t1.join();
	return 0;
}

可以发现,执行的过程中由于两个线程在为同一个资源cout竞争,所以输出很混乱。

2.1使用互斥对象同步资源

我们使用一个互斥锁来解决上面的问题,修改为如下代码:

#include"pch.h"
#include<iostream>
#include<thread>
#include<string>
#include<mutex>
using namespace std;
//创建互斥对象
mutex mut;

void shared_print(const string msg, int id) {
	mut.lock();
	cout << msg <<id<< endl;
	mut.unlock();
}

void function_1() {
	for (int i = 0; i > -10; i--) {
		shared_print("From t1:", i);
		
	}
}

int main() {
	thread t1(function_1);
	for (int i = 0; i < 10; i++) {
		shared_print("From main:", i);
	}
	t1.join();
	return 0;
}

可以看到,现在不会出现打印混乱的情况了。

但这个代码仍然有一个致命的问题,就是当执行完mu.lock();后,程序出现了异常,那这个互斥锁将永远处于锁住的状态

所以不推荐使用  lock()和 unlock()。可以使用 lock_guard<mutex>来解决这个问题

void shared_print(const string msg, int id) {
	//有异常出现时会永远锁住,不推荐使用
	//mut.lock();
	//当guard对象析构时,不管有没有异常发生,都会对mut自动解锁
	lock_guard<mutex> guard(mut);
	cout << msg <<id<< endl;
	//mut.unlock();
}

但异常情景下的问题虽然解决了,这个程序仍然是有问题的。由于cout是一个全局变量,所以cout并没有完全在mut的保护之下。其他的线程仍然可以在不加锁的情况下直接使用cout。所以需要更好的方法来解决这个问题。为了更好的保护资源,必须将互斥对象与资源进行绑定!

创建一个类来进行竞争资源的管理

#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<fstream>
using namespace std;
//创建互斥对象
mutex mut;

class LofFile {
public:
	LofFile() {
		f.open("log.txt");
	}
	void shared_print(string id, int value) {
		lock_guard<mutex> locker(m_mutex);
		f << "From" << id << ":" << value << endl;
	}
	~LofFile()
	{
		f.close();
	}
protected:
private:
	mutex m_mutex;
	std::ofstream f;
};


void function_1(LofFile& log) {
	for (int i = 0; i > -10; i--) {
		log.shared_print("From t1", i);
	}
}

int main() {
	LofFile log;
	thread t1(function_1,ref(log));
	for (int i = 0; i < 10; i++) {
		log.shared_print("From main:", i);
	}
	t1.join();
	return 0;
}

3.死锁

下面的代码由于申请锁和释放锁的顺序在两个线程中发生了冲突,所以会导致死锁

//创建互斥对象
mutex mut;

class LofFile {
public:
	LofFile() {
		//f.open("log.txt");
	}
	void shared_print(string id, int value) {
		lock_guard<mutex> locker(m_mutex1);
		lock_guard<mutex> locker2(m_mutex2);
		cout << "From" << id << ":" << value << endl;
	}
	void shared_print2(string id, int value) {
		lock_guard<mutex> locker2(m_mutex2);
		lock_guard<mutex> locker(m_mutex1);
		cout << "From" << id << ":" << value << endl;
	}
	~LofFile()
	{
		//f.close();
	}
protected:
private:
	mutex m_mutex1;
	mutex m_mutex2;
	//std::ofstream f;
};


void function_1(LofFile& log) {
	for (int i = 0; i > -100; i--) {
		log.shared_print("From t1", i);
	}
}

int main() {
	LofFile log;
	thread t1(function_1,ref(log));
	for (int i = 0; i < 100; i++) {
		log.shared_print2("From main:", i);
	}
	t1.join();
	return 0;
}

如何避免死锁:

  • 评估程序是否需要两个以上的mutex
  • 避免在锁住mutex的同时去使用一些陌生的函数
  • 如果必须同时锁住两个或以上的mutex,那么使用std::lock(mutex1,mutex2);去同时锁住,编译器会确保锁住的顺序不会有问题

4.UniqueLock与Initialization

4.1 unique_lock

uniqye_lock可以被移动,lock_guard不能被移动

unique_lock可以用于对特定的代码块加锁,如下述代码:

	void shared_print(string id, int value) {
		std::unique_lock<std::mutex> locker(m_mutex1);
		cout << "From" << id << ":" << value << endl;
		locker.lock();
		cout << "被锁住内容" << endl; 
		locker.unlock();
		cout << "没被锁住的内容" << endl;
		locker.lock();
		cout << "又被锁住了!!" << endl;
		locker.unlock();
		//支持移动语义
		unique_lock<mutex> locker2 = move(locker);
	}

unique_lock<>比lock_guard多了很多功能,但是带来方便的同时也带来了性能的损耗;如果更关注性能并且不需要这么多操作就不要使用unique_lock;

4.2 initialization

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值