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

news2025/1/11 6:02:59

目录

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网络的内容了。

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

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

相关文章

openGauss学习笔记-122 openGauss 数据库管理-设置密态等值查询-密态支持函数/存储过程

文章目录 openGauss学习笔记-122 openGauss 数据库管理-设置密态等值查询-密态支持函数/存储过程122.1 创建并执行涉及加密列的函数/存储过程 openGauss学习笔记-122 openGauss 数据库管理-设置密态等值查询-密态支持函数/存储过程 密态支持函数/存储过程当前版本只支持sql和P…

初始Linux(五)(定时任务)

crontab 进行 定时任务的设置 概述: 任务调度:是指系统在某个时间执行的特定的命令或程序 任务调度分类: 1.系统工作:有些重要的工作必须周而复始的执行,如病毒扫描 2.个别用户工作: 个别用户可能希望执行某些程序,比如mysql数据库的备份 基本语法: crontab [选项] -e 表示…

CCF CSP认证历年题目自练Day45

这几天搞泰迪杯数据分析技能赛去了。等拿国奖了就出一期关于泰迪杯的。 题目 试题编号&#xff1a; 201703-3 试题名称&#xff1a; Markdown 时间限制&#xff1a; 1.0s 内存限制&#xff1a; 256.0MB 问题描述&#xff1a; 问题描述   Markdown 是一种很流行的轻量级标记…

11-13 周一 同济子豪兄CNN卷积神经网络学习记录

11-13 周一 同济子豪兄CNN卷积神经网络学习记录 时间版本修改人描述2023年11月13日14:02:14V0.1宋全恒新建文档2023年11月13日19:05:29V0.2宋全恒完成 大白话讲解卷积神经网络的学习 简介 为了深入理解CNN&#xff0c;进行B站 同济子豪兄深度学习之卷积神经网络的学习. 主要内…

加速mvn下载seatunnel相关jar包

seatunnel安装的时候&#xff0c;居然要使用mvnw来下载jar包&#xff0c;而且是从https://repo.maven.apache.org 下载&#xff0c;速度及其缓慢&#xff0c;改用自己本地的mvn下载。 修改其安装插件相关脚本&#xff0c;复制install-plugin.sh重命名为install-plugin-mvn.sh …

【数据分享】我国独角兽企业数据(excel格式\shp格式)

企业是经济活动的参与主体。一个城市的企业数量决定了这个城市的经济发展水平&#xff01;比如一个城市的金融企业较多&#xff0c;那这个城市的金融产业肯定比较发达&#xff1b;一个城市的制造业企业较多&#xff0c;那这个城市的制造业肯定比较发达。 本次我们为大家带来的…

论文导读 | 融合大规模语言模型与知识图谱的推理方法

前 言 大规模语言模型在多种自然语言处理相关任务上展现了惊人的能力&#xff0c;如智能问答等&#xff0c;但是其推理能力尚未充分展现。本文首先介绍大模型进行推理的经典方法&#xff0c;然后进一步介绍知识图谱与大模型融合共同进行推理的工作。 文章一&#xff1a;使用思维…

Python大神用的贼溜的九个技巧,超级实用~

文章目录 一、整理字符串输入二、迭代器&#xff08;切片&#xff09;三、跳过可对对象的开头四、只包含关键字参数的函数 (kwargs)五、创建支持「with」语句的对象六、用「slots」节省内存七、限制「CPU」和内存使用量八、控制可以/不可以导入什么九、实现比较运算符的简单方法…

Spring IOC - Bean的生命周期之实例化

在Spring启动流程文章中讲到&#xff0c;容器的初始化是从refresh方法开始的&#xff0c;其在初始化的过程中会调用finishBeanFactoryInitialization方法。 而在该方法中则会调用DefaultListableBeanFactory#preInstantiateSingletons方法&#xff0c;该方法的核心作用是初始化…

【Android】画面卡顿优化列表流畅度四之Glide几个常用参数设置

好像是一年前快两年了&#xff0c;笔者解析过glide的源码&#xff0c;也是因为觉得自己熟悉一些&#xff0c;也就没太关注过项目里glide的具体使用对当前业务的影响&#xff1b;主要是自负&#xff0c;还有就是真没有碰到过这样的数据加载情况。暴露了经验还是不太足够 有兴趣的…

