c++多线程

多线程的概念就不需要多说了,多线程的主要难点在于争用条件,撕裂,死锁,和伪共享

争用条件很简单,也就是多个线程要访问共享资源。比如一个线程读取了内存的数据10,它将其增加了1变成了11,正当他写入到内存的时候,另一个线程登场,它读取了内存的数据,此时内存的数据还是10,它将其减少了1,然后写入到内存,内存现在的数据是9,之前的线程来了,它也要写入自己的数据11。这就偏离我们的目的。

其他的情况都类似,撕裂就是可能在读取文件的时候,因为多线程的影响只读取了一部分,另一部分被其他线程修改了,文件变成了四不像,缝合怪。死锁也是,就是一个线程进去访问资源,但是因为某些原因,比如没有权限,就阻塞在里面,因为外面加了锁,它不离开这个加锁的区域,也就没办法解锁,所以其他线程也被阻塞在外面,最终死锁了。伪共享是因为缓存行被多个线程同时使用了,这样修改的时候出现了交叉干扰,影响并发性能,java里面一般是字节填充

1.创建线程。

c++的线程对象允许传入多个参数,这和Linux不一样

#include 
#include  

void counter(int id, int numIteration)
{
	for(int i = 0; i < numIteration; i++)
	{
		std::cout << "Counter " << id << " has value " << i << std::endl; 
	}
}

int main(int argc, char** argv) {
	
	std::thread t1{counter, 1, 5};
	std::thread t2{counter, 2, 8};
	
	t1.join();
	t2.join();
	return 0;
}

对于c++创建线程,要习惯函数方法和函数对象方法,但是如果使用函数对象,需要实现operator(),特殊的调用方法还有lambda和成员函数

lambda的创建方法如下:

#include 
#include  


int main(int argc, char** argv) {

	int id{1};
	int num{5};
	std::thread t1{
		[id, num] {for(int i = 0; i < num; ++i)
		{
			std::cout << "Counter " << id << "has value " << i << std::endl;
		}
	}};
	t1.join();
	return 0;
}

通过成员函数:

#include 
#include  

class Counter{
public:
	Counter(int a, int b){
		id = a;
		num = b;
	}
	
	void fun(){
		for(int i{0}; i < num; ++i)
			std::cout << id << ":" << i << std::endl;
		
	}
	
private:
	int id;
	int num;
};
int main(int argc, char** argv) {

	Counter c{10, 5};
	std::thread t1{&Counter::fun, &c};
	t1.join();
	return 0;
}

2.线程本地储存

对于一个全局的共享变量,不同的线程访问会导致这个变量发生变化,而c++创造了一种新的变量thread_local,可以将任何变量标记为线程本地数据

#include 
#include  

int k = 0;
thread_local int n;


void fun(int id)
{
	std::cout << "id = " << id << " k = " << k++ << " n = " << n++ << std::endl;
}
int main(int argc, char** argv) {

	std::thread t1{fun, 1};
	std::thread t2{fun, 2};
	t1.join();
	t2.join();
	return 0;
}

输出结果可以发现k发生了变化,但是n没有,这是因为n是每一个线程本地的,互不相干,但是k是共有的。

3.取消线程

c++11里面实际上没有专门用来取消线程的方法,而c++20引进了jthread,在没有特定的线程库可使用的情况下,建议使用原子量和条件变量

4.异常处理

多线程的异常处理用下面的函数展示

#include 
#include  
#include
#include 
//抛出异常的函数 
void doSomeWork(){
	for(int i{0}; i < 5; ++i)
	{
		std::cout<< i << std::endl;
	}
	std::cout << "抛出一个runtime_error异常" << std::endl;
	throw std::runtime_error {"Exception from thread"};
}

