C++11——线程

本文详细介绍了C++11标准中的线程库,包括其与系统线程库的关系,使用方法(如线程对象、lambda表达式等),以及线程安全问题,如shared_ptr和单例模式的处理。重点讨论了线程函数的参数传递、线程ID获取、互斥锁和条件变量的使用,以及atomic类的原子操作概念。
摘要由CSDN通过智能技术生成
C++11中添加了线程库,学习过操作系统的人大致都了解,其实语言的多线程只不过就是将系统
的多线程封装了一下,这也意味着语言层面的多线程具有跨平台的特性,那么现在我们就来认识
一下C++11标准指定的线程库它的使用是什么样子的。

1. 线程库

简单介绍

在这里插入图片描述
我们说C++11线程库的实现实际上就是把系统中的线程库再次封装了一边,在Linux中线程库使用的的POSIX线程库中的pthread库实现的,C语言实现,对于线程的使用是一种面向过程的,而在C++11中的线程库我们可以看到它是一个类,其实这种封装也就是将系统的线程库封装成一种面向对象的使用方式。其实一看到这个类中的成员函数,并且再看看构造函数,我们就能简单的使用C++的线程库了:
在这里插入图片描述
在这里插入图片描述

使用起来是和系统线程库有一些相似的,但是还有不同之处,首先就是线程函数,原生线程库中的函数是有类型要求的,但是在C++中的线程函数是没有这一个约束的,只要是个可执行就可以。
还有就是传参,原生线程库中的传参,假如要传多个参数就得借助结构体或者一个类来实现多参数传参,但是在C++线程库中可以传任意个参数。
我们再来看C++线程对象的构造函数:
在这里插入图片描述
我们可以看到,线程对象可以是一个空对象,这个空对象的创建好之后,这个对象的线程是不会启动的,因为没有可以执行的线程函数,所以是一个空线程。
而C++线程对象构造中可以传任意可执行和任意个参数的原因就是因为它的第二个构造函数,它的第一个参数是一种万能引用,可以接受任意类型的可执行,第二个参数是一个参数包,用以支持传任意个数的参数。
我们还可以看到,线程对象不支持拷贝构造,他也不支持复制重载,但是支持右值的拷贝构造和复制重载。
通过线程对象的第一个构造函数和可支持右值拷贝和复制重载,我们就可以写出一个创建多线程的代码:
在这里插入图片描述
这里的v初始大小为5个,那么它的内容就是线程对象所构造的空线程对象,
然后在循环中进行将亡值的转移,这也就是支持右值拷贝构造的原因。
对于一个线程来说,还应该有线程id,在这里线程id有两种方式:
在这里插入图片描述
通过线程对象调用,那么还有一种情况那就是线程函数中如何获取线程id呢?线程函数中可没有线程对象啊。那么在thread库中又封装了一个类:
在这里插入图片描述
在这里插入图片描述
他有一些静态成员函数供使用,那么在线程函数中调用该类中的get_id就可以获取执行当前线程函数中的线程id了:
在这里插入图片描述
在这里插入图片描述
在这个this_thread类中的yield函数多用于无锁编程,用来释放CPU资源的一个函数。
剩下两个函数就是线程库提供的休眠函数了。

C++11与线程库

C++中的线程库是在C++11制定的,它也融合了C++11的其他特性。
前面说了,C++线程库的线程函数的传参是一个可执行对象,在C++11之后可执行对象就有以下几种:

1. 函数指针
2. 仿函数
3. lambda表达式
4. 包装器

那么我们的线程创建就可以是这样:
在这里插入图片描述
我们可以使用lambda表达式来作为可执行对象,而且我们发现,我们不需要再传参数了,因为lambda表达式可以捕捉该域内的变量。
同样在C++线程库中上述代码也会出现线程不安全的问题:
在这里插入图片描述
所以我们也需要进行临界区的保护,也就是加锁。
关于线程的其他接口的意思就跟pthread库中的意思一样,可以看一手我的这篇博客pthread库
还有一点就是关于线程函数的返回值,这里C++并没有设计关于获取线程函数返回值的功能,如果想要获取返回值的话,可以通过输出型参数的方式来实现。

2. 互斥锁

简单介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
它的基本设计也跟pthread库是相近的:
在这里插入图片描述
在这里插入图片描述

这里在在一个函数中需要传局部的锁会有一些问题:
在这里插入图片描述
那就是,当对线程对象传参构造时,因为是封装了系统线程库的原因,这个参数不是直接给上面的函数传过去而是会在底层有一个拷贝的过程,导致就算线程函数以引用接收参数时,接收过来的也只是一个拷贝,所以会报错。

