【C++】线程库

        在 C++11 问世之前,多线程基本和平台关系密切,不同的平台下多线程各有特点,使得 Windows 和 Linux 下必须拥有各自的系统调用接口,但同时也使得代码的可移植性较差。

        C++11中最重要的特性,就是支持了多线程编程,使 C++ 代码在并行编程时无须依赖第三方库,同时针对原子性操作引入了原子类的概念,大大提升了代码的跨平台性。

        本篇博客整理 C++ 线程库的概念和相关接口,旨在让读者了解并熟悉 C++ 下的多线程编程。

(欲知线程、线程控制和多线程的概念,请移步至:【Linux系统】线程与线程控制-CSDN博客 或 【Linux系统】多线程-CSDN博客)

目录

一、线程库封装了不同平台的接口

二、线程类 thread

1)创建 thread 对象

.1- 无参构造

.2- 带参构造

.3- 移动构造

2)thread 类提供的非默认成员函数

.1- 获取线程TID

.2- 判定线程是否是有效

.3- 阻塞和分离

3)线程例程的传参问题

.1- 借助 std::ref()

.2- 地址的拷贝

.3- 借助 lambda 表达式

三、互斥量库 mutex

1)四种互斥量

.1- std::mutex

.2- std::recursive_mutex

.3- std::timed_mutex

.4- std::recursive_timed_mutex

2)RAII 锁

.1- 死锁问题

.2- lock_guard

.3- unique_lock

四、原子性操作库 atomic

1)线程安全问题

​编辑

2)解决方案

.1- 加锁

.2- 原子性操作类型

.补- 原子性操作的原理

五、条件变量库 condition_variable

1)wait 等待系列

2)notify 唤醒系列

补、实现两个线程交替打印数字 1 到 10

补、shared_ptr 的线程安全

补、懒汉模式的线程安全 


一、线程库封装了不同平台的接口

        由于在 Linux 操作系统中,线程的相关接口的遵循了 POSIX 标准(Portable Operating System Interface of UNIX,可移植操作系统接口),因此 Linux 中与线程有关的代码也只在具备POSIX 标准的操作系统之间具备可移植性。

//Linux 创建5个线程
#include<iostream>
#include<pthread.h>
#include<vector>
//线程例程
void* function(void* arg)
{int* tid = (int*)arg;printf("I am %d thread\n", *tid);delete tid;return nullptr;
}
const int num = 5;
int main()
{std::vector<pthread_t> tids(num);for(int i = 0; i < num; i++){//创建线程pthread_create(&tids[i],nullptr,function,new int(i));}for(int i = 0; i < num; i++){//回收线程pthread_join(tids[i],nullptr);}return 0;
}

        而在 Windows 操作系统中,线程的相关接口也是 Windows 独有的 API 接口(Application Programming Interface,程序间的接口),也遵循了 POSIX 标准,同样的, Windows 中与线程有关的代码也只在具备POSIX 标准的操作系统之间具备可移植性。

//Windows 创建5个线程
#include<iostream>
#include<Windows.h>
//线程例程
DWORD WINAPI ThreadFunction(LPVOID args)
{int* tid = (int*)args;printf("I am %d thread\n", *tid);delete tid;return 0;
}int main()
{const int num = 5;HANDLE threads[num];for (int i = 0; i < num; i++){   //创建线程threads[i] = CreateThread(nullptr, 0, ThreadFunction, new int(i), 0, nullptr);if (threads[i] == nullptr){std::cout << "create thread fail" << std::endl;return 1;}}//回收线程WaitForMultipleObjects(num, threads, TRUE, INFINITE);for (int i = 0; i < num; i++){//释放线程CloseHandle(threads[i]);}return 0;
}

        从以上代码可以看到,Linux 和 Windows 下的线程接口哪怕功能类似,它们的实现(名称、参数、返回值等)也各有特点。

        为了提升不同平台下代码的跨平台性,C++11 通过类似条件编译的形式,将不同平台的接口封装为一个线程库,从而实现了“一码多用”。

//C++11 创建5个线程
#include<thread>
#include<iostream>
#include<vector>
void function(int i)
{printf("I am %d thread.\n", i);
}
int main()
{const int num = 5;std::vector<std::thread> threads(num);for (int i = 0; i < num; i++){//创建线程threads[i] = std::thread(function, i);}//回收线程for (auto& thd : threads){thd.join();}return 0;
}

Windows下 VS2022 编译后的运行结果:

Linux(CentOS 7.6版本)下 g++ 编译后的运行结果:

二、线程类 thread

        C++11将线程封装成了线程类 thread,并将不同平台下的线程接口封装成了线程类 thread的成员函数,由此,只需要实例化一个 thread 类,就可以通过实例化对象所暴露的外部接口对线程进行操作。

1)创建 thread 对象

【Tips】关于 thread 类提供的构造函数

  1. 线程是一个操作系统层面的概念,thread 对象可以从语言层面关联一个系统层面的线程,具体的方式是让 thread 对象与线程的例程建立关联(线程的执行函数)并以此控制线程以及获取线程的状态。
  2. 如果一个 thread 对象在被创建时,没有为其提供线程例程,那么这个 thread 对象实际没有关联任何线程;如果一个 thread 对象在被创建时,为其提供了线程例程,那么就会启动一个线程来执行这个例程,且这个启动的线程与主线程一起运行。
  3. thread 类是防拷贝的,不允许拷贝构造和拷贝赋值仅允许移动构造和移动赋值,因此,可以将一个 thread 对象与一个例程的关联状态转移给其他 thread 对象,且转移期间不会影响例程的执行。 

.1- 无参构造

        thread 类提供了无参的构造函数。由无参构造所实例化的 thread 对象没有关联任何线程例程。

thread t1;

        但这并不意味着无参构造没有任何意义,例如当后续需要该 thread 对象与线程例程建立关联时,仅需通过 thread 提供的移动构造和移动赋值创建一个匿名对象,同时将匿名对象的关联状态转移给该 thread 对象即可。 

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t1;//...t1 = thread(func, 10);//...return 0;
}

.2- 带参构造

        thread 类也提供了带参的构造函数,可以通过指定的线程例程来创建和关联一个 thread 对象。

//带参构造的定义:
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
//其中,
//fn是线程例程的模板(包括函数指针、仿函数、lambda表达式、经包装器处理过的可调用对象等)
//args... 是可变参数模板
//调用示例
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);//...return 0;
}

