从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)

目录

1. C++多线程

1.1 thread库

1.2 mutex库

1.3 RAII锁

1.4 atomic+CAS

1.5 condition_variable

1.6 分别打印奇数和偶数

2. shared_ptr线程安全

2.1 库里面的shared_ptr使用

2.2 shared_ptr加锁代码

3. 单例模式线程安全

3.1 懒汉模式线程安全问题

3.2 懒汉模式最终代码

3.3 懒汉模式的另一种写法

本篇完。


此篇建议学了Linux系统多线程部分再来看。

1. C++多线程

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。

C++11中最重要的特性就是支持了多线程编程,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。

1.1 thread库

查下文档:

如图所示,C++11提供了thread库,thread是一个类,在使用的时候需要包含头文件pthread。

构造函数:

  • 默认构造函数thread()使用该构造函数创建的线程对象仅是创建对象,线程并没有被创建,也没有允许。
  • thread(Fn&& fn, Args&&... args),这是一个万能引用模板。使用该构造函数时,第一个参数是可调用对象,可以是左值也可以是右值,比如函数指针,仿函数对象,lambda表达式等等。后面的可变参数就是传给线程函数的实参,是一个参数包,也就是可变参数。
  • thread(const thread&) = delete,线程之间是禁止拷贝的。
  • thread(thread&& x),移动构造函数。

成员函数:

  •  get_id,用来获取当前线程的tid值。调用该函数通常都是当前线程,但是当前的从线程从并没有自己的thread对象

所以线程库由提供了一个命名空间,该空间中有上图所示的几个函数,可以通过命名空间来直接调用,如:

this_thread::get_id(); // 获取当前线程tid值

哪个线程执行这条语句就返回哪个线程的tid值,命名空间中的其他几个函数的用法也是这样。

  • yield调用该接口的线程会让其CPU,让CPU调度其他线程。
  • sleep_until调用该接口的线程会延时至一个确定的时间点。
  • sleep_for调用该接口的线程会延时一个时间段,如1s。

  •  operator=(thread&& t),移动赋值。

将一个线程对象赋值给另一个线程对象,通常这么用:

	thread t1; // 仅创建对象,不创建线程
	t1 = thread(func); // t1线程函数并且执行

此时原本只创建的线程对象就有一个线程在跑了。

注意:只能赋右值,不能赋左值,因为赋值运算符重载被禁掉了,只有移动赋值


  •  join,线程等待,用来回收线程资源。一般主线程会调用该函数,以t.join()的形式,t就是需要被等待的线程对象,此时主线程会阻塞在这里,直到从线程运行结束。

如上面的多线程一样,必须使用join,否则线程资源不会回收,而且如果从线程运行的时间比主线程长的话,主线程会直接运行完并且回收所有资源,导致从线程被强制结束。


  •  joinable,用来判断线程是否有效。

如果是以下任意情况则线程无效:

  1. 采用无参构造函数构造的线程对象
  2. 线程对象的状态已经转移给其他线程对象
  3. 线程已经调用 join 或者 detach 结束

  • detach,线程分离,从线程结束后自动回收资源。

其他的就不介绍了,用到的时候自行查文档即可。

要谨记:thread是禁止拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值


使用一下:

#include <iostream>
#include <thread>
using namespace std;

void Print(int n, int& x)
{
	for (int i = 0; i < n; ++i)
	{
		cout << this_thread::get_id() << ":" << i << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		++x;
	}
}
int main()
{
	int count = 0;
	thread t1(Print, 10, ref(count));
	thread t2(Print, 10, ref(count));

	t1.join();
	t2.join();

	cout << count << endl;

	return 0;
}

多次运行的结果不一样,可能会出现像第一行一样的抢着打印的问题(学了Linux多线程应该比较清楚),下面就应该想到加锁了。


1.2 mutex库

如上图所示,C++11提供了mutex库,mutex同样是一个类,在使用的时候要包含头文件mutex。

构造函数:

  • 只有默认构造函数mutex(),在创建互斥锁的时候不需要传任何参数。
  • mutex(const mutex&)=delete,禁止拷贝。

其他成员函数:

  • lock(),给临界区加锁,加锁成功继续向下执行,失败则阻塞等待。
  • unlock(),给临界区解锁。
  • try_lock(),给临界区尝试加锁,加锁成功返回true,加锁失败返回false使用try_lock时,如果申请失败则不阻塞,跳过申请锁的部分,执行非临界区代码。

来看伪代码:

