【C++11常见新特性(三)】线程库

thread类

C++11新特性支持线程,使得C++在并行编程中不需要使用第三方库,并且在原子操作中还引入了原子类的概念。要使用标准库中的thread需要包含头文件thread点击查看thread类
下面介绍thread类常用的成员函数:
在这里插入图片描述
通过thread对象,我们可以使其关联一个线程用来控制线程和获取线程的状态。

当我们创建了一个thread对象,并且分配给该线程一个函数,该线程就会运行该函数,与主线程一起运行。具体地,线程函数一般情况下可按照以下三种方式提供:

  • 函数指针
  • lamdba表达式
  • 函数对象

下面给出启动线程的样例:

#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
 cout << "Thread1" << a << endl;
}
class TF
{
public:
 void operator()()
 {
 cout << "Thread3" << endl;
 }
};
int main()
{
    // 线程函数为函数指针
 thread t1(ThreadFunc, 10);
    
    // 线程函数为lambda表达式
 thread t2([]{cout << "Thread2" << endl; });
    
    // 线程函数为函数对象
    TF tf;
 thread t3(tf);
    
 t1.join();
 t2.join();
 t3.join();
 cout << "Main thread!" << endl;
 return 0;
}

在分配给线程函数的时候,线程才开始正式启动。

关于thread类的其它特性:

  • thread类是 不允许拷贝的,但是可以移动构造和移动赋值。这一点查看C++手册中关于thread类的成员函数声明就可以看到:
    -
    在这里插入图片描述
  • 可以采用joinable()函数判断该线程是否有效,如果是以下任何情况,线程无效(这里的无效是指线程没有运行函数):
    • 采用无参构造函数构造的线程对象,因为该对象并未关联真正的线程
    • 线程对象的状态已经转移给其它线程对象,比如移动赋值给了另一个thread对象
    • 线程已经detach分离或者join结束

线程函数参数

观察下面代码:

#include<iostream>
#include <thread>
using namespace std;
void ThreadFunc1(int& x)
{
	x += 10;
}
int main()
{
	int a = 10;
	thread t1(ThreadFunc1, a);
	t1.join();
	cout << a << endl;
	return 0;
}

上面代码运行发现失败了
在这里插入图片描述
对于上面代码的函数ThreadFunc1,创建一个线程去运行失败了。但是主线程调用却没有问题
在这里插入图片描述
为什么会这样呢?
原因在于,thread 并不会自动将传递的参数转换为引用,这导致 ThreadFunc1 在运行时没有接收到引用,从而产生编译错误或运行时错误。也就是说,thread构造函数并不会区分被传递的函数参数a是否是一个引用类型。我们可以使用ref函数来显示传递引用,比如:
在这里插入图片描述
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数也可以使用bind和function包装器提前绑定this参数,这一点在之前的博客中有介绍。

并行与并发的区别

  • 概念上来看,并发是指同一时间段内多个线程一起运行,而并行是同一时刻内多个线程一起运行
  • 并行需要多个处理核心,同一时刻多个处理器可以处理不同的线程。
  • 并发在单核通过快速切换任务实现

原子性操作库

有了线程库,C++可以很方便的进行多线程并发编程,但是伴随而来的线程安全问题也需要解决。线程安全问题其实就是多个线程访问共享资源导致数据不一致。虽然在C98就可以通过加锁来解决线程安全问题,但是加锁会导致其他线程进入阻塞等待,这会影响程序运行的效率,而且加锁使用不当可能造成死锁问题。于是C++11引入了原子操作(需要引入头文件atomic)。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。 下面是常见的一些原子类型的名称:
在这里插入图片描述
观察下面代码运行结果:
在这里插入图片描述
我们发现,由于两个线程同时访问sum,使得sum最后得到的结果无法确定。
现在给出使用原子操作的代码样例并观察结果:

在这里插入图片描述
在上面代码中,我们将sum定义成一个原子类型,后面在对sum进行的操作就都是原子性的, 我们不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。 上面代码中的atomic_int也可以用atomic模板实例化来代替。比如:

atomic<int>sum(1);

关于atomic类模板

