【C++11】thread线程库

【C++11】thread线程库

目录

  • 【C++11】thread线程库
    • thread类的简单介绍
      • 函数指针
      • lambda表达式常用在线程中
    • 线程函数参数
    • join与detach
        • 利用RAII思想来自动回收线程
    • 原子性操作库(atomic)
        • atomic中的load函数:
        • atomic中对变量进行原子操作的一些函数
      • CAS(Compare-And-Swap)无锁编程
        • CAS实现无锁队列
        • 尝试使用CAS编程实现++x
    • Mutex的种类
      • mutex
      • recursive_mutex(递归互斥锁
      • timed_mutex
        • chrono命名空间
      • lock_guard(RAII思想)
      • unique_lock
      • <condition_variable>头文件的介绍
        • 成员函数wait
        • wait对应的成员函数notify_one
      • 例题:控制两个线程交替打印奇数和偶数
    • 有关share_ptr智能指针中线程安全的问题
    • 有关单例模式中线程安全的问题

作者:爱写代码的刚子

时间:2024.3.24

前言:本篇博客将会介绍C++11中非常重要的部分——C++11的线程库,CAS无锁编程,有关share_ptr智能指针、单例模式等中多线程的问题

thread类的简单介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得 代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了(条件编译),使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。

函数名功能
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,args1,args2,…)构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数
get_id()获取线程id
joinable()线程是否还在执行,joinable代表的是一个正在执行中的线程。
join()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程 变为后台线程,创建的线程的“死活”就与主线程无关

注意:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
  2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

#include <thread>
int main() {std::thread t1;cout << t1.get_id() << endl;return 0;
}

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个 结构体:

// vs下查看
typedef struct
{ /* thread identifier for Win32 */void *_Hnd; /* Win32 HANDLE */unsigned int _Id;
} _Thrd_imp_t;

在这里插入图片描述

创建了一个线程对象但尚未开始执行该线程。在这种情况下,线程对象关联的线程 ID 可能是一个无效值,需要线程开始后再获取其id

  1. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一 般情况下可按照以下几种可执行对象提供:
  • 函数指针
  • 仿函数
  • lambda表达式
  • 包装器

C++的做法:

函数指针

  • 演示:

在这里插入图片描述

在这里插入图片描述

std::this_thread 命名空间中的函数是静态成员函数

在这里插入图片描述

  • **yield函数:**使线程主动让出执行权,以便让其他线程继续执行而不被阻塞

在这里插入图片描述

在这里插入图片描述

  1. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
  • 举例:

在这里插入图片描述

在这里插入图片描述

  • 但是下面的这种写法是错误的:

在这里插入图片描述

我们正确做法是将t2改为右值:

在这里插入图片描述

  1. 可以通过joinable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
  • 采用无参构造函数构造的线程对象

  • 线程对象的状态已经转移给其他线程对象

  • 线程已经调用join或者detach结束

  • 所以,不能在已经移动的线程对象上调用 join(),这会导致 std::system_error 异常,因为 t1 不再表示一个有效的线程。:

在这里插入图片描述

在这里插入图片描述

防止出现这种错误,还可以使用**joinable()**函数来规避

在这里插入图片描述

lambda表达式常用在线程中

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 总结:
#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;
}

线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改 后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参

#include <thread>
void ThreadFunc1(int& x)
{x += 10; 
}
void ThreadFunc2(int* x)
{*x += 10; 
}
int main() {int a = 10;// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的 是线程栈中的拷贝thread t1(ThreadFunc1, a);t1.join();cout << a << endl;// 如果想要通过形参改变外部实参时,必须借助std::ref()函数 thread t2(ThreadFunc1, std::ref(a);t2.join();cout << a << endl;// 地址的拷贝thread t3(ThreadFunc2, &a); t3.join();cout << a << endl;return 0;
}
  • 注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。

join与detach

启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?thread库给我们两种选择:

  • join()方式

join():主线程被阻塞,当新线程终止时,join()会清理相关的线程资源,然后返回,主线程再继续向下执行,然后销毁线程对象。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程对象只能使用一次join(),否则程序会崩溃。

在这里插入图片描述