mutex mtx;

if(mtx.try_lock())
{
	// 临界区代码
	// ......
}
else
{
	// 非临界区代码
	// ......
}

mutex不能递归使用,如下面伪代码所示:

    void Func(int n)
	{
		lock(); // 加锁
		// 临界区代码
		// ......
		Func(n - 1); // 递归调用
		unlock(); // 解锁
	}

在递归中不能使用这样的锁,会造成死锁。正确使用下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

void Print(int n, int& x, mutex& mtx)
{
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();
		cout << this_thread::get_id() << ":" << i << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		++x;
		mtx.unlock();
	}
}

int main()
{
	mutex m;
	int count = 0;
	thread t1(Print, 10, ref(count), ref(m));
	thread t2(Print, 10, ref(count), ref(m));

	t1.join();
	t2.join();
	cout << count << endl;
	return 0;
}

后面再来看看怎么实现交错打印的效果,再看看另一种用法:(lambda)

int main()
{
	mutex mtx;
	int x = 0;
	int n = 10;
	thread t1([&](){
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();
			cout << this_thread::get_id() << ":" << i << endl;
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			++x;
			mtx.unlock();
		}
	});

	thread t2([&](){
		for (int i = 0; i < n; ++i)
		{
			mtx.lock();
			cout << this_thread::get_id() << ":" << i << endl;
			std::this_thread::sleep_for(std::chrono::milliseconds(100));
			++x;
			mtx.unlock();
		}
	});

	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

上面代码的问题:如果加锁解锁之间存在抛异常就死锁了,这时就要用到RAII锁。


1.3 RAII锁

lock_guard是一个类,采用了RAII方式来加锁解锁——将锁的生命周期和对象的生命周期绑定在一起。看下在Linux篇章写的代码:(把锁封装了)

#pragma once
#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t* mtx) 
        :_pmtx(mtx)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmtx);
        std::cout << "进行加锁成功" << std::endl;
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
        std::cout << "进行解锁成功" << std::endl;
    }
    ~Mutex()
    {}
protected:
    pthread_mutex_t* _pmtx;
};

class lockGuard // RAII风格的加锁方式
{
public:
    lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化
        :_mtx(mtx)
    {
        _mtx.lock();
    }
    ~lockGuard()
    {
        _mtx.unlock();
    }
protected:
    Mutex _mtx;
};

看库里的构造函数:

  • lock_guard(mutex_type& m),在创建这个对象的时候需要传入一把锁,在构造函数中,进行了加锁操作。
  • lcok_guard(const lock_guard&)=delete,该对象禁止拷贝,因为互斥锁就不可以拷贝。

析构函数的作用就是将lock_guard对象的资源释放,也就是进行解锁操作。

lock_guard只有构造函数和析构函数,使用该类对象加锁时不需要我们去关心锁的释放,但是它不能在对象生命周期结束之前主动解锁。

看一下unique_lock:

unique_lock也是一种RAII的加锁对象,它和lock_guard的功能一样,将锁的生命周期和对象的生命周期绑定在一起,但是又有区别。

  • unique_lock(mutex_type& m),这个和lock_guard的用法一样,在构造函数中加锁。
  • unique_lock(const unique_lock&)=delete,同样禁止拷贝。

析构函数中和lock_guard一样,也是进行解锁操作。

  • lock,加锁。
  • unlock,解锁。
  • try_lock,尝试加锁。

lock_guard中就没有这几个接口,所以unique_lock可以在析构之前主动解锁,主动解锁后仍然可以再主动加锁,这一点lock_guard是不可以的。

  • try_lock_for,尝试加锁一段时间,时间到后自动解锁。
  • try_lock_until,尝试加锁到指定时间,时间到来后自动解锁。

用法很多,需要使用的时候可以结合库文档来使用。用一下lock_guard+lambda的另一种用法:

int main()
{
	mutex mtx;
	int n = 10;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				{
					lock_guard<mutex> lk(mtx);
					cout << this_thread::get_id() << ":" << i << endl;
				}
				std::this_thread::sleep_for(std::chrono::milliseconds(100));
			}
		});
	}
	for (auto& t : v)
	{
		t.join();
	}
	return 0;
}

1.4 atomic+CAS

C++11提供了原子操作,我们知道,线程不安全的主要原因就是访问某些公共资源的时候,操作不是原子的,如果让这些操作变成原子的后,就不会存在线程安全问题了。

