【C++11新特性】多线程

目录

一、thread类

二、互斥锁

1.四种锁

(1)mutex

(2)recursive_mutex

(3)time_mutex

(4)recurive_timed_mutex

2.lock_guard

3.unique_lock

4.锁的原理

三、原子操作

四、简单的线程池

五、条件变量


一、thread类

在C++11之前,涉及多线程问题,都是和平台相关的,比如windows系统下和linux系统下都有各自的接口,这使得代码的可移植性比较差。C++11最重要的特性就是支持了多线程。使得C++在并行编程时不需要依赖第三方库(可以跨平台使用)。并且在原子操作中引入了原子类的概念,要使用标准库中的线程,必须包含头文件thread。

函数名功能
thread()构造一个线程对象,没有任何关联的线程函数,即没有启动任何线程
thread(fn,args,args2,...)构造一个线程对象,并关联线程函数fn,args1,args2...为线程函数的参数
get_id()获取线程id
joinable()线程是否正在执行,joinable代表一个正在执行的线程
join()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象之后会马上调用,用于把创建线程与线程对象分开,分离的线程变为后台线程,创建的线程的死活与主线程无关

1.线程对象可以关联一个线程,用来控制线程和获取线程的状态。

2.当创建一个线程之后,没有提供任何线程函数,该对象没有对应的任何线程。

下面来写一个简单的创建线程的小程序:

void Func(int n)
{
	cout << n << endl;
}
int main()
{
	thread();//创建一个空线程,什么都不做
	thread t1(Func, 10);
	thread t2([](int n) { cout << n << endl; }, 20);
	t1.join();
	t2.join();
}

有一个细节需要注意,那就是thread在向函数传参的时候,不能使用左值引用进行传参:

void Func(int& n)//用引用接收会发生错误
{
	cout << n << endl;
}
int a=10;
thread t1(Func,a);

这里引入了一个ref函数来满足这一操作。即:

thread t1(Func,ref(a));

二、互斥锁

在C++11中,Mutex包含了四种互斥量的种类:

1.四种锁

(1)mutex

函数名函数功能
lock()上锁
unlock()解锁
try_lock()

非阻塞获取锁

当线程函数调用lock()的时候,有以下三种情况:

(1)锁没有被取走,直接获取锁。

(2)当前线程已经有该锁了,形成死锁。

(3)锁被其他线程取走,进行阻塞等待。

当函数调用try_lock的时候,有以下三种情况:

(1)锁没有被取走,直接获取锁。

(2)该线程已经有该锁了,产生死锁。

(3)锁被其他线程取走,会返回false,而不是被阻塞。

(2)recursive_mutex

允许递归上锁,来过得互斥对象的多层所有权,在释放互斥量的时候也需要采用等量的unlock来进行解锁。

(3)time_mutex

比mutex多了两个成员函数.

函数名函数功能
try_lock_for()接受一个时间范围,表示在一段时间之内,线程如果没有获得锁则被阻塞住,如果此期间其他线程释放了锁,则该线程可以获得锁,如果超时,返回false
try_lock_util()接受一个时间点作为参数,在指定时间未到来之前,线程如果没有获得锁则被阻塞住,如果此期间其他线程释放了锁,则该线程可以获得锁,如果超时返回false

(4)recurive_timed_mutex

使用的比较少,这里不多介绍。

2.lock_guard

lock_guard是C++11中定义的模板类。主要通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁的问题。
缺陷:太单一,用户没有办法对锁进行控制,因此C++11又提供了unique_lock。

3.unique_lock

与lock_gard类似 ,unique_lock类模板也是采用RAII的方式进行封装,并且也是独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。

在构造时,unique_lock对象需要传入一个Mutex对象作为它的参数,新创建的unique_lock对象负责传入的Mutex对象的上锁和解锁的操作。使用以上类型互斥量实例化unique_lock对象的时候,自动调用构造函数上锁,unique_lock更加的灵活,提供了很多成员函数。

上锁、解锁:lock,try_lock,try_lock_for,try_lock_util和unlock

修改操作:移动赋值,交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权),释放(release:返回当前unique_lock所管理的互斥量的指针)

获取属性:owns_lock(返回当前对象是否上来锁),operator>bool(与owns_lock()的功能向同),mutex(返回当前unique_lock所管理的互斥量的指针)

4.锁的原理

我们都知道,锁是来控制信号量的,防止两个线程对信号量进行混乱的修改。

static int x = 0;
void Func1(int n)
{
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
}
void Func2(int n)
{
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
}
int main()
{
	thread t1(Func1, 10);
	thread t2(Func2, 10);
	t1.join();
	t2.join();
	return 0;
}

在这段代码中,t1和t2两个线程对同一个信号量进行++操作,由于底层的++操作不是原子的,可能会导致x的数值混乱,因为对于一个++操作来说,它底层的汇编大概会分为三步,分别是ld,++,sd

假设当一个线程该执行完读入和x++后,被切走了,第二个线程读入并执行++多次,然后写回。第一个线程时间片再到来的时候,会带着它的上下文数据,发现改进行++了,就进行++操作,然后写回。这就导致线程2做的工作全白做了。因此会造成混乱。