//线程引用的函数 
void threadFunc(std::exception_ptr& err)
{
	try{
		doSomeWork();
	}catch(...){
		std::cout << "捕捉到异常"<

这个程序很简单,但是展示了抛出异常的处理过程

doWorkInthread函数创建一个线程,然后运行一个函数threadFunc,这个函数会调用doSomeWork,然后抛出一个异常,异常在threadFunc中获取,并被error给接受了,这是一个引用类型,最终在threadFunc中获取,传递给了主函数。

几个关键的函数:

(1).exception_ptr current_exception()

这个函数会获取当前正常处理的异常,要用在catch块中,如果异常存在,则返回exception_ptr对象,不存在则返回空指针

(2). void rethrow_exception(exception_ptr p)

重新抛出exception_ptr的异常

(3) template exception_ptr make_exception_ptr(E e)

这个是创建一个exception的对象,在需要的时候被自动调用,一般不会显式的被使用。

5.原子操作库

原子类型的数据不需要关注同步机制。

原子类型定义在中,各种基本类型都已经被定义,比如int的原子型为:atomic_int

原子模板可以创建原子类型的变量,但是如果硬件不支持,使用原子模板则会出问题,有些硬件使用锁机制来实现原子,使用is_lock_free()检查是否支持无锁操作。关于原子操作可以去看专门的文章。原子操作的一个好处是不用费劲心思弄同步。

创建一个程序,将a每次加100,这个程序执行时间有点长,所以必然会发生线程竞争

比如下面这种情况,会导致输出不一定为1000

#include 
#include  
#include
#include
#include

using namespace std;

//对a加100
void add(int& a)
{
	for(int i{0}; i < 100; ++i)
		++a;
}

//创建十个线程一起加 
int main(){
	
	int a = 0;
	vector t;
	for(int i{0}; i < 10; i++)
		t.push_back(thread{add, std::ref(a)});
	for(int i{0}; i < 10; i++)
		t[i].join();
	cout << a << endl;//本来应该输出1000 
	

	return 0;
}

引入原子量之后,就可以得到预期结果

#include 
#include  
#include
#include
#include

using namespace std;

//对a加100
void add(atomic_int& a)
{
	for(int i{0}; i < 100; ++i)
		++a;
}

//创建十个线程一起加 
int main(){
	
	atomic_int a{0};
	vector t;
	for(int i{0}; i < 10; i++)
		t.push_back(thread{add, std::ref(a)});
	for(int i{0}; i < 10; i++)
		t[i].join();
	
	cout << a << endl;//本来应该输出1000 
	

	cout << t.size() << endl;
	return 0;
}

线程等待原子变量(c++20)

有时候需要线程阻塞在某个地方,然后再某个条件发生之后重新苏醒,这个时候就可以使用下面的特性:

wait(oldValue) 阻塞线程,知道其他线程调用notify_one或者notify_all,告知原子变量已经被改变了

notify_one,通知一个线程变量已经改变

notify_all,通知所有线程变量改变

6.互斥

1.自旋锁

#include
#include
#include
#include
#include
using namespace std;

atomic_flag spinlock = ATOMIC_FLAG_INIT;
static const int n{50};
static const int loop{100};

void dowork(int threadNumber, vector& data){
	for(int i{0}; i < loop; ++i){
		while(spinlock.test_and_set());//线程获取锁,在这里忙等待
		data.push_dack(threadNumber);//记录线程编号 
		spinlock.clear();//释放锁 
	}
}

int main(){
	vector threads;
    vector data;
	for(int i{0}; i < n; ++i){
		threads.push_back(thread{dowork, i ,ref(data)});
	}
	
	for(auto& t : threads){
		t.join();
	}
	cout<< data.size() << " " << n * loop<

这里的自旋锁是使用atomic_flag实现,所有的线程在test_and_set试图获取锁,clear会解开锁

2.非定时互斥体

这种锁有三个,分别是mutex,recursive_mutex,shared_mutex。前两个类在中定义,后者在中定义,每个类都支持下面方法:

lock():调用线程将尝试获取锁

try_lock():线程会尝试调用锁,如果获取成功,则返回true,否则返回false

unlock();释放获取的锁

mutex是独占锁,最能锁一次,如果锁两次,就有点麻烦了。

recursive_metux可以锁多次,解开锁的时候,要使用同样多次数的unlock()

shared_mutex是一种读写锁,独占所有权是写锁,共享所有权是读锁,它有类似的方法:

lock_shared(),try_lock_shared(),unlock_shared()

4.定时的互斥体类

和上面提到的差不多,只是有时间限制,这里不多讲

7.锁

锁也有不同的类型主要有以下三种:(1)lock_guard(),(2)unique_lock()(3)shared_lock()

他们都有多种构造函数,以互斥量和延迟时间为参数,获取互斥量的锁。

另外还有两种获取多个锁的对象,lock()和scoped_lock()

8.call_once和once_flag

结合call_once和once_flag保证函数只被调用一次,即便是不同的线程,也只会接续进行而已。这个其实是c++20的特性

你可能感兴趣的:(开发语言,c++)