CAS原理:

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

  • CAS包含三个操作数:内存位置(V),预期原值(A)和新值(B)。
  • 如果内存位置的值与预期原值相等,那么处理器就会自定将该位置的值更新为新值。
  • 如果内存位置的值与预期原值不相等,那么处理器不会做任何操作。

val是临界资源,两个线程t1和t2同时对这个值进行加加操作,每个线程都是将该值先拿到寄存器eax中。

  • 线程将val值拿到寄存器eax中时,同时将该值放入原值V中。
  • 在修改val值之前,CPU会先判断eax中的值与原值V中的值是否相等,如果相等则修改并且更新值,如果不相等则不修改。

伪代码原理:

while(1)
{
	eax = val; // 将val值取到寄存器eax中
	if(eax == V) // 和原值相同可以修改
	{
		eax++;
		V = eax; // 修改原值
		val = eax; // 修改val值
		break; // 访问结束,跳出循环
	}
}
  •  t1和t2虽然同时运行,但是时间粒度划分到极小的时候,CPU仍然是一个个在执行。

t1线程将val值拿到寄存器中,并且赋原值,经过判断发现和原值相同,所以修改val值,并放回到val的地址中。

此时t2线程被唤醒,它将val值拿到寄存器中后与最开始的原值V相比,发现不相同了,所以就不进行修改,而且继续循环,知道寄存器中的值和原值相等才会改变。

  • 原子操作虽然保证了线程安全,但是另一个无法写的的线程会不停的循环,而这也会占用一定的CPU资源。

CAS具体的原理有兴趣可以自行去了解,深入了解后写在简历是加分项。


atomic也是一个类,所以也有构造函数:

 经常使用的是atomic(T val),在创建的时候传入我们想要进行原子操作的变量。

int a = atomic(1);

此时变量a的操作就都成了原子操作了,在多线程访问的时候可以保证线程安全。

成员函数:

该类重载了++,–等运算符,可以直接对变量进行操作。

看看没用atomic也没加锁的:

int main()
{
	mutex mtx;
	int x = 0;
	int n = 100000;
	int m = 2;
	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				++x;
			}
		});
	}
	for (auto& t : v)
	{
		t.join();
	}
	cout << x << endl;
	return 0;
}

两个线程互相抢着加,就会出现有一个线程没加的情况,看看加锁的:

再看看用atomic的:

和加锁效果一样。


1.5 condition_variable

C++11中同样也有条件变量,用来实现线程的同步。

构造函数:

在创建条件变量的时候不用传入参数,同样是不允许被拷贝的。


其他成员函数:

放入等待队列:

wait(unique_lock<mutex>& lock),该接口是将调用它的线程放入到条件变量的等待队列中。
wait(unique_lock<mutex>& lck, Predicate pred),该接口和上面的作用一样,只是多了一个pred参数,当这个参数为true的话不放入等待队列,为false时放入等待队列。

这里传入的锁是unique_lock而不是lock_guard。

这是因为,当一个线程申请到锁进入临界区,但是条件不满足被放入条件变量的等待队列中时,会将申请到的锁释放。

lock_guard只能在对象生命周期结束时自动释放锁。

unique_lock可以在任意位置释放锁。

如果使用了lock_guard的话就无法在进入等待队列的时候释放锁了。


wait_for和wait_until都是等待指定时间,一个是在等待队列中待指定时间,另一个是在等待队列中带到固定的时间点后自定唤醒。

notify_one唤醒等待队列中的一个线程,notify_all唤醒等待队列中的所有线程。


1.6 分别打印奇数和偶数

写一个程序:支持两个线程交替打印,一个打印奇数,一个打印偶数。

分析:

  • 首先创建一个全局的变量val,让两个线程去访问该变量并且进行加一操作。
  • 考虑到线程安全,所以需要给对应的临界区加互斥锁mutex
  • 又是交替打印,所以要使用条件变量condition_variable来控制顺序,为了方便管理,使用的锁是unique_lock<mutex>

代码实现:

int main()
{
	int val = 0;
	int n = 10; // 打印的范围
	mutex mtx; // 创建互斥锁
	condition_variable cond; // 创建条件变量

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

	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;
}

上面代码两个线程执行的函数对象是lambda表达式,所以创建线程对象时,调用的是移动构造函数。

  • wait()的第二个参数是false的时候,该线程被挂起到等待队列中,是true的时候不挂起,而且执行向下执行。
  • 第二个参数的false和true可以是返回值,如代码就是使用的lambda表达式的返回值。