程序员可以使用atomic类模板,定义出需要的任意原子类型。

atmoic<T> t;    // 声明一个类型为T的原子类型变量t

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。 这一点查看手册可以看到:
在这里插入图片描述

对比锁和原子操作

atomic和加锁都是用于多线程编程中实现同步和防止竞争条件的技术,它们之间的区别是什么呢?

  • atomic适用于简单的、独立的操作,类似于上面的sum++。
  • atomic依赖底层硬件指令,如Compare-And-Swap指令
  • 显式加锁会显式的阻塞其他线程
  • 加锁适用范围广,多用于复杂的、多个操作需要作为一个整体进行的场景。
  • 使用锁可能导致死锁

lock_guard与unique_lock

在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。 使用锁需要引入头文件mutex
比如:一个线程对变量number进行加一100次,另外一个减一100次,每次操作加一或者减一之后,输出number的结果,要求:number最后的值为1。
在这里插入图片描述
上面代码的问题,如果在锁中间发生了某种异常导致线程直接终止,这个时候锁没有得到释放,就会造成死锁问题。虽然上面的代码看起来不会发生什么异常,但是有时候我们需要在一些复杂的临界区上加锁,中间发生异常的概率就会大大提高。为了避免线程得到锁之后意外终止线程从而导致死锁问题,C++11采用RAII的方式对锁进行了封装,即lock_guardunique_lock

lock_guard

std::lock_gurad 是 C++11 中定义的模板类。定义如下:

template<class _Mutex>
class lock_guard
{
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}
	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}
	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

仔细观察上面的代码我们就能发现为什么lock_guard能解决之前的问题。借用类的构造函数和析构函数,我们将锁传递给lock_guard的构造函数,并由这个构造函数来进行上锁。栈帧销毁时,自动调用lock_guard对象的析构函数,该析构函数实现了释放锁的操作。这样一来,即使在上锁之后临界区因为某种异常退出,也不会造成死锁,因为只要结束线程函数,lock_guard就会自动销毁从而释放锁。更具体的说,lock_guard将锁与对象的生命周期进行绑定。值得注意的是,和上面的一些类模板一样,lock_guard类对象也是禁止拷贝构造赋值拷贝

虽然lock_guard使用起来很方便,但是无灵活性:不支持延迟锁定、提前解锁或重新锁定等操作

unique_lock

与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。(说白了和lock_guard主要功能差不多)

与lock_guard不同的是,unique_lock更加的灵活

  • 可以延迟锁定、提前解锁和重新锁定
  • 适用于条件变量的等待,因为它可以暂时释放锁,并在条件满足后重新锁定。

虽然unique_lock比lock_guard更加灵活,但是相应的额外开销也就较大

两个线程交替打印奇数和偶数

为了实现交替打印,这里使用了条件变量condition_variable类,和linux中的条件变量其实是类似的,下面这个类进行简单的介绍。

主要成员函数有:

  • wait:使线程阻塞,直到接收到通知或条件满足
  • notify_one:通知一个等待中的线程。
  • notify_all:通知所有等待中的线程。
    其中在调用 wait 时,需要提供一个 lambda 函数或条件检查,确保只有在条件满足时才继续执行

#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>

using namespace std;
mutex mtx;
condition_variable c;
int n = 100;
bool flag = true;

void PrintEven()//打印偶数
{
	int i = 0;
	while (i <= n) {
		unique_lock<mutex> lock(mtx);
		c.wait(lock, [&]() ->bool{return flag; });//flag为true的时候继续打印偶数
		cout << i << endl;
		i += 2;
		flag = false;
		c.notify_one();//唤醒
	}
}

void PrintOdd()//打印奇数
{
	int i = 1;
	while (i <= n) {
		unique_lock<mutex> lock(mtx);
		c.wait(lock, [&]() ->bool {return !flag; });//flag为false的时候继续打印奇数
		cout << i << endl;
		i += 2;
		flag = true;
		c.notify_one();
	}
}
int main() {
	thread t1(PrintEven);
	thread t2(PrintOdd);

	t1.join();
	t2.join();


}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值