// join()的误用一
void ThreadFunc() { cout<<"ThreadFunc()"<<endl; } 
bool DoSomething() { return false; }
int main()
{std::thread t(ThreadFunc);if(!DoSomething())return -1;t.join();return 0; 
}
/* 说明:如果DoSomething()函数返回false,主线程将会结束,join()没有调用,线程资源没有回收, 造成资源泄漏。
*/
// join()的误用二
void ThreadFunc() { cout<<"ThreadFunc()"<<endl; } 
void Test1() { throw 1; }
void Test2()
{int* p = new int[10];std::thread t(ThreadFunc);try{Test1(); }catch(...) {delete[] p;throw; }t.join(); 
}// 说明:与上述原因相似
利用RAII思想来自动回收线程

因此:采用join()方式结束线程时,join()的调用位置非常关键。为了避免该问题,可以采用RAII的方式 对线程对象进行封装,比如:

#include <thread>
class mythread
{
public:explicit mythread(std::thread &t) :m_t(t){}~mythread(){if (m_t.joinable())m_t.join();}mythread(mythread const&)=delete;mythread& operator=(const mythread &)=delete;
private:std::thread &m_t;
};
void ThreadFunc() { cout << "ThreadFunc()" << endl; }
bool DoSomething() { return false; }
int main() {thread t(ThreadFunc);mythread q(t);if (DoSomething())return -1;return 0; 
}
  • detach()方式

detach():该函数被调用后,新线程与线程对象分离,不再被线程对象所表达,就不能通过线程对象控 制线程了,新线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。

就像是你和你女朋友分手,那之后你们就不会再有联系(交互)了,而她的之后消费的各种资源也就不需要你去埋单了(清理资源)。

**detach()**函数一般在线程对象创建好之后就调用,因为如果不是join()等待方式结束,那么线程对象可能会在新线程结束之前被销毁掉而导致程序崩溃。因为std::thread的析构函数中,如果线程的状态是joinable,std::terminate将会被调用,而terminate()函数直接会终止程序。

在这里插入图片描述

因此,线程对象销毁前,要么以join()的方式等待线程结束,要么以detach()的方式将线程与线程对象分离。

原子性操作库(atomic)

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦

  • 例:

在这里插入图片描述

在这里插入图片描述

C++98中传统的解决方式:可以对共享修改的数据可以加锁保护。

#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
std::mutex m;
unsigned long sum = 0L;
void fun(size_t num)
{for (size_t i = 0; i < num; ++i){m.lock();sum++;m.unlock();} 
}
int main() {cout << "Before joining,sum = " << sum << std::endl;thread t1(fun, 10000000);thread t2(fun, 10000000);t1.join();t2.join();cout << "After joining,sum = " << sum << std::endl;return 0; 
}

虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。

同时,频繁地对较少的临界资源加锁会影响效率,不适合用互斥锁,会导致线程频繁阻塞,适合用自旋锁(但是库里面没有提供,也可以不断try_lock())。

在这里插入图片描述

  • 但是这个场景也不太适合用自旋锁,对CPU消耗也很大。

改进:

在这里插入图片描述

C++11中还引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。

在这里插入图片描述