而要解决这种问题,一种方式是使用全局的锁,但是全局的资源往往是不推荐的。
还有一种方式就是在传参时对参数加ref,以声明我要传的是引用:
在这里插入图片描述

其他类型的锁

在这里插入图片描述
除了普通的互斥锁外,还有两种锁一种就是时间锁,可以和时间进行关联然后进行锁资源的申请与释放。还有一种就是recursive_mutex,这个锁是针对线程函数是递归函数时,如果对递归函数加普通的锁的话,会出现死锁问题,这个锁就可以解决这个问题

RAII

我在这篇博客对pthread库中的互斥锁进行了一个RAII思想封装的锁标题:局部变量的锁
那么这种通过RAII思想实现的锁资源的自动申请与释放,C++线程库中也有:
在这里插入图片描述

lock_guard

在这里插入图片描述
这是它的构造函数,使用起来也很简单:
在这里插入图片描述

在这里插入图片描述

unique_lock

在这里插入图片描述
在这里插入图片描述
unique_lock使用起来跟lock_guard差不多,但是unique_lock可以配合其他种的锁进行使用,并且unique_lock是可以支持手动加锁释放锁的,这是lock_guard本质的区别。
使用RAII的锁可以防止程序抛异常接收异常时执行代码的跳转导致锁没有及时释放的问题。

3. 条件变量

C++线程库中也对条件变量进行了封装,条件变量的作用就是防止出现在临界区资源未准备好时,线程一直申请锁释放锁做无意义浪费资源的情况出现。同时条件变量也可以一定程度上控制线程的执行顺序, 它需要配合互斥锁来实现对资源更好的管理与分配:
在这里插入图片描述
其中notify_one是唤醒一个在条件变量下等待的线程,notify_all是唤醒所有在条件变量下等待的线程。
使用条件变量可以写出这样的代码,控制两个函数交替打印奇偶数:
在这里插入图片描述
在使用条件变量进行等待时,需要传一把锁,在这里这把锁的类型时unique_lock类型的锁,原因就是unique_lock可以手动释放锁,而在条件变量下等待的线程会将自己具有的锁资源释放掉。这就是条件变量的使用。

4. atomic

我们知道对临界资源加锁的方式其实是让临界区的代码具有类似原子操作的表现,而对于上面的简单的进行对一个变量加加的代码,这个加加的代码执行起来可能还有没申请锁耗费的时间多,也就是临界区代码执行时间短。对于这种情况对临界区加锁其实是效率较低下的,针对这种情况,就有了自旋锁。对于自旋锁读者可以自行去了解,这里提出第二种方式,在C++线程库中有一个类atomic,它可以将一个变量的简单的行为变为原子的:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过atomic可以将变量的简单操作变成原子操作。
在这里插入图片描述
这里建议的是,一般将内置类型设置为原子的,自定义类型的都是需要函数重载来实现这些简单操作,实际上会很浪费资源。
而这背后的原理与锁无关,而是一个叫做CAS编程的思想,有兴趣也可以自行了解。

5. 线程安全

在多线程中有两个较常用的功能会存在线程安全问题

a. shared_ptr

我们知道shared_ptr的底层实际上是一个计数器来记录指向资源指针数,当指向资源的指针数为0时就释放资源,它的底层代码大致是这样:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
那么当两个线程同时要对共享指针进行拷贝时,势必会产生两个线程的count同时++的情况,所以这里的count是线程不安全的。解决方式有两种,一种就是对临界区进行加锁,还有种方式,那就是将count的操作变为原子的:
在这里插入图片描述
在这里插入图片描述
需要注意的是shared_ptr本身是线程安全的,但是它保护的资源可不是线程安全的,所以当shared_ptr所保护的资源成为共享资源被多线程使用时,仍然需要锁的保护。

b. 单例模式

单例模式中饿汉模式就是在主函数开始执行前就会创建好,使用的是全局变量的方式,它是线程安全的,而对于懒汉模式来说,他会在程序需要的时候
再进行创建,懒汉模式创建时的代码大致如下:
在这里插入图片描述
这样的代码,在多线程下,可能会出现多个线程同时第一次需要单例实例,然后在第一个创建实例执行之前,就会有许多线程同时判断到sl为nullptr,从而重复创建单例导致内存泄漏。所以这里也是需要对临界区加锁:
在这里插入图片描述

又因为,判断单例模式是否为空只需要一次就够了,所以这里会出现双检测的方式:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值