【蓝桥杯选拔赛真题67】Scratch鹦鹉学舌 少儿编程scratch图形化编程 蓝桥杯选拔赛真题解析

目录 scratch鹦鹉学舌 一、题目要求 编程实现 二、案例分析 1、角色分析

SQL-----STUDENT

【学生信息表】 【宿舍信息表】 【宿舍分配表】 为了相互关联&#xff0c;我们需要在表中添加外键。在宿舍分配表中添加用于关联学生信息表的外键 student_id&#xff0c;以及用于关联宿舍信息表的外键 dormitory_id&#xff1b; sql代码 -- 创建学生信息表 CREATE TABLE st…

如何利用黑群晖虚拟机和内网穿透实现公网远程访问

文章目录 前言本教程解决的问题是&#xff1a;按照本教程方法操作后&#xff0c;达到的效果是前排提醒&#xff1a; 1. 搭建群晖虚拟机1.1 下载黑群晖文件vmvare虚拟机安装包1.2 安装VMware虚拟机&#xff1a;1.3 解压黑群晖虚拟机文件1.4 虚拟机初始化1.5 没有搜索到黑群晖的解…

NLP在网安领域中的应用(初级)

NLP在网安领域的应用 写在最前面1. 威胁情报分析1.1 社交媒体情报分析&#xff08;后面有详细叙述&#xff09;1.2 暗网监测与威胁漏洞挖掘 2. 恶意软件检测2.1 威胁预测与趋势分析 3. 漏洞管理和响应4. 社交工程攻击识别4.1 情感分析与实时监测4.2 实体识别与攻击者画像构建4.…

超越任务调度的极致:初探分布式定时任务 XXL-JOB 分片广播

XXL-JOB 是一个分布式任务调度平台&#xff0c;支持分片任务执行。 1. 依赖引入 在项目中引入 XXL-JOB 的相关依赖。通常&#xff0c;你需要在项目的 pom.xml 文件中添加如下依赖&#xff1a; <dependency><groupId>com.xuxueli</groupId><artifactId&…

【02】Istio流量治理

2.1 Istio流量治理 Istio的流量路由规则使运维人员可以轻松控制服务之间的流量和API调用 Istio简化了诸如断路器&#xff0c;超时和重试之类的服务级别属性的配置&#xff0c;并使其易于设置重要任务&#xff08;如A/B测试&#xff0c;canary部署和基于百分比的流量拆分的分段部…

postswigger 靶场(CSRF)攻略-- 2.令牌验证

靶场地址&#xff1a; What is CSRF (Cross-site request forgery)? Tutorial & Examples | Web Security Academy (portswigger.net)https://portswigger.net/web-security/csrf 令牌(token)验证取决于请求方法 题目中已告知易受攻击的是电子邮件的更改功能&#xff0…

汽车展示服务预约小程序的效果

汽车/摩托车/电动车等是人们主要的交通工具&#xff0c;市场各品牌及经销店非常多&#xff0c;其行业可拓展性非常强&#xff0c;因此在线下经营的同时&#xff0c;线上也同样具备获客转化品牌传播等属性。 而品牌所遇的难题也非常明显&#xff0c;那就是线上不足&#xff0c;线…

【LIUNX】配置DNS服务器

【LIUNX】配置DNS A.安装bind bind-utilsB.修改named.conf配置文件C.生成并修改uos.com.db 文件1.复制模版文件named.localhost 到文件uos.com.db2.修改uos.com.db文件 D.重启named服务E.配置DNS服务器F.测试DNS服务器 A.安装bind bind-utils yum -y install bind bind-utilso…

青少年编程学习 等级考试 蓝桥杯/NOC/GESP等比赛资料合集

一、博主愚见 在当今信息技术高速发展的时代&#xff0c;编程已经成为了一种必备的技能。随着社会对于科技人才的需求不断增加&#xff0c;青少年编程学习正逐渐成为一种趋势。为了更好地帮助青少年学习编程&#xff0c;提升他们的技能和素质&#xff0c;博主结合自身多年从事青…