注意:需要使用以上原子操作变量时,必须添加头文件(#include < atomic>

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。 更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

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

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

#include <atomic>
int main() {atomic<int> a1(0);//atomic<int> a2(a1);atomic<int> a2(0);//a2 = a1;return 0; 
}
  • atomic内部类似自旋(自旋锁用了atomic),自旋锁和atomic都适用于临界区很短的场景

在这里插入图片描述

atomic中的load函数:

在这里插入图片描述

  • C++官网示例:
// atomic::load/store example
#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::memory_order_relaxed
#include <thread>         // std::threadstd::atomic<int> foo (0);void set_foo(int x) {foo.store(x,std::memory_order_relaxed);     // set value atomically
}void print_foo() {int x;do {x = foo.load(std::memory_order_relaxed);  // get value atomically} while (x==0);std::cout << "foo: " << x << '\n';
}int main ()
{std::thread first (print_foo);std::thread second (set_foo,10);first.join();second.join();return 0;
}
  • 如果x是atomic类型的直接转换类型是不安全的

在这里插入图片描述

在这里插入图片描述

  • 正确写法:

在这里插入图片描述

atomic中对变量进行原子操作的一些函数

在这里插入图片描述

CAS(Compare-And-Swap)无锁编程

atomic在内核其实是CAS无锁编程

++x分为3步

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

CAS减少了线程切换上下文的次数,提高了效率(相比old值,相同则执行,不相同则再走一轮循环)

  • CAS原理(重要)

在这里插入图片描述

  • C++官网有关的函数(了解即可):

在这里插入图片描述

CAS实现无锁队列

在C++11中,new操作保证了内存分配(如果需要)和对象构造完成后, 才会将地址赋给instance,这保证了线程安全

在这里插入图片描述

  • 当两个线程都往同一个链表进行尾插时就会触发线程安全的问题(处理不好内存泄漏 !)

有关无锁编程的博客陈皓前辈的文章写的非常好!

尝试使用CAS编程实现++x

在这里插入图片描述

在这里插入图片描述

  • 注意C++11中要求atomic_compare_exchange_weak函数第一个参数是atomic模版类型

Mutex的种类

在多线程环境下,如果想要保证某个变量的安全性,只要将其设置成对应的原子类型即可,即高效又不容易出现死锁问题。但是有些情况下,我们可能需要保证一段代码的安全性,那么就只能通过锁的方式来进行控制。

mutex

在这里插入图片描述

  • mutex类:
    在这里插入图片描述

在C++11中,Mutex总共包了四个互斥量的种类:

  1. std::mutex C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
函数名函数功能
lock()上锁:锁住互斥量
unlock()解锁:释放对互斥量的所有权
try_lock()尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞

注意,线程函数调用lock()时,可能会发生以下三种情况:

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直 拥有该锁
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

线程函数调用try_lock()时,可能会发生以下三种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
  1. std::recursive_mutex

允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量 时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

  1. std::timed_mutex

比std::mutex多了两个成员函数,try_lock_for(),try_lock_until()。

  • try_lock_for()

    接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程 释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返 回 false。

  • try_lock_until()

    接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期 间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得 锁),则返回 false。

  1. std::recursive_timed_mutex
  • 实验:

注意,锁是不支持传值的!!!

  • lambda表达式可以直接捕获这个锁(规避完美转发问题):

在这里插入图片描述

在这里插入图片描述

  • 千万要注意以下这个坑:

【问题】:明明函数中锁的参数是一个引用且传递方式是正确的,但是还是发生了报错?

在这里插入图片描述

在这里插入图片描述

【解决且原因】:

在这里插入图片描述

原因:在C++中,std::ref() 是一个函数模板,它用于创建对给定对象的引用的引用包装器。std::ref() 函数通常与标准库中的多线程相关类一起使用,比如 std::thread

在多线程编程中,当我们想要将一个对象传递给线程函数,并且希望该线程函数可以修改这个对象时,我们通常需要将对象作为引用传递给线程函数。然而,std::thread 的构造函数是通过值传递参数的,这意味着如果我们直接传递一个对象给 std::thread,它将会被复制到新线程的栈上,而不是原始对象本身。为了避免这种复制,可以使用 std::ref()

在这里插入图片描述

【问题】:为什么std::thread 的构造函数是通过值传递参数的?

因为是将mtx传递给thread的构造函数,再将mtx传递给线程处理函数,如果是以mtx传递,thread实例化的时候会自动识别该类型,变为传值拷贝(因为传的就是mtx,属性被破坏了),要想保持mtx的引用属性则需要借助ref()函数,走一层完美转发(C++11)来保持属性。

完美转发:

在这里插入图片描述

本质由于模版的可变参数和引用折叠导致的问题

trylock函数

  • 演示:

在这里插入图片描述

在这里插入图片描述