.3- 移动构造

        thread 类提供了支持右值引用的移动构造函数,配合移动赋值,能够用一个作为右值的 thread 对象来实例化另一个新的 thread 对象。

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t3 = thread(func, 10);//...return 0;
}

2)thread 类提供的非默认成员函数

get_id获取 thread 对象关联的线程TID
swap    交换将两个 thread 对象的关联状态
joinable判断一个 thread 对象是否有效,是则返回 true,否则返回 false
join阻塞等待一个线程
detach分离一个线程

.1- 获取线程TID

         get_id() 可以获取当前 thread 对象所关联的线程的TID。

#include<thread>
#include<iostream>
#include<vector>
using namespace std;
void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t(func, 10);thread::id tid = t.get_id();cout << tid << endl;//...return 0;
}

        如果要在线程例程中获取线程id,就需要指定 this_thread 命名空间下的 get_id()。


void func()
{cout << this_thread::get_id() << endl; //此时哪个线程执行这条语句就返回哪个线程的tid值
}
int main()
{thread t(func);//...return 0;
}

        这是因为,get_id() 其实是定义在 this_thread 命名空间中的,而该命名空间中的其他接口也是类似的用法,主要针对执行该接口的线程来使用。

【补】 this_thread 命名空间中的接口

  • yield():当前线程“放弃”执行,让 CPU 调度另一线程继续执行。
  • sleep_until():让当前线程休眠到一个具体时间点。
  • sleep_for():让当前线程休眠一个时间段,例如 1 秒。

.2- 判定线程是否是有效

        joinable() 可以判断一个 thread 对象是否有效,如果 thread 对象满足以下任意情况,则是无效的:

  • 通过无参构造创建,则这个 thread 对象没有关联任何线程。
  • 一个 thread 对象的关联状态已转移给另一个 thread 对象,则其原本关联的线程已被交给其他 thread 对象管理。
  • thread 对象已调用 join() 或 detach() 终止线程。

.3- 阻塞和分离

        一个线程可以被创建后,也相应地需要被回收,否则会导致内存泄露等问题。

        thread 类中提供了阻塞和分离两种回收线程的方式,它们分别对应成员函数 join() 和 detach()。

  • 阻塞 join()

        join() 主要针对于关联了线程的 thread 对象,后续回收它所关联的线程资源。一个 thread 对象调用 join() 会使自身暂时处于不可关联的状态,并将原本关联的线程安全地销毁。

【ps】调用 join() 的注意事项

        一个 thread 对象在关联了一个线程后,只允许调用一次 join(),否则会导致程序崩溃。

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t(func, 20);t.join();t.join(); //程序崩溃return 0;
}

        除非一个已关联线程的 thread 对象在调用一次 join() 后,又经历了一次移动赋值,此时才可以又调用一次 join()。

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
int main()
{thread t(func, 20);t.join();t = thread(func, 30);//移动赋值使 t 又关联了一个线程t.join(); //关联了线程的 thread 对象可以安全地调用 join()return 0;
}

【补】RAII 方式回收线程

        调用 join() 可以回收线程资源并终止线程,但如果一个线程在执行 join() 所在的代码语句之前被中途切走或中断,导致无法执行 join() ,就会留下一些内存资源上的隐患。例如以下这样一个情景:

void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
bool DoSomething()
{return false;
}
int main()
{thread t(func, 20);if (!DoSomething())return -1;t.join(); //不会被执行return 0;
}

        为了避免这个问题,可以采用 RAII 的方式对 thread 对象进行封装,利用对象的生命周期特性来控制线程资源的释放。