因此需要引入锁这一现象:

当线程1到来时,抢到锁之后会将a1寄存器的值(初值为0)与内存中锁的值进行交换,当时间片结束之后,带着它的上下文数据离开。当线程2到来的时候也会和内存中的mutex的值进行交换,只不过此时mutex的值是0,最终线程2的a1寄存器也为0,因此就可以通过a1寄存器的值来判断谁拿到了锁,从而让它对临界资源进行修改。

那么问题来了,我们应该在循环里面进行枷锁操作还是在循环外面进行加锁操作呢?

如果在循环外枷锁,就相当于两个线程串行运行了,降低了效率。但如果加在里面虽然是并行运行,这样频繁的加锁解锁是需要消耗资源的。

这里我们选择加在循环的外面,因为++执行的太快了,不适合频繁的加锁解锁。

void Func1(int n)
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
	mtx.unlock();
}

三、原子操作

C++将原子操作也封装成了一个对象:

原子类型支持多个原子操作的函数:

可以将上文中的x定义为原子类型,表示的是一条汇编语句就执行了他的++操作。

atomic<int> x = 0;
void Func1(int n)
{
	//mtx.lock();
	for (int i = 0; i < n; i++)
	{
		cout << this_thread::get_id() << "->" << x << endl;
		++x;
	}
	//mtx.unlock();
}

这样书写和加锁的操作是一样的

四、简单的线程池

int main()
{
	atomic<int>x = 0;
	int N, M;
	cin >> N, M;
	vector<thread> vthds;
	vthds.resize(N);
	for (int i = 0; i < N; i++)
	{
		vthds[i] = thread([M,&x]
			{
				for (int i = 0; i < M; i++)
				{
					++x;
				}
			}
		);
	}
	for (auto& e : vthds)
	{
		cout << x << endl;
		e.join();
	}
	return 0;
}

我们可以将vector的每一个元素类型都设为thread类型,在循环调用,循环等待,就可以完成线程池的工作了。

五、条件变量

假如我们设计一个程序,让线程1和线程2交替进行打印,线程1打印奇数,线程2打印偶数。很容易想到使用加锁操作进行解决:

int main()
{
	int n = 100;
	int i = 0;
	mutex mtx;
	thread t1([n, &i, &mtx]
	{
		while (i < n)
		{
			mtx.lock();
			cout << this_thread::get_id() << "->" << i << endl;
			++i;
			mtx.unlock();
		}
	});
	thread t2([n, &i, &mtx]
		{
			while (i < n)
			{
				mtx.lock();
				cout << this_thread::get_id() << "->" << i << endl;
				++i;
				mtx.unlock();
			}
		});
	t1.join();
	t2.join();
	return 0;
}

当运行这段代码的时候很快就能发现问题:我们无法控制两个线程交替枪锁,在大部分的时候都是第一个线程抢到了锁。

这就无法满足我们交替进行打印的条件。这是因为当线程1抢到锁的时候线程2被阻塞住了,那么如何控制两个线程进行交替执行呢?

此时就需要引入条件变量:condition_variable,它提供了几个函数供我们选择:

函数作用
wait阻塞,直到被notify
wait_for最多等待多长时间
notify_one唤醒一个线程
notify_all唤醒所有线程
void wait(unique_lock<mutex>& lck);
void wait(unique_lock<mutex>& lck, Predicate pred);

注意当某个线程调用了wait函数的时候,会调用unlock()释放锁,一旦被notify了,会立刻调用lock()获取锁。因此调用wait的时候需要传入锁。对于第二方式来说第二个参数表示的是一个标记,只有当prep返回值为false的时候才会发生阻塞,相当于

while(!prep())
	wait(lock);

对于notify_one函数来说,当有线程在该条件变量上阻塞的时候,会通知其开始抢锁,当没有线程在条件变量上阻塞的时候,什么都不会做。

我们可以使用条件变量的等待-通知机制来完成两个线程的交替执行:

int n = 100;
int i = 0;
mutex mtx;
condition_variable cv;
bool flag = false;
thread t1([&n, &i, &mtx, &cv, &flag]
	{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]() {return flag; });
			cout << this_thread::get_id() << "->" << i << endl;
			++i;
			flag = false;
			cv.notify_one();
		}
	}
);
thread t2([&n, &i, &mtx,&cv,&flag]
	{
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]() {return !flag; });
			cout << this_thread::get_id() << "->" << i << endl;
			++i;
			flag = true;
			cv.notify_one();
		}
	});
t1.join();
t2.join();

注意,添加条件变量调用wait的时候,需要将锁进行封装成unique_lock类型,该类型会在创建时调用构造函数自动上锁,在销毁的时候会调用析构函数自动解锁。

分析这段代码,线程1首先获取flag,上锁,wait的第二个变量是false,此时线程1在条件变量下发生阻塞等待;执行到线程2,上锁后,线程2第二个变量是true,继续执行++i,flag设为true,notify_one()唤醒一个线程。

唤醒线程2,阻塞等待,唤醒线程1,继续执行++i,flag设为false,接着唤醒,以此类推。

此时的运行结果是:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值