recursive_mutex(递归互斥锁

在这里插入图片描述

递归互斥锁(Recursive Mutex)是一种互斥锁的变体,允许同一线程在持有锁的情况下多次获取该锁而不会发生死锁。在典型的互斥锁中,同一线程尝试再次获取已经持有的锁时会导致死锁,因为锁已经被该线程所占用。但是,递归互斥锁允许同一线程多次获取锁,每次获取都必须有相应的释放操作,这样可以保证线程在递归调用中能够正常工作而不会因为获取同一锁而阻塞自己。

  • 在递归函数里面使用mutex会导致死锁(尝试申请已持有的锁会死锁)

在这里插入图片描述

递归互斥锁原理:

递归互斥锁通常会维护一个计数器来记录某个线程已经获取锁的次数。当线程第一次获取锁时,计数器会增加;每次成功获取锁后,计数器会增加;每次释放锁时,计数器会减少。只有当计数器减为零时,锁才会完全释放。

timed_mutex

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

chrono命名空间

在这里插入图片描述

在这里插入图片描述

lock_guard(RAII思想)

  • 运行一段这样的代码

在这里插入图片描述

在这里插入图片描述

上述代码的缺陷:锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛 异常。(因为抛出异常后程序就不会执行到unlock()函数)

怎么解决?

因此: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类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要 加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域 前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

  • 写一个示例:

在这里插入图片描述

在这里插入图片描述

这样写是错的!!!正确写法:

在这里插入图片描述

在这里插入图片描述

这样就不会发生死锁了,锁会随着局部变量的生命周期而释放

  • 完整的测试代码:
// Created Time:    2024-03-24 22:43:31
// Modified Time:   2024-03-26 13:01:33
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <mutex>
using namespace std;void func()
{if(rand()%5==0){throw runtime_error("异常");}else{cout<<"func"<< endl;}
}template <class Lock>
class LockGuard
{
public:LockGuard(Lock& lk):_lk(lk)//锁不支持拷贝!!!{_lk.lock();}~LockGuard()     {_lk.unlock();}
private:Lock& _lk;
};
int main(int argc, char *argv[]) {mutex mtx;size_t n1=10000;size_t n2=10000;size_t x=0;srand(time(0));thread t1([n1,&x,&mtx](){try{  for(int i=0;i<n1;++i){LockGuard<mutex> lg(mtx);//mtx.lock();++x;cout<<"thread-------1"<<endl;func();//mtx.unlock();}}catch(const exception&e){cout<<e.what()<<endl;}});thread t2([n2,&x,&mtx](){for(int i=0;i<n2;++i){mtx.lock();++x;cout<<"thread-------2"<<endl;mtx.unlock();}});t1.join();t2.join();cout<<x<<endl;return 0;
}
  • 当然<mutex>库里面也提供了现成的lock_guard函数

在这里插入图片描述

在这里插入图片描述

unique_lock

在这里插入图片描述

在这里插入图片描述

一定要注意lock_guard和unique_lock的区别:

  • unique_lock支持手动解锁,再加锁

  • unique_lock可以和time_mutex配合使用

  • unique_lock可以和<condition_variable>条件变量进行配合

<condition_variable>头文件的介绍

在这里插入图片描述

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

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

在这里插入图片描述

我们发现wait函数用的是unique_lock,因为unique_lock函数能调用unlock()而lock_guard里面没有lock()成员函数。

同时注意,调用wait函数阻塞线程前会将锁unlock(),不然会死锁:

在这里插入图片描述

wait对应的成员函数notify_one

在这里插入图片描述

  • 还有一个相似的成员函数notify_all,这个函数不要随便用,使用不当可能会发生惊群现象(本质就是导致了无谓的资源竞争)

C++中的"惊群现象"通常指的是在多线程编程中的一种性能问题,特别是在使用互斥锁时出现的情况。当多个线程被阻塞等待同一个资源时,一旦该资源可用,所有线程都会被唤醒,即使只有一个线程真正需要该资源。这种情况下,会导致不必要的竞争和上下文切换,降低了程序的性能。

举个例子,假设有多个线程等待某个共享资源的释放,一旦资源可用,所有这些线程都会被唤醒。然后它们开始竞争获取资源的访问权限,但实际上只有一个线程可以获得资源并继续执行,其他线程会再次被阻塞。这种情况下,除了获得资源的线程之外,其他线程被唤醒是没有必要的,这就是"惊群现象"。

想要减少惊群现象的发生,可以采用更加精细的同步机制,例如使用条件变量(condition variables)来唤醒等待的线程,只有当条件满足时才唤醒需要的线程。另外,也可以考虑使用更轻量级的同步原语,如自旋锁(spinlock),以减少上下文切换的开销。

例题:控制两个线程交替打印奇数和偶数

  • 错误示例(一个线程加锁并等待,另一个线程用于唤醒):

  • 因为是多线程,t2调用notify_one唤醒的时候可能t1还没有wait等待

【优解】:

注释中有些注意的地方:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
int main()
{mutex mtx;int x=1;condition_variable cv;bool flag=false;//如何保证t2线程先运行?thread t1([&](){for(size_t i=0;i<10;++i){unique_lock<mutex> lock(mtx);while(flag)//这里if和while都可以,用while是为了防止notify_one()失败,但是理论上不会失败。{cv.wait(lock);}cout<<this_thread::get_id()<<":"<<x<<endl;++x;flag = true;cv.notify_one();}});thread t2([&](){for(size_t i=0;i<10;++i){unique_lock<mutex> lock(mtx);while(!flag){cv.wait(lock);}cout<<this_thread::get_id()<<":"<<x<<endl;++x;flag = false;cv.notify_one();}});t1.join();t2.join();return 0;
}

在这里插入图片描述

在这里插入图片描述

【讨论】:

  • 场景1:

假设t1先运行,t1先抢到lock,flag是false,t1先打印,flag改成true

t2两种情况:

a、没启动起来,或者没有分到时间片->t2总会开始运行,lock,flag是true,他不会wait t2打印值,flag改成false,notify唤醒t1.后续就是类似交替运行

b、运行起来,lock阻塞

notify没有线程等待,出作用域解锁

a、如果t2是在a状况,t1又抢到锁,但是flag为true,wait阻塞(unlock)

b、如果t2是在b状况,t1解锁,唤醒t2,t2获取到锁,flag是true,t2不会阻塞打印

  • 场景2:

t2先启动,t2会lock,wait(unlock)

t1两种状况:

a、没启动,或者没分到时间片。->t1总会分到时间片运行,lock,打印,flag改成true,notify t2

b、t1慢一步,但是也分到时间片开始执行了,t1 lock阻塞,t2wait时,unlock会唤醒t1获取锁,保证了t1先运行

有关share_ptr智能指针中线程安全的问题

  • share_ptr源码
template <class T>
class shared_ptr
{
public:// RAII// 像指针一样shared_ptr(T *ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}// function<void(T*)> _del;template <class D>shared_ptr(T *ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}~shared_ptr(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;// delete _ptr;_del(_ptr);delete _pcount;}}T &operator*(){return *_ptr;}T *operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T> &sp): _ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}shared_ptr<T> &operator=(const shared_ptr<T> &sp){if (_ptr == sp._ptr)return *this;if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);return *this;}int use_count() const{return *_pcount;}T *get() const{return _ptr;}private:T *_ptr;int *_pcount;function<void(T *)> _del = [](T *ptr){ delete ptr; };
};
  • 因为多线程可能导致多个线程对引用计数进行++,可能存在线程安全的问题。

  • 添加以下测试代码:

在这里插入图片描述

在这里插入图片描述

结果在情理之中,报错了。

【解决】:添加atomic来对引用计数进行原子操作

在这里插入图片描述

在这里插入图片描述

注意:shared_ptr本身是线程安全的,但是指向的资源不是线程安全的

与unique_lock配合处理:

在这里插入图片描述

在这里插入图片描述

  • 完整的代码:
#include <iostream>
#include <functional>
#include <atomic>
#include <mutex>
#include <thread>using namespace std;template <class T>
class shared_ptr
{
public:// RAII// 像指针一样shared_ptr(T *ptr = nullptr): _ptr(ptr), _pcount(new atomic<int>(1)){}// function<void(T*)> _del;template <class D>shared_ptr(T *ptr, D del): _ptr(ptr), _pcount(new atomic<int>(1)), _del(del){}~shared_ptr(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;// delete _ptr;_del(_ptr);delete _pcount;}}T &operator*(){return *_ptr;}T *operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T> &sp): _ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}shared_ptr<T> &operator=(const shared_ptr<T> &sp){if (_ptr == sp._ptr)return *this;if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);return *this;}int use_count() const{return *_pcount;}T *get() const{return _ptr;}private:T *_ptr;atomic<int> *_pcount;function<void(T *)> _del = [](T *ptr){ delete ptr; };
};void test_share_ptr()
{mutex mtx;shared_ptr<double> sp(new double(1.1));thread t1([&](){for(size_t i=0;i<1000;++i){shared_ptr<double> copy(sp);{//局部域unique_lock<mutex> lock(mtx);++(*copy);}}});thread t2([&](){for(size_t i=0;i<1000;++i){shared_ptr<double> copy(sp);{unique_lock<mutex> lock(mtx);++(*copy);}}});t1.join();t2.join();cout<<sp.use_count()<<endl;cout<<(*sp)<<endl;
}int main()
{test_share_ptr();return 0;
}

有关单例模式中线程安全的问题

  • 单例模式源码:
namespace hungry
{class Singleton{public:// 2、提供获取单例对象的接口函数static Singleton& GetInstance(){return _sinst;}void func();void Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}void Print(){for (auto& e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}private:// 1、构造函数私有Singleton(){// ...}// 3、防拷贝Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;map<string, string> _dict;// ...static Singleton _sinst;};Singleton Singleton::_sinst;void Singleton::func(){// _dict["xxx"] = "1111";}
}namespace lazy
{class Singleton{public:// 2、提供获取单例对象的接口函数static Singleton& GetInstance(){if (_psinst == nullptr){// 第一次调用GetInstance的时候创建单例对象_psinst = new Singleton;}return *_psinst;}// 一般单例不用释放。// 特殊场景:1、中途需要显示释放  2、程序结束时,需要做一些特殊动作(如持久化)static void DelInstance(){if (_psinst){delete _psinst;_psinst = nullptr;}}void Add(const pair<string, string>& kv){_dict[kv.first] = kv.second;}void Print(){for (auto& e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}class GC{public:~GC(){lazy::Singleton::DelInstance();}};private:// 1、构造函数私有Singleton(){// ...}~Singleton(){cout << "~Singleton()" << endl;// map数据写到文件中FILE* fin = fopen("map.txt", "w");for (auto& e : _dict){fputs(e.first.c_str(), fin);fputs(":", fin);fputs(e.second.c_str(), fin);fputs("\n", fin);}}// 3、防拷贝Singleton(const Singleton& s) = delete;Singleton& operator=(const Singleton& s) = delete;map<string, string> _dict;// ...static Singleton* _psinst;static GC _gc;};Singleton* Singleton::_psinst = nullptr;Singleton::GC Singleton::_gc;
}

饿汉模式由于一上来就创建对象,所以不存在线程安全的问题。

  • 而懒汉模式存在线程安全的问题:

在这里插入图片描述

多线程中这一步明显存在问题,_psinst可能会被不同线程赋值

【解决】:

在这里插入图片描述

在这里插入图片描述

  • 修改后的源码:
#include <iostream>
#include <map>
#include <mutex>
using namespace std;
namespace hungry
{class Singleton{public:// 2、提供获取单例对象的接口函数static Singleton &GetInstance(){return _sinst;}void func();void Add(const pair<string, string> &kv){_dict[kv.first] = kv.second;}void Print(){for (auto &e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}private:// 1、构造函数私有Singleton(){// ...}// 3、防拷贝Singleton(const Singleton &s) = delete;Singleton &operator=(const Singleton &s) = delete;map<string, string> _dict;// ...static Singleton _sinst;};Singleton Singleton::_sinst;void Singleton::func(){//_dict["xxx"] = "1111";}
}namespace lazy
{class Singleton{public:// 2、提供获取单例对象的接口函数static Singleton &GetInstance(){if (_psinst == nullptr) // 双检查来保证效率,使其之后不用频繁申请锁(保护第一次){unique_lock<mutex> lock(_mtx); // 锁必须放外面,因为线程是接着运行的,只要进去了if语句,后面就又会进去执行if (_psinst == nullptr){// 第一次调用GetInstance的时候创建单例对象_psinst = new Singleton;}}return *_psinst;}// 一般单例不用释放。// 特殊场景:1、中途需要显示释放  2、程序结束时,需要做一些特殊动作(如持久化)static void DelInstance(){if (_psinst){delete _psinst;_psinst = nullptr;}}void Add(const pair<string, string> &kv){_dict[kv.first] = kv.second;}void Print(){for (auto &e : _dict){cout << e.first << ":" << e.second << endl;}cout << endl;}class GC{public:~GC(){lazy::Singleton::DelInstance();}};private:// 1、构造函数私有Singleton(){// ...}~Singleton(){cout << "~Singleton()" << endl;// map数据写到文件中FILE *fin = fopen("map.txt", "w");for (auto &e : _dict){fputs(e.first.c_str(), fin);fputs(":", fin);fputs(e.second.c_str(), fin);fputs("\n", fin);}}// 3、防拷贝Singleton(const Singleton &s) = delete;Singleton &operator=(const Singleton &s) = delete;map<string, string> _dict;// ...static Singleton *_psinst;static mutex _mtx;static GC _gc;};Singleton *Singleton::_psinst = nullptr;Singleton::GC Singleton::_gc;
}
  • 最简单的懒汉模式:

在这里插入图片描述


//最简单的懒汉
namespace lazy
{class Singleton{public:// 2、提供获取单例对象的接口函数static Singleton &GetInstance(){//局部的静态对象,是在第一次调用时初始化,所以没有线程安全!//C++11之前的编译器,这个代码是不安全的//C++11之后可以保证局部静态对象的初始化是线程安全的,只初始化一次(C++11之前,这是一个缺陷)static Singleton inst;return inst;}private:// 1、构造函数私有Singleton(){cout<<"Singleton()"<<endl;}// 3、防拷贝Singleton(const Singleton &s) = delete;Singleton &operator=(const Singleton &s) = delete;  };
}
  • 局部的静态对象,是在第一次调用时初始化,所以没有线程安全!
  • C++11之前的编译器,这个代码是不安全的
  • C++11之后可以保证局部静态对象的初始化是线程安全的,只初始化一次**(C++11之前,这是一个缺陷)**

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

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

相关文章

VGG16神经网络搭建

一、定义提取特征网络结构 将要实现的神经网络参数存放在列表中&#xff0c;方便使用。 数字代表卷积核的个数&#xff0c;字符代表池化层的结构 cfgs {"vgg11": [64, M, 128, M, 256, 256, M, 512, 512, M, 512, 512, M],VGG13: [64, 64, M, 128, 128, M, 256, …

「11」显示器采集:捕获单个显示器的完整视频画面

「11」显示器采集捕获单个显示器的完整视频画面 在OBS软件中&#xff0c;「显示器采集」是一种用于捕集显示器屏幕画面的功能&#xff0c;您可以将其用于整个桌面窗口的采集到直播间。该功能主要用于捕捉您的计算机桌面屏幕内容&#xff0c;以便将其实时显示在直播窗口中&#…

MySQL Explain 字段详解

Explain 工具介绍 Explain 一般被称为解释器&#xff0c;通过 Explain 工具&#xff0c;我们能分析我们使用的查询语句或是结构的性能瓶颈&#xff0c;它提供 MySQL 如何执行语句的信息。 使用语法&#xff1a; explain [extended|partition] select在 select 关键字前加 ex…

第 1 章.提示词:开启AI智慧之门的钥匙

什么是提示词&#xff1f; 提示词&#xff0c;是引导语言模型的指令&#xff0c;让用户能够驾驭模型的输出&#xff0c;确保生成的文本符合需求。 ChatGPT&#xff0c;这位文字界的艺术大师&#xff0c;以transformer架构为基石&#xff0c;能轻松驾驭海量数据&#xff0c;编织…

R 生存分析3:Cox等比例风险回归及等比例风险检验

虽然Kaplan-Meier分析方法目前应用很广&#xff0c;但是该方法存在一下局限: 对于一些连续型变量&#xff0c;必须分类下可以进行生存率对比 是一种单变量分析&#xff0c;无法同时对多组变量进行分析 是一种非参数分析方法&#xff0c;必须有患者个体数据才能进行分析 英国…

阳光倒灌高准直汽车抬头显示器HUD太阳光模拟器

阳光倒灌高准直汽车抬头显示器HUD太阳光模拟器是一种高级别的模拟设备&#xff0c;用于模拟太阳光的光谱、强度及照射角度&#xff0c;应用于太阳能电池板、光伏系统等领域的研究和测试。其参数包括光谱范围、光强度、光源、照射角度、均匀性和稳定性&#xff0c;可根据需求调整…

ubuntu20.04安装截图工具flameshot

ubuntu20.04 自带的截图工具&#xff0c;可以使用快捷键“shift printScreen” ,但是它不能对截图进行编辑。 现在安装截图工具 flameshot&#xff0c;使用以下命令&#xff1a; sudo apt install flameshot 安装完成后&#xff0c;使用以下命令打开&#xff1a; flamesho…

Go 语言基础语法

目录 行分隔符 注释 标识符 字符串连接 关键字 Go 语言的空格 格式化字符串 Printf 实例 Go 语言变量 变量声明 多变量声明 值类型和引用类型 简短形式&#xff0c;使用 : 赋值操作符 Go 程序可以由多个标记组成&#xff0c;可以是关键字&#xff0c;标识符&#…

C++ 之LeetCode刷题记录(四十)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 开始cpp刷题之旅。 目标&#xff1a;执行用时击败90%以上使用 C 的用户。 27. 移除元素 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值…

文献速递:文献速递:基于SAM的医学图像分割--SAM-Med3D

Title 题目 SAM-Med3D 01 文献速递介绍 医学图像分析已成为现代医疗保健不可或缺的基石&#xff0c;辅助诊断、治疗计划和进一步的医学研究]。在这一领域中最重要的挑战之一是精确分割体积医学图像。尽管众多方法在一系列目标上展现了值得称赞的有效性&#xff0c;但现有的…

jmeter二次开发发送java请求_保姆级教程!!!

一、引言 JMeter是Apache基金会开发的一款开源性能测试工具&#xff0c;广泛应用于软件性能测试领域。它能够模拟多线程并发用户对应用程序进行压力测试&#xff0c;以评估应用程序的性能和稳定性。然而&#xff0c;在实际使用过程中&#xff0c;用户可能会遇到需要发送Java请…

RHCE实验-建立NFS服务器,使的客户端顺序共享数据

第一步&#xff1a;服务端及客户端的准备工作 # 恢复快照[rootserver ~]# setenforce 0​[rootserver ~]# systemctl stop firewalld​[rootserver ~]# yum install nfs-utils -y # 服务端及客户端都安装 第二步&#xff1a;服务端建立共享文件目录&#xff0c;并设置权限…

Vue3 新项目默认工程文件理解

Vue3 新项目默认工程文件理解 文章目录 Vue3 新项目默认工程文件理解0、工程文件结构图1、main.ts2、index.html源文件编译后 3、App.vue4、.d.ts 文件作用 0、工程文件结构图 1、main.ts // 引入 createApp 函数 import { createApp } from vue // 引入 style.css 文件&#…

【QT+QGIS跨平台编译】之九十五:【QGIS_App跨平台编译】—【错误处理:找不到标识符QwtPolarPoint】

文章目录 一、找不到标识符QwtPolarPoint二、原因分析三、错误处理一、找不到标识符QwtPolarPoint 报错信息如下: 二、原因分析 未找到QWT_POLAR_VERSION 从而执行的函数: mypMarker->setPosition( QwtPolarPoint( currentInfo.azimuth, currentInfo.elevation )

DevSecOps平台架构系列-微软云Azure DevSecOps平台架构

目录 一、概述 二、Azure DevOps和黄金管道 2.1 概述 2.2 Azure DevOps架构说明 2.2.1 架构及管道流程图 2.2.2 架构内容 2.2.2.1 Azure Boards 2.2.2.2 Azure Repos 2.2.2.3 Azure Test Plans 2.2.2.4 Azure Pipelines 2.2.2.5 Azure Application Insights 2.2.2.6…

浏览器https受信任证书生成——openssl颁发受信任证书

站点常常由于没有受信任的第三方CA机构颁发证书,使用https访问时,浏览器常常会弹出不安全的提示,为解决该问题,可以使用openssl颁发个人证书来解决该问题。 1openssl安装及使用方式参考:32.9 x509_OpenSSL 中文手册https://www.openssl.net.cn/docs/230.html2.本文章所有生…

java打家劫舍2(力扣Leetcode213)

打家劫舍2 力扣原题链接 问题描述 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋&#xff0c;每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈&#xff0c;这意味着第一个房屋和最后一个房屋是紧挨着的。同时&#xff0c;相邻的房屋装有相互连通的防盗系统&a…

相机显示储存卡未格式化怎么回事?怎么办

在摄影的学习和实践中&#xff0c;相机是我们记录美好瞬间的得力助手。然而&#xff0c;当相机突然提示储存卡未格式化时&#xff0c;这往往会让我们感到困惑和焦虑。本文将探讨相机显示储存卡未格式化的可能原因&#xff0c;并提供相应的解决方案。 图片来源于网络&#xff0c…

web自动化测试系列-selenium的运行原理和常用方法介绍(二)

目录 1.selenium的运行原理 2.常用方法介绍 接上文 &#xff1a;web自动化测试系列-selenium的安装和运行(一)-CSDN博客 在上文中我们编写了一段简单的代码 &#xff0c;可以驱动浏览器访问百度并搜索关键字 。这里我们再把这段代码再拿来加以说明 。 # 1. 导包 from selen…

搭建Spark单机版环境

在搭建Spark单机版环境的实战中&#xff0c;首先确保已经安装并配置好了JDK。然后&#xff0c;从群共享下载Spark安装包&#xff0c;并将其上传至目标主机的/opt目录。接着&#xff0c;解压Spark安装包至/usr/local目录&#xff0c;并配置Spark的环境变量&#xff0c;以确保系统…