class myThread
{
public:myThread(thread& t):_t(t){}~myThread() {if (_t.joinable())_t.join(); // 利用对象的生命周期特性来控制线程资源的释放}//防拷贝myThread(myThread const&) = delete;myThread& operator=(const myThread&) = delete;
private:thread& _t;
};
//1.每当创建一个thread对象后,就用一个类将其封装。
//2.当封装的类所实例化的对象,生命周期结束时,就会调用析构函数,
//  在析构中调用join()来回收线程。void func(int n)
{for (int i = 0; i <= n; i++){cout << i << endl;}
}
bool DoSomething()
{return false;
}
int main()
{thread t(func, 20);myThread mt(t); //使用一个类将thread对象封装起来if (!DoSomething())return -1;//t.join(); //就算被切走/中断,join()也能够正常调用return 0;
}

  •   分离 detach()

         detach()可以将一个 thread 对象所关联的线程放在在后台运行,将线程的所有权和控制权交给C++运行库,让C++运行库去保证线程能够被正确地回收,使这个线程与主线程分离,能够被自动回收。

【ps】调用 detach() 的注意事项

        detach()的调用一般即刻发生在thread对象关联了一个线程后。如果 thread 对象因某些原因,在后续调用 detach() 之前被销毁,就会导致程序崩溃。

3)线程例程的传参问题

        thread 对象的线程例程的参数通过值拷贝的方式,被拷贝进其所关联的线程的栈空间中,因此,例程中的参数(形参)哪怕是引用类型,实际引用的也是线程栈空间中的拷贝,在例程中对参数进行修改操作,并不会影响到例程外部的实参。

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

        如果希望通过线程例程的形参改变外部的实参,可以参考以下三种方式:

  1. 借助 std::ref()
  2. 地址的拷贝
  3. 借助 lambda 表达式

.1- 借助 std::ref()

        当线程例程的参数为引用类型时,std::ref() 可以做到在传入实参时保持对实参的引用,而非栈空间中的拷贝。

void ThreadFunc1(int& x)
{x += 10;
}
int main()
{int a = 10;thread t(ThreadFunc1, std::ref(a));t.join();cout << a << endl; //20return 0;
}

.2- 地址的拷贝

        当线程例程的参数为指针类型时,线程栈空间中拷贝的是一个地址,这个拷贝的地址和外部所传的地址是一致的,对这个拷贝地址的操作也等效于对外部所传地址的操作,因此,只要将实参的地址传入,就可以通过修改该地址上的变量进而影响到外部实参。

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

.3- 借助 lambda 表达式

        将 lambda 表达式作为线程例程,就可以通过 lambda 的捕捉列表,以引用的方式捕捉外部实参,使 lambda 表达式中形参的改变也能影响到外部实参。

#include<thread>
#include<iostream>
using namespace std;int main()
{int a = 10;thread t3([&a] {a+=10; });t3.join();cout << a << endl; //20return 0;
}

三、互斥量库 mutex

        与线程库类似,C++11也将线程互斥相关的互斥量、接口封装成了 mutex 类,以更好地支持线程互斥代码的可移植性。

1)四种互斥量

        mutex 类一共包含四种互斥量,它们分别是mutex、std::recursive_mutex、timed_mutex、recursive_timed_mutex。

.1- std::mutex

        mutex 是 C++11 提供的最基本的互斥量,mutex 对象之间不能进行拷贝和移动,常用的成员函数有 lock()、try_lock()、unlock()。

lock    对互斥量进行加锁
try_lock尝试对互斥量进行加锁
unlock对互斥量进行解锁,释放互斥量的所有权

【补】加锁的情况说明

        加锁和解锁操作一般在线程例程中完成。 

        当一个例程中调用 lock() 加锁时,可能会发生以下情况:

  • 如果要加锁的互斥量当下没有被其他线程锁住,则让当前线程锁住互斥量,在该线程调用 unlock() 解锁之前,该线程将一直持有该锁。
  • 如果要加锁的互斥量已经被其他线程锁住,则当前调用 lock() 的线程会被阻塞。
  • 如果要加锁的互斥量已经被其他线程锁住,且当前 lock() 调用成功,即该互斥量又被当前线程锁住,则会产生死锁。

        当一个例程中调用 try_lock() 加锁时,可能会发生以下情况:

  • 如果要加锁的互斥量当下没有被其他线程锁住,则让当前线程锁住互斥量,在该线程调用 unlock() 解锁之前,该线程将一直持有该锁。
  • 如果要加锁的互斥量已经被其他线程锁住,则 try_lock() 直接返回false,当前调用 try_lock() 的线程不会被阻塞。
  • 如果要加锁的互斥量已经被其他线程锁住,且当前 try_lock() 调用成功,即该互斥量又被当前线程锁住,则会产生死锁。
//示例:让两个线程加锁并各自输出1-10
#include<thread>
#include <mutex>
#include<iostream>
using namespace std;void func(int n, mutex& mtx)
{mtx.lock(); //for循环体外加锁可以节省资源的申请开销for (int i = 1; i <= n; i++){cout << i << endl;}mtx.unlock();
}
int main()
{mutex mtx;thread t1(func, 10, ref(mtx));thread t2(func, 10, ref(mtx));t1.join();t2.join();return 0;
}