线程t1负责打印奇数,t2负责打印偶数,两个线程通过条件变量的控制交替打印。

还可以这么用:

int main()
{
	int val = 0;
	int n = 10; // 打印值的范围
	mutex mtx;
	condition_variable cond;
	bool ready = true;

	// t1线程打印奇数
	thread t1([&](){
		while (val < n)
		{
			{
				unique_lock<mutex> lock(mtx);
				cond.wait(lock, [&ready](){return !ready; });

				cout << "thread1:" << this_thread::get_id() << "->" << val << endl;
				val += 1;

				ready = true;

				cond.notify_one();
			}

			//this_thread::yield();
			this_thread::sleep_for(chrono::microseconds(10));
		}
	});

	// t2线程打印偶数
	thread t2([&]() {
		while (val < n)
		{
			unique_lock<mutex> lock(mtx);
			cond.wait(lock, [&ready](){return ready; });

			cout << "thread2:" << this_thread::get_id() << "->" << val << endl;
			val += 1;
			ready = false;

			cond.notify_one();
		}
	});

	t1.join();
	t2.join();

	return 0;
}

成功按照预期打印。


2. shared_ptr线程安全

智能指针复习:从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr-CSDN博客

以前敲的shared_ptr(加一个返回引用计数的接口):

namespace rtx
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pCount(new int(1))
		{}

		void Release()
		{
			if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数
			{
				delete _ptr;
				delete _pCount;
			}
		}
		~shared_ptr()
		{
			Release();
		}

		shared_ptr(const shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pCount(sp._pCount)
		{
			(*_pCount)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2;
			{                    // 比较_pCount也行
				//if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数
				//{
				//	delete _ptr;
				//	delete _pCount;
				//}
				Release();

				_ptr = sp._ptr;
				_pCount = sp._pCount;
				(*_pCount)++;
			}
			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		int use_count()
		{
			return *_pCount;
		}
	protected:
		T* _ptr;
		int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员
	};
}

先看看库里面的使用:

