首页 > 编程语言 >【C++11常见新特性(三)】线程库

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

时间:2024-07-14 23:26:04浏览次数:21  
标签:11 thread lock C++ 原子 guard 线程 unique

文章目录

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();


}

在这里插入图片描述

标签:11,thread,lock,C++,原子,guard,线程,unique
From: https://blog.csdn.net/qq_62987647/article/details/140422004

相关文章

  • 基于java+springboot+vue实现的健身房管理系统(文末源码+Lw)113
     基于SpringBoot+Vue的实现的健身房管理系统(源码+数据库+万字Lun文+流程图+ER图+结构图+演示视频+软件包)系统功能:本健身房管理系统管理员,会员,员工。管理员功能有个人中心,会员管理,员工管理,会员卡管理,会员卡类型管理,教练信息管理,解聘管理,健身项目管理,指导项目管理,健身器材管......
  • 基于java+springboot+vue实现的在线教育系统(文末源码+Lw)111
     基于SpringBoot+Vue的实现的在线教育系统(源码+数据库+万字Lun文+流程图+ER图+结构图+演示视频+软件包)系统功能:本在线教育系统管理员功能有个人中心,用户管理,讲师管理,普通管理员管理,课程管理员管理,课程管理,课程分类管理,教师管理,名师管理,系统管理,订单管理。普通管理员和课......
  • CF1107F Vasya and Endless Credits
    KM做法这么简单好想为什么都在dp?我第一次过也是用的dp。建模非常好想,每天只能收一次钱,最简单的思路是我们枚举第几天开车跑路,但是再一想我们不关心是第几天,只关心每次贷款离开车跑路还差几天,于是我们从\(i\)向\(j\)连边,边权是\(a_i+b_i\times\min(k_i,j)\),意义为第\(i\)......
  • C++11 标准库 线程库<thread>梳理
    目录<thread>this_thread命名空间1.get_id()2.sleep_for()3.sleep_until()4.yield()thread类构造函数:类方法1.get_id()2.join()3.detach()4.joinable()5.operator=6.hardware_concurrency(static)多线程的两种计算场景<thread>this_thread命名空间在C++11中不仅添加......
  • [操作系统]线程
    线程线程的状态转换参考文章线程在一定条件下,状态会发生变化。线程一共有以下几种状态:新建状态(New):新创建了一个线程对象。就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即......
  • C++11时间工具<chrono>梳理
    目录<chrono>时间间隔duration常用的duration时间点time_point时钟system_clock&steady_clocksystem_clock代码举例steady_clock例程:转换函数1.duration_castDescription:duration支持隐式转换的规则2.time_point_cast<chrono>C++11中提供了日期和时间相关的库chrono。chro......
  • 【力扣 709】转换成小写字母 C++题解(字符串)
    给你一个字符串s,将该字符串中的大写字母转换成相同的小写字母,返回新的字符串。示例1:输入:s=“Hello”输出:“hello”示例2:输入:s=“here”输出:“here”示例3:输入:s=“LOVELY”输出:“lovely”提示:1<=s.length<=100s由ASCII字符集中的可打印字符组......
  • 【力扣 58】最后一个单词的长度 C++题解(字符串)
    给你一个字符串s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。单词是指仅由字母组成、不包含任何空格字符的最大子字符串。示例1:输入:s=“HelloWorld”输出:5解释:最后一个单词是“World”,长度为5。示例2:输入:s="flymetot......
  • 1、多线程同步——CPU、core核、线程、内存
    CPU的运行原理控制单元在时序脉冲的作用下,将指令计数器里所指向的指令地址(这个地址是在内存里的)送到地址总线上去,然后CPU将这个地址里的指令读到指令寄存器进行译码。对于执行指令过程中所需要用到的数据,会将数据地址也送到地址总线,然后CPU把数据读到CPU的内部存储单元(就......
  • 求助!!![TJOI2009] 开关样例过不了,如何解决?(语言-c++)
    题目链接:https://www.luogu.com.cn/problem/P9869我的输出:1  12#include<bits/stdc++.h>usingnamespacestd;constintN=100300;intn,m,c,a,b;structnode{intf=0;intsum,l,r;//sum为开灯总数}tr[N<<2];voidup(intk){tr[k].sum+=tr[k......