//未加锁时的输出结果
#include<thread>
#include <mutex>
#include<iostream>
using namespace std;void func(int n, mutex& mtx)
{//mtx.lock(); for (int i = 1; i <= n; i++){cout << i << endl;}//mtx.unlock();
}
int main()
{mutex mtx;thread t1(func, 10, ref(mtx));thread t2(func, 10, ref(mtx));t1.join();t2.join();return 0;
}

.2- std::recursive_mutex

        recursive_mutex 是一种递归互斥锁,专门针对递归的线程例程中的加锁,recursive_mutex也提供了 lock()、try_lock()、unlock(),其特性与 mutex 提供的大致相同。

        如果在递归例程中使用 mutex 互斥量进行加锁,那么在例程进行递归调用时,可能会因重复申请已申请的锁而导致死锁问题。recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),以获得互斥量对象的多层所有权,但在解锁时,需要调用与该锁层次深度相同的unlock()。

.3- std::timed_mutex

        timed_mutex 在 mutex 的基础上增加了计时功能,可以让一个线程定时申请锁。

        除了 lock()、try_lock()、unlock() 之外,timed_mutex 还提供了 try_lock_for()、try_lock_until()。

        try_lock_for() 的参数是一个时间范围,用于线程在锁释放后申请锁。一个线程在某一段时间之内,如果没有获得锁,则该线程会被阻塞住;如果在此期间其他线程释放了锁,则该线程可以进行加锁;如果超时(在这一段时间之内没有获得锁),则返回false。
        try_lock_untill() 的参数是一个时间点,也用于线程在锁释放后申请锁。一个线程在指定的时间点到来之前,如果没有获得锁,则该线程会被阻塞住;如果在此期间其他线程释放了锁,则该线程可以进行加锁;如果超时(在这一段时间之内没有获得锁),则返回false。

.4- std::recursive_timed_mutex

        recursive_timed_mutex 相当于 recursive_mutex 和 timed_mutex 的结合版本,既支持在递归例程中加锁,又支持定时申请锁。

2)RAII 锁

.1- 死锁问题

        在使用一个互斥量时,如果临界区太大(加锁和解锁操作之间的代码),在临界区中又有线程例程的返回语句(在解锁前返回),或者临界区中抛了异常,那么后续申请这个互斥量的线程就会被阻塞住,进而导致死锁问题。

mutex mtx;
void func()
{mtx.lock();//加锁//...FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //中途返回(尚未解锁)}//...mtx.unlock();//解锁
}
int main()
{func();return 0;
}

        为了避免上述问题,C++11 采用了 RAII 方式将互斥量分别封装为了 lock_guard、unique_lock。 

        所谓 RAII,是一种设计思想,通过对象的生命周期的特性(定义时自动调用构造来创建,出作用域时自动调用析构来销毁),来更好地管理资源。

.2- lock_guard

        lock_guard 本质是一个模板类:

//lock_guard的定义
template <class Mutex>
class lock_guard;

        在需要加锁的位置,可以用互斥量实例化一个 lock_guard 对象,来代替对互斥量加锁的操作。

        在创建 lock_guard 对象时会自动调用构造函数,而在 lock_guard 类的构造函数中会调用 lock() 进行加锁;当 lock_guard 对象出作用域时会自动调用析构函数,而在 lock_guard 类中的析构函数中会调用 unlock() 进行解锁。