int main()
{
	std::shared_ptr<double> sp1(new double(7.77));
	std::shared_ptr<double> sp2(sp1);

	mutex mtx;

	vector<thread> v(5);
	int n = 100000;
	for (auto& t : v)
	{
		t = thread([&](){
			for (size_t i = 0; i < n; ++i)
			{
				// 拷贝是线程安全的
				std::shared_ptr<double> sp(sp1);

				// 访问资源不是
				(*sp)++;
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}
	cout << *sp1 << endl;
	cout << sp1.use_count() << endl;
	return 0;
}

2.1 库里面的shared_ptr使用

能指针共同管理的动态内存空间是线程不安全的,访问资源要自己加锁:

再把std换成自己的命名空间:

程序直接崩溃了,因为有时候引用计数不对。

多个线程及主线程中的所有智能指针都共享引用计数,又因为拷贝构造以及析构都不是原子的,所以导致线程不安全问题。

解决办法和Linux中一样,需要加锁:

引用计数加加和减减都要加锁

放个代码:

2.2 shared_ptr加锁代码

namespace rtx
{
	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;
			{                    // 比较_pCount也行
				//if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数
				//{
				//	delete _ptr;
				//	delete _pCount;
				//}
				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;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员
		mutex* _pMtx;
	};
}

int main()
{
	rtx::shared_ptr<double> sp1(new double(7.77));
	rtx::shared_ptr<double> sp2(sp1);

	mutex mtx;

	vector<thread> v(7);
	int n = 100000;
	for (auto& t : v)
	{
		t = thread([&](){
			for (size_t i = 0; i < n; ++i)
			{
				// 拷贝是线程安全的
				rtx::shared_ptr<double> sp(sp1);

				// 访问资源不是
				mtx.lock();
				(*sp)++;
				mtx.unlock();
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}
	cout << *sp1 << endl;
	cout << sp1.use_count() << endl;
	return 0;
}


3. 单例模式线程安全

单例模式复习:

从C语言到C++_37(特殊类设计和C++类型转换)单例模式-CSDN博客

3.1 懒汉模式线程安全问题

在C++11之后饿汉模式是没有线程安全问题的(做了相关补丁),因为单例对象是在main函数之前就实例化的,而多线程都是在main函数里面启动的。

但是懒汉模式是存在线程安全问题的,当多个线程使用到单例对象时候,在使用GetInstance()获取对象时,用因为调度问题出现误判,导致new多个单例对象。

这里给懒汉模式加个锁:(这里在getInstance这样加锁有没有什么问题?)

此时,每个调用GetInstance()的线程都需要申请锁然后释放锁,对锁的操作也是有开销的,会有效率上的损失。

单例模式在单例一经创建以后就不会再创建了,无论多少线程在访问已经创建的单例对象时都不会再创建,线程就已经安全了。所以在单例对象创建以后,根本没有必要再去申请锁和释放锁。

如果把加锁放在 if 里面呢?这样是不行的,因为第二次线程来的时候单例对象已经不是空的了,所以锁就白加了。

此时就要双检查加锁:

3.2 懒汉模式最终代码

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 双检查加锁
		if (m_pInstance == nullptr) // 保护第一次后,后续不需要加锁
		{
			unique_lock<mutex> lock(_mtx); // 加锁,防止new抛异常就用unique_lock
			if (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;

int main()
{
	Singleton* ps = Singleton::GetInstance();//获取单例对象

	return 0;
}

成功运行。


3.3 懒汉模式的另一种写法

放个代码:

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 局部的静态对象,第一次调用时初始化

		// 在C++11之前是不能保证线程安全的
		// C++11之前局部静态对象的构造函数调用初始化并不能保证线程安全的原子性。
		// C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上使用
		static Singleton _s;
		return &_s;
	}

private:
	// 构造函数私有
	Singleton()
	{};

	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
};

int main()
{
	Singleton::GetInstance();

	return 0;
}

C++11之前局部静态对象的构造函数调用初始化并不能保证线程安全的原子性。

C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上使用。


本篇完。

应该算是本专栏的最后一篇了,泪目泪目。道祖且长,行则将至,想再深入学习C++以后就靠自己拓展了。后一部分就是网络和Linux网络的内容了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenAI API-KEY如何获取购买,推荐使用卡密自助发货更方便

在信息爆炸的时代&#xff0c;人们面临海量信息的洪流&#xff0c;其中蕴含了无尽的知识和见解。AI垂直问答技术的兴起&#xff0c;应运而生于这一背景下。与传统的搜索引擎不同&#xff0c;垂直问答聚焦于特定领域&#xff0c;通过深度学习和自然语言处理技术&#xff0c;为用…

UWB应用于金属工具管理

超宽带&#xff08;Ultra-Wideband&#xff0c;UWB&#xff09;技术在金属工具管理方面有许多应用案例&#xff0c;它可以帮助提高工具管理的效率、安全性和精确度。以下是一些UWB在金属工具管理中的应用案例&#xff1a; 工具定位和跟踪&#xff1a;UWB技术可以用于实时定位和…

你知道王者荣耀是怎么实现技能范围指示器的吗?

引言 一文教会你实现类似王者荣耀的技能范围指示器。 技能范围指示器是许多游戏中常见的一个元素&#xff0c;特别是在MOBA&#xff08;多人在线战斗竞技场&#xff09;游戏中&#xff0c;如《王者荣耀》、《英雄联盟》等。 本文将介绍如何在Cocos Creator中实现一个技能范围…

Programming Abstractions in C阅读笔记:p196

《Programming Abstractions in C》学习第63天&#xff0c;p196总结。涉及到编程之外的知识&#xff0c;依然是读起来很费劲&#xff0c;需要了解作者在书中提到的人物(Edouard Lucas)、地点(Benares)、神话传说(Brahma)等等。虽然深知自己做不到对人文知识&#xff0c;历史知识…

RT-DETR算法优化改进:PPHGNetV2 Backbone改进 | RepConv、GhostConv优化HGBlock

🚀🚀🚀本文内容:1)RT-DETR原理介绍;2)RepConv、GhostConv优化HGBlock 🚀🚀🚀RT-DETR改进创新专栏:http://t.csdnimg.cn/vuQTz 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; RT-DETR模型创新优化,涨点技巧分享,科研小助手; 1.RT-DETR介绍 论文…

什么叫做云安全?云安全有哪些要求?

云安全(Cloud Security)是一种基于云计算的安全防护策略&#xff0c;旨在保护企业数据和应用程序的安全性和完整性。云安全利用云计算的分布式处理和存储能力&#xff0c;以更高效、更灵活的方式提供安全服务。 云安全的要求主要包括以下几个方面&#xff1a; 数据安全和隐私保…

【中国知名企业高管团队】系列67:华帝Vatti

前两天&#xff0c;华研荟介绍了中国厨房电器领域的领头羊——方太和老板&#xff0c;今天为您介绍另一个专注于厨房电器的公司——华帝Vatti。 一、关于华帝 根据官网介绍&#xff1a; 华帝股份有限公司自1992年创立至今&#xff0c;专注厨电领域&#xff0c;始终以产品创新…

自然语言处理实战项目21-两段文本的查重功能,返回最相似的文本字符串,可应用于文本查重与论文查重

大家好,我是微学AI,今天给大家介绍一下自然语言处理实战项目21-两段文本的查重功能,返回最相似的文本字符串,可应用于论文查重。本文想实现一种文本查重功能,通过输入两段文本,从中找出这两段文本中最相似的句子。这项技术有助于检测抄袭、抄袭的论文和文章,提高知识创新…

【教3妹学编程-算法题】阈值距离内邻居最少的城市

3妹&#xff1a;好冷啊&#xff0c; 冻得瑟瑟发抖啦 2哥 : 立冬之后又开始降温了&#xff0c; 外面风吹的呼呼的。 3妹&#xff1a;今天还有雨&#xff0c;2哥上班记得带伞。 2哥 : 好的 3妹&#xff1a;哼&#xff0c;不喜欢冬天&#xff0c;也不喜欢下雨天&#xff0c;要是我…

从5亿行数据中,筛选出重复次数在1000行的数据行,也爆内存了

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 独在异乡为异客&#xff0c;每逢佳节倍思亲。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python最强王者交流群【巭孬&#x1f577;】问了一个问…

【Linux】Linux基础IO(上)

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【Linux】…

嵌入式软件工程师面试题——2025校招社招通用(十三)

说明&#xff1a; 面试题来源于网络书籍&#xff0c;公司题目以及博主原创或修改&#xff08;题目大部分来源于各种公司&#xff09;&#xff1b;文中很多题目&#xff0c;或许大家直接编译器写完&#xff0c;1分钟就出结果了。但在这里博主希望每一个题目&#xff0c;大家都要…

基于SSM的网络直播带货网站

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

格式化或删除了存储卡的照片?值得收藏的几个有效方法

最好的恢复软件可以从 SD 卡、固态硬盘和硬盘恢复已删除的照片、视频和数据 您是否不小心重新格式化了存储卡或删除了想要保留的照片&#xff1f;最好的照片恢复软件可以提供帮助&#xff01;如果您使用数码相机拍摄的时间足够长&#xff0c;当您错误地删除了您想要保留的图像…

win下oracle安装与navicat远程连接配置

oracle安装 navicat远程连接配置 1、打开navicat&#xff0c;工具>选项>环境 2、配置 找到oracle安装目录 3、连接

2023亚太杯数学建模A题B题C题思路汇总分析

文章目录 0 赛题思路1 竞赛信息2 竞赛时间3 建模常见问题类型3.1 分类问题3.2 优化问题3.3 预测问题3.4 评价问题 4 建模资料5 最后 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 竞赛信息 2023年第十三…

浅谈如何预防高层小区电气火灾的发生

【摘要】&#xff1a;随着国民经济的发展和人民生活水平的不断提高&#xff0c;我国工业用电和家庭用电量逐年增加。电气火灾造成的人员伤亡和财产损失巨大&#xff0c;时刻威胁着人们的生命及财产安全。众所周知&#xff0c;因供电线路或用电设备的损坏引发的接地电气火灾的例…

数据仓库入门简介

一&#xff0c;数组仓库介绍 数据仓库 &#xff08;英语&#xff1a;Data Warehouse&#xff0c;简称数仓、DW&#xff09;是一个为数据分析而设计的企业级数据管理系统。它旨在 支持企业决策过程中的数据分析和业务智能 。数据仓库的基本原理是将不同来源的数据整合到一个中心…

12358748257

问题一&#xff1a;.浮点数打印问题 float red_increment (target_red_value - initial_red_value) / STEPS; u8 STEPS 100; printf("绿色值每一次增量------%f\n", red_increment); 后面三个参数均为u8类型 希望采用 %f打印出每次的步进值。但是结果为空白 希…

CodeEase标准化的低代码平台

目录 一、引言二、网站简介三、网站特色四、为什么推荐这个网站&#xff1f;五、总结 一、引言 随着互联网的快速发展&#xff0c;我们每天都会浏览各种各样的网站。今天&#xff0c;我想向大家推荐一个独特而出色的网站——CodeEase&#xff0c;这是一个致力于为用户提供便捷…