文章目录
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_guard
和unique_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