mutex mtx;
void func()
{lock_guard<mutex> lg(mtx); //自动调用构造加锁//...FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //自动调用析构解锁}//...} //自动调用析构解锁int main()
{func();return 0;
}

        如果想用 lock_guard 对指定的一部分代码进行保护,可以定义匿名的局部域来控制 lock_guard 对象的生命周期。

mutex mtx;
void func()
{//...//匿名局部域{lock_guard<mutex> lg(mtx); //自动调用构造加锁FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //自动调用析构解锁}} //自动调用析构解锁//...
}
int main()
{func();return 0;
}

【补】模拟实现 lock_guard 

//1.lock_guard类中包含一个互斥量(引用类型)
//2.在构造中加锁,在析构中解锁
//3.一定要防拷贝
namespace NeeEko
{template<class Mutex>class lock_guard{public://构造lock_guard(Mutex& mtx):_mtx(mtx){mtx.lock(); //加锁}//析构~lock_guard(){mtx.unlock(); //解锁}//防拷贝lock_guard(const lock_guard&) = delete;lock_guard& operator=(const lock_guard&) = delete;private:Mutex& _mtx;};
}

.3- unique_lock

        虽然 lock_guard 能够避免死锁问题,但用法过于单一,用户无法直接控制互斥量,无法灵活地使用。对此,C++11 又提供了 unique_lock。

        与 lock_guard 类似的是,unique_lock 也可以在自动调用构造时加锁、在自动调用析构时解锁;但 与 lock_guard 不同的是,unique_lock 提供了更多的成员函数,可以更加灵活地控制互斥量。

【补】unique_lock 的成员函数:

  • 加锁/解锁:lock()、try_lock()、try_lock_for()、try_lock_until() 、unlock()。
  • 修改:移动赋值、swap()、release()(返回它所管理的互斥量对象的指针,并释放所有权)。
  • 获取属性:owns_lock()(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex()(返回当前unique_lock所管理的互斥量的指针)。

【补】unique_lock 的使用示例:

        如图,func2() 在 func1() 内部调用,其中,func1() 的大部分代码需要加锁保护,但调用func2() 的部分不需要用到保护 func1() 的锁,调用 func2() 的部分和 func2() 的内部则由其他锁来保护。

        因此,在调用 func2() 之前需要对保护 func1() 的锁进行解锁,当 func2() 的调用返回后再重新加锁,如此一来,在调用 func2() 时,如果存在其他线程调用 func1() ,就能使其他线程也可以持有这个保护 func1() 的锁。

四、原子性操作库 atomic

1)线程安全问题

        多线程之间能够共享一份数据,每个线程都可以对这份数据做访问活修改操作。如果共享数据都是只读的,多线程就只对数据做访问操作,不会涉及修改操作,因此所有线程都能够访问到同样的数据。但当多个线程要对共享数据做修改操作时,就可能引起数据不一致问题,即线程安全问题。

        例如下面这个示例:

#include<thread>
#include <mutex>
#include<iostream>
using namespace std;//每个线程对n++的次数
void func(int& n, int times)
{for (int i = 0; i < times; i++){n++;}
}
int main()
{int n = 0;int times = 100000;//创建两个线程thread t1(func, ref(n), times);thread t2(func, ref(n), times);t1.join();t2.join();cout << n << endl; //打印n的值return 0;
}

        按理来说,两个线程都对 n 进行了 ++ 操作,最终结果应该是 200000,可最终结果却不到预期的 200000。

         这其实是因为,++ 操作并不是一个原子操作,它本身可以转化成三句汇编语句,对应三步操作:

  1. load:将共享变量 n 从内存加载到寄存器中。
  2. updata:更新寄存器里面的值,执行 +1 操作。
  3. store:将新值从寄存器写回共享变量 n 的内存地址。

         多线程是并发运行的,当线程 1 刚完成 load 操作(++ 操作的第一步),就可能因 CPU 的调度而被切走,换上线程 2 继续执行;线程 2 可能顺利完成了 load 操作、updata 操作、store 操作(一次完整的 ++ 操作),然后被切走,换上线程 1 ,由线程 1 继续完成剩余的两步 updata 操作 和 store 操作。

        于是,虽然两个线程都对共享变量 n 执行了一次 ++ 操作,但整个过程中 n 的值却只被 ++ 了一次......循环往复,n 的值最后也不足预期的 200000 了。这就是线程安全问题。

2)解决方案

.1- 加锁

        为避免线程安全问题,可以对共享数据相关的临界区进行加锁保护。

void func(int& n, int times, mutex& mtx)
{mtx.lock();//在for循环体外进行加锁解锁,虽然可以节省申请锁的开销//但会使两个线程的执行逻辑从并行变为串行,//如果对锁的控制不得当,还很容易造成死锁for (int i = 0; i < times; i++){//mtx.lock();n++;//mtx.unlock();}mtx.unlock();
}
int main()
{int n = 0;int times = 100000; mutex mtx;thread t1(func, ref(n), times, ref(mtx));thread t2(func, ref(n), times, ref(mtx));t1.join();t2.join();cout << n << endl; return 0;
}

.2- 原子性操作类型

        为了避免线程安全问题和更好地支持线程同步,C++11 引入了原子性操作类型和原子性操作库 atomic 中。

        除了可以使用已定义好的原子性操作类型,还可以使用 atomic 模板类自行定义任意的原子类型。

【ps】原子类型变量的初始化需用到 C++11 的 { } 初始化列表方式。

void func(atomic_int& n, int times)
{for (int i = 0; i < times; i++){n++;}
}
int main()
{atomic_int n = { 0 };int times = 100000;thread t1(func, ref(n), times);thread t2(func, ref(n), times);t1.join();t2.join();cout << n << endl; return 0;
}

【补】相关说明

  • 原子类型通常属于“资源类型”数据,多个线程只能访问单个原子类型的拷贝,因此在 C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及赋值重载等,为防止意外,标准库已经将 atomic 模板类中的拷贝构造、移动构造、赋值重载 delete 掉了。
  • 原子类型不仅仅支持原子的 ++ 操作,还支持原子的 --、加一个值、减一个值、与、或、异或操作等。
void func(atomic<int>& n, int times)
{for (int i = 0; i < times; i++){n++;}
}
int main()
{atomic<int> n = 0;//int n = atomic(0); //也可以这样写int times = 100000;thread t1(func, ref(n), times);thread t2(func, ref(n), times);t1.join();t2.join();cout << n << endl;return 0;
}

.补- 原子性操作的原理

        原子性操作的原理是 CAS(compare and swap)。

        CAS包含三个操作数:内存位置的原值(V),预期原值(A)和新值(B),它们与 CPU 的关系如下:

  • 若 V 与 A 相等,则 CPU 会将 V 更新为 B。
  • 若 V 与 A 不相等,则 CPU 不会做任何操作。

        以两个线程 t1 和 t2 同时对变量 val 进行 ++ 操作为例, t1 和 t2 都会将 val 的值送入加法器 eax 中,同时将当前 val 在内存中的值设为 V ,在正式修改 val 值之前,CPU 会先判断 eax 中的 val 值与 V 是否相等,若相等(说明操作不是原子的了)则将 eax 中的值进行 ++ 操作得到新值 B,然后将 V 和 val 的值更新为 B ,若不相等(说明操作还是原子的)则不修改。

//伪代码过程:
while(1)
{eax = val;       // 将 val 值取到寄存器 eax 中if(eax == V)     // eax 中的值和 V 相同则可以修改{eax++;V = eax;     // 修改 V 为 Bval = eax;   // 修改 val 的值为 Bbreak;       // 访问结束,跳出循环}
}

        t1 和 t2 虽然是并行的,但从极小的时间粒度的角度去看,CPU 仍然是挨个在执行它们。首先,t1 将 val 值送到 eax 中,并对 V 赋值,判断的条件为真,于是修改 val 的值,并放回到 val 的地址中;然后, t2 被唤醒,将 val 值送到 eax 中后,发现 eax 中的值与最开始的 V 不同了,于是不对 val 的值做修改,继续循环,直到 eax 中的值和 V 相等才做修改。

        总得来说,虽然原子性操作能够保证线程安全,但另一个无法写的线程会因不停执行循环而陷入等待,就会占用一定的 CPU 资源。

五、条件变量库 condition_variable

        为了更好地支持线程同步,C++11将不同平台下的条件变量的相关接口封装成了条件变量库 condition_variable。

        condition_variable 中提供的成员函数,主要分为 wait 系列和 notify 系列。 

1)wait 等待系列

        wait 系列包括 wait()、wait_for() 、wait_until(),其作用就是让线程在一个条件变量下进行阻塞等待。

        其中最常用的是 wait()。

【Tips】wait() 的使用手册

        wait() 提供了两个不同版本的接口:

//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);

        调用版本一时,只需传入一个 unique_lock 类型的互斥锁,线程在调用后会立即被阻塞,等待被唤醒。需要传入一个互斥锁是因为,wait() 的调用一般发生在临界区中,为了让当前线程在阻塞时,其他线程也能够获取到锁,wait() 专门将一个互斥锁作为参数,使当前线程在被阻塞时,互斥锁能够自动解锁,而等该线程被唤醒时,又能自动持有这个互斥锁。由此,wait() 实际上有两个功能,一个是让线程在条件不满足时在一个条件变量下进行阻塞等待,另一个是让线程原本持有的互斥锁自动解锁或再次被线程持有。
        调用版本二时,除了需要传入一个 unique_lock 类型的互斥锁,还需要传入一个返回类型为 bool 的可调用对象。与版本一不同的是,当调用版本二阻塞的线程被唤醒后,还需要调用这个 bool 的可调用对象,如果可调用对象的返回值为 false,那么该线程还会继续被阻塞。

【补】wait_for() 和 wait_until() 的相关说明

        wait_for() 也提供了两个版本的接口,且这两个版本的接口都比 wait() 对应的接口多了一个参数,这个参数是一个时间段,表示让线程在该时间段内进行阻塞等待,如果超过这个时间段则线程被自动唤醒。


        wait_until() 也提供了两个版本的接口,且这两个版本的接口都比 wait() 对应的接口多了一个参数,这个参数是一个具体的时间点,表示让线程在该时间点之前进行阻塞等待,如果超过这个时间点则线程被自动唤醒。
        线程调用wait_for() 或 wait_until() 在阻塞等待期间,其他线程调用 notify 系列也可以将其唤醒。

        此外,如果调用的是 wait_for() 或 wait_until() 的第二个版本的接口,那么当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么当前线程还会继续被阻塞。

2)notify 唤醒系列

        notify 系列包括 notify_one() 和 notify_all(),其作用是唤醒阻塞等待中的线程。

        一个条件变量下可能会有多个线程在进行阻塞等待,这些线程会被放到一个等待队列中进行排队——

  • notify_one():唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。

  • notify_all():唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

补、实现两个线程交替打印数字 1 到 10

#include<thread>
#include <mutex>
#include<iostream>
using namespace std;
int main()
{int val = 0;int n = 10;              // 打印的范围mutex mtx;               // 创建互斥锁condition_variable cond; // 创建条件变量//t1打印奇数thread t1([&](){while (val < n){unique_lock<mutex> lock(mtx); // 加锁while (val % 2 == 0)// 判断是否是偶数{// 是偶数则放入等待队列中等待cond.wait(lock);}// 是奇数时打印cout << "thread1:" << this_thread::get_id() << "->" << val++ << endl;cond.notify_one(); // 唤醒等待队列中的一个线程去打印偶数}});this_thread::sleep_for(chrono::microseconds(100));//t2打印偶数thread t2([&](){while (val < n){unique_lock<mutex> lock(mtx);while (val % 2 == 1){cond.wait(lock);}cout << "thread2:" << this_thread::get_id() << "->" << val++ << endl;cond.notify_one();//唤醒等待队列中的一个线程去打印奇数}});t1.join();t2.join();return 0;
}

补、shared_ptr 的线程安全

(关于 shared_ptr 的更多说明,请见【C++】智能指针-CSDN博客)

namespace CVE
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)),_pMtx(new mutex){}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount), _pMtx(sp._pMtx){_pMtx->lock();(*_pCount)++;_pMtx->unlock();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2;{                    Release(); // 防止内存泄漏_ptr = sp._ptr;_pCount = sp._pCount;_pMtx->lock();(*_pCount)++;_pMtx->unlock();}return *this;}void Release() // 防止内存泄漏{bool flag = false;_pMtx->lock();if (--(*_pCount) == 0){delete _ptr;delete _pCount;flag = true;}_pMtx->unlock();if (flag){delete _pMtx; // new出来的,引用计数为0时要delete}}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}protected:T* _ptr;int* _pCount;// 引用计数mutex* _pMtx;// 互斥锁};
}

补、懒汉模式的线程安全 

(关于懒汉模式的更多说明,请见:【C++】特殊类的设计-CSDN博客)

//写法一
class Singleton
{
public:static Singleton* GetInstance(){// 双检查加锁if (m_pInstance == nullptr) // 保护第一次后,后续不需要加锁{unique_lock<mutex> lock(_mtx); // 加锁,防止new抛异常就用unique_lockif (m_pInstance == nullptr) // 保护第一次时,线程安全{m_pInstance = new Singleton;}}return m_pInstance;}private:Singleton() // 构造函数私有{}Singleton(const Singleton& s) = delete; // 禁止拷贝Singleton& operator=(const Singleton& s) = delete; // 禁止赋值// 静态单例对象指针static Singleton* m_pInstance; // 单例对象指针static mutex _mtx;
};
//初始化
Singleton* Singleton::m_pInstance = nullptr; 
mutex Singleton::_mtx;
//写法二
class Singleton
{
public:static Singleton* GetInstance(){static Singleton _s; // 局部的静态对象,第一次调用时才初始化// 在C++11之前是不能保证线程安全的,// C++11之前局部静态对象的构造函数调用初始化并不能保证线程安全的原子性。// 而 C++11 修复了这个问题,这种写法也只能在支持C++11以后的编译器上使用return &_s;}private:// 构造函数私有Singleton(){};Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/36479.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【高性能服务器】服务器概述

&#x1f525;博客主页&#xff1a; 我要成为C领域大神&#x1f3a5;系列专栏&#xff1a;【C核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 ​ 服务器概述 服…

计算机网络 —— 基本概念

基本概念 1. 通信协议2. 面向连接 v.s. 面向无连接3. 电路交换 v.s. 分组交换4. 单工通信 v.s. 双工通信 1. 通信协议 通信协议就是计算机与计算机之间通过网络实现通信时事先达成的一种“约定”。这种“约定”使那些由不同厂商的设备、不同的CPU 以及不同的操作系统组成的计算…

C# 实现websocket双向通信

&#x1f388;个人主页&#xff1a;靓仔很忙i &#x1f4bb;B 站主页&#xff1a;&#x1f449;B站&#x1f448; &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;C# &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff…

人工智能在多模态多组学领域的最新研究进展|顶刊速递·24-06-29

小罗碎碎念 本期推文主题&#xff1a;人工智能在多模态与多组学中的最新研究进展 今天这期推文比较特殊&#xff0c;起来就开始干活&#xff0c;只能跑来会场写了。 小罗观点 今天这期推文覆盖面挺广的&#xff0c;前四篇与肿瘤治疗相关&#xff0c;并且两篇都直接与免疫微环境…

Websocket解析及用法(封装一个通用订阅发布主题的webSocket类)

1、什么是WebSocket? websocket的目标是通过一个长连接实现与服务器全双工&#xff0c;双向的通信。是一种在单个TCP连接上进行全双工通信的协议&#xff0c;使得客户端和服务器之间的数据交换变得更加简单&#xff0c;允许服务端主动向客户端推送数据。在 js中创建websocket…

统计信号处理基础 习题解答11-1

题目 观测到的数据具有PDF 在μ给定的条件下&#xff0c;是相互独立的。均值具有先验PDF&#xff1a; 求μ的 MMSE 和 MAP 估计量。另外&#xff0c;当和时将发生什么情况? 解答 和两者都是独立高斯分布&#xff0c;与例题10.1一致&#xff0c;直接套用&#xff08;10.11&am…

Nvidia Jetson/RK3588+AI双目立体相机,适合各种割草机器人、扫地机器人、AGV等应用

双目立体视觉是基于视差原理&#xff0c;依据成像设备从不同位置获取的被测物体的图像&#xff0c;匹配对应点的位置偏移&#xff0c;得到视差数据&#xff0c;进而计算物体的空间三维信息。为您带来高图像质量的双目立体相机&#xff0c;具有高分辨率、低功耗、远距离等优点&a…

ubuntu丢失网络/网卡的一种原因解决方案

现象 开机进入ubuntu后发现没有网络&#xff0c;无论是在桌面顶部状态栏的快捷键 还是 系统设置中&#xff0c;都没有”有线网“和”无线网“的选项&#xff0c;”代理“的选项是有的使用数据线连接电脑和手机&#xff0c;手机开启”通过usb共享网络“&#xff0c;还是没有任何…

Nginx 1.26.1最新版部署笔记

Nginx是一个高性能的 HTTP 和反向代理服务器&#xff0c;也是一个 IMAP/POP3/SMTP 代理服务器。 以下是 Nginx 的一些核心功能和特点&#xff1a; 高性能的 Web 服务器&#xff1a; Nginx 被设计为处理高并发连接&#xff0c;具有非常高的性能和稳定性。反向代理&#xff1a; …

分享3个AI工具-包括自学AI文档和AI搜索和智能体

文章目录 通往AGI之路-自学神器秘塔AI扣子 通往AGI之路-自学神器 这是是一个有关AI知识的开源文档。 但是&#xff0c;我认为这是小白学习AI的最强王者&#xff0c;每一个想学习AI、想使用AI的人都可以把它设为首页&#xff0c;从它开始。 飞书文档&#xff1a;通往AGI之路 …

Python 面试【★★★★】

欢迎莅临我的博客 &#x1f49d;&#x1f49d;&#x1f49d;&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

golang跨平台GUI框架fyne介绍与使用详解,开放案例

golang跨平台GUI框架fyne介绍与使用详解 Fyne 是一个使用 Go 编写的易于使用的 UI 工具包和应用程序 API。 它旨在构建使用单一代码库在桌面和移动设备上运行的应用程序。 通过批量调用身份证实名和三网手机实名和银行卡核验等接口&#xff0c;完成fyne框架的基本使用介绍 主要…

CVPR 2024 | 双手协作双物体的数据集TACO:引领可泛化手物交互的新方向

论文题目&#xff1a; TACO: Benchmarking Generalizable Bimanual Tool-ACtion-Object Understanding 论文链接&#xff1a; https://arxiv.org/pdf/2401.08399.pdf 项目主页&#xff1a; https://taco2024.github.io/ 视频链接&#xff1a; https://www.youtube.com/watch…

完全离线的本地问答模型LocalGPT如何实现无公网IP远程连接提问

文章目录 前言环境准备1. localGPT部署2. 启动和使用3. 安装cpolar 内网穿透4. 创建公网地址5. 公网地址访问6. 固定公网地址 前言 本文主要介绍如何本地部署LocalGPT并实现远程访问&#xff0c;由于localGPT只能通过本地局域网IP地址端口号的形式访问&#xff0c;实现远程访问…

技术驱动的音乐变革:AI带来的产业重塑

&#x1f4d1;引言 近一个月来&#xff0c;随着几款音乐大模型的轮番上线&#xff0c;AI在音乐产业的角色迅速扩大。这些模型不仅将音乐创作的门槛降至前所未有的低点&#xff0c;还引发了一场关于AI是否会彻底颠覆音乐行业的激烈讨论。从初期的兴奋到现在的理性审视&#xff0…

石家庄高校大学智能制造实验室数字孪生可视化系统平台项目验收

智能制造作为未来制造业的发展方向&#xff0c;已成为各国竞相发展的重点领域。石家庄高校大学智能制造实验室积极响应国家发展战略&#xff0c;结合自身优势&#xff0c;决定引进数字孪生技术&#xff0c;构建一个集教学、科研、生产于一体的可视化系统平台。 数字孪生可视化…

免费内网穿透、配置超级简单

巴比达内网穿透 曾经那些所谓的内网穿透服务&#xff0c;给我带来的只有无尽的烦恼。有的像&#xff0c;毫无规律地每天更改固定访问地址和端口。有一次&#xff0c;我正在进行一个重要的项目投标&#xff0c;需要及时与团队成员共享文件和沟通。可就在关键时刻&#xff0c;网络…

endswith()方法——是否以指定子字符串结尾

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 语法参考 endswith()方法用于检索字符串是否以指定子字符串结尾。如果是则返回True&#xff0c;否则返回False。endswith()方法的语法格式如下&…

启智畅想:AI集装箱箱号识别系统,解决方案提供商

AI集装箱箱号识别系统 当前,智能卡口管理行业正处于快速发展的阶段。随着物联网、大数据、人工智能等技术的不断进步,智能卡口管理系统已经能够实现对集装箱运输的全程跟踪、监控和管理,大大提高了管理效率和安全性。然而,市场上现有的智能卡口管理系统仍然存在一些痛点问题,如…

JAVA-矩阵置零

给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 思路&#xff1a; 找到0的位置&#xff0c;把0出现的数组的其他值夜置为0 需要额外空间方法&#xff1a; 1、定义两个布尔数组标记二维数组中行和列…