【C++】关于多线程,你应该知道这些

这篇具有很好参考价值的文章主要介绍了【C++】关于多线程,你应该知道这些。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

👉多线程相关的类👈

thread类的简单介绍

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

【C++】关于多线程,你应该知道这些

【C++】关于多线程,你应该知道这些
注:当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。thread 类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行。

【C++】关于多线程,你应该知道这些
可以通过 jionable 函数判断线程是否是有效的,如果是以下任意情况,则线程无效。

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

【C++】关于多线程,你应该知道这些
【C++】关于多线程,你应该知道这些

get_id 函数使用示例:

【C++】关于多线程,你应该知道这些

yield 函数的介绍和使用示例

【C++】关于多线程,你应该知道这些

【C++】关于多线程,你应该知道这些
sleep_for 函数的使用示例

【C++】关于多线程,你应该知道这些

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

void Print(int n)
{
	cout << "ThreadID:" << this_thread::get_id() << endl;
	for (int i = 0; i < n; ++i)
		cout << i << " ";
	cout << endl;
}

int main()
{
	thread t1(Print, 10);
	t1.join();

	thread t2(Print, 10);
	t2.join();

	return 0;
}

【C++】关于多线程,你应该知道这些
除了用函数(函数指针)来构造线程,还可以使用 lambda 表达式,仿函数等来构造线程。

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

int main()
{
	int x = 0;
	int n = 10;
	int m = 0; // 线程个数

	cin >> m;
	vector<thread> threads;
	threads.resize(m);

	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中的线程对象
		threads[i] = thread([&]() {
			cout << "ThreadID: " << this_thread::get_id() << endl;
			for (int i = 0; i < n; ++i)
			{
				++x;
				cout << i << " ";
			}
			cout << endl;
		});
		threads[i].join();
	}
	cout << "x: " << x << endl;

	return 0;
}

【C++】关于多线程,你应该知道这些
面试题:并发和并行的区别

并发和并行都是指多个任务同时执行的情况,但是它们的含义有所不同。

并发是指在同一个时间段内,多个任务交替地执行,这些任务可以在同一台计算机上运行,也可以在不同的计算机上运行,彼此之间通过网络或其他方式进行通信和同步。并发常常用来提高系统的吞吐量和响应性,以及实现资源共享和负载均衡等功能。

而并行是指在同一个时间点上,多个任务同时执行,这些任务通常在多个处理器或多个计算机上运行,每个任务分配给不同的处理器或计算机进行处理。并行常常用来加速计算和处理速度,提高系统的性能。

mutex类的简单介绍

C++11 中引入了一个新的 mutex 类,它是一个互斥量,用于实现线程之间的同步和数据保护。mutex 类的基本用法是在多个线程之间锁定共享资源以防止竞争条件。在一个线程获得了 mutex 的锁之后,其他线程尝试获取这个锁将会被阻塞,直到锁被释放。

在 C++11 中,mutex 类定义在头文件 中,主要有两个常用的成员函数:

  • lock:用于获得锁,如果锁已经被其他线程占用,则该函数会阻塞当前线程,直到锁被释放为止。
  • unlock:用于释放锁,使其他线程可以获得锁。
  • try_lock:一种非阻塞的互斥锁获取方式,它尝试立即获取互斥锁,如果互斥锁当前未被其他线程持有,则获取锁成功,并返回 true。如果互斥锁已经被其他线程持有,则获取锁失败,try_lock 函数会立即返回 false,而不会像 lock 函数一样阻塞等待锁的释放。
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void Print(const std::string& msg)
{
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << msg << std::endl;
}

int main()
{
    std::thread t1(Print, "Hello from thread 1");
    std::thread t2(Print, "Hello from thread 2");

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

    return 0;
}

在这个示例中,我们创建了两个线程 t1 和 t2,它们都调用了 Print 函数来输出一条消息。由于 Print 函数需要访问共享的输出流 std::cout,我们在函数中使用了 std::lock_guardstd::mutex 来获取锁,保护输出流不会被多个线程同时访问。这里的 mtx 是我们定义的全局互斥量。

当一个线程获取了锁之后,其他线程将会被阻塞,直到锁被释放。在这个示例中,t1 和 t2 会依次输出各自的消息,因为它们会依次获取 mtx 的锁,然后执行 Print 函数,最后释放锁。

需要注意的是,如果一个线程获取了锁之后没有及时释放,那么其他线程将会被永久阻塞,程序可能会陷入死锁状态。因此,我们应该始终确保在使用 mutex 类时,尽可能快地获取和释放锁,避免阻塞其他线程的执行。

关于 lock_guard

lock_guard 是一种用于管理互斥锁的 RAII(Resource Acquisition Is Initialization)类。它可以保证在作用域结束时自动释放互斥锁,以避免忘记手动释放锁所导致的问题。

使用 lock_guard 类可以避免手动管理互斥锁的问题,可以提高程序的可读性和可维护性。在创建 lock_guard 对象时,需要传入一个互斥锁对象,这个互斥锁对象会被 lock_guard 类包装,当 lock_guard 对象被销毁时,它会自动调用互斥锁对象的 unlock 函数,释放互斥锁。

lock_guard 类的使用非常简单,只需要在需要使用互斥锁的代码块中创建一个 lock_guard 对象即可,不需要手动加锁和解锁。当程序流程离开这个代码块时,lock_guard 对象会自动释放互斥锁。由于 lock_guard 对象的生命周期是由编译器控制的,因此无论代码流程中出现何种异常情况,lock_guard 对象都会在作用域结束时被自动销毁,从而保证了程序的正确性。

lock_guard 的模拟实现

template <class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lock)
		: _lock(lock)
	{
		_lock.lock();
	}

	~LockGuard()
	{
		_lock.unlock();
	}
	
	LockGuard(const LockGuard&) = delete;
	LockGuard& operator=(const LockGuard&) = delete;

private:
	Lock& _lock;
};

如何将互斥锁作为执行例程的参数

因为互斥锁是不支持拷贝构造的,所以就不能使用传值的方式来传递互斥锁。那么我们可以采用指针或引用的方式来传递。以指针方式来传递互斥锁比较简单,我们来学习一下如果通过引用来传递互斥锁。

为何通过引用来传递互斥锁

【C++】关于多线程,你应该知道这些
线程的执行函数的参数尽管使用引用来修饰,也是值传递的方式,那怎么样才能实现真正的引用传递方式呢?见下图所示:

【C++】关于多线程,你应该知道这些

【C++】关于多线程,你应该知道这些

在 C++11 线程库中,线程执行例程的参数可以是引用也可以是拷贝,具体取决于用户如何传递参数。如果将参数作为值传递,则会进行拷贝构造,而如果将参数作为引用传递,则不会进行拷贝构造。

如果需要真正实现传递引用,可以使用 std::ref 函数将引用类型的参数包装成 std::reference_wrapper 类型的对象,然后将这个对象作为参数传递给线程的执行例程。 std::reference_wrapper 是一个模板类,它提供了一种引用的包装方式,可以像普通对象一样进行拷贝和赋值,同时可以通过调用其 get 函数获取其包装的引用。

如果有一个函数需要传递一个引用类型的参数,可以使用 ref 函数将这个引用包装成 reference_wrapper 类型的对象,并将其作为参数传递给线程的执行例程。

注:线程构造函数的参数包并不是直接传给执行例程的,而是先用参数包的参数去构造线程,然后再将这些参数传递给线程的执行例程。互斥锁没有被识别成引用传递的问题是出现在构造线程时参数包传递的过程。线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

std::recursive_mutex

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

std::timed_mutex

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

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

unique_lock

与 lock_guard 类似,unique_lock 类模板也是采用 RAII 的方式对锁进行了封装,并且也是以独占所有权的方式管理 Mutex 对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动赋值)时,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 所管理的互斥量的指针)。

atomic类的简单介绍

int main()
{
	int x = 0;
	int n = 10000;
	int m = 0; // 线程个数

	cin >> m;
	vector<thread> threads;
	threads.resize(m);

	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中的线程对象
		threads[i] = thread([&]() {
			for (int i = 0; i < n; ++i)
			{
				++x;
			}
		});
	}
	
	for (auto& thread : threads)
	{
		thread.join();
	}

	cout << "x: " << x << endl;

	return 0;
}

在多线程场景下,多个线程可能会同时访问共享资源,如变量、内存区域、文件等。如果多个线程同时修改同一个共享资源,就会导致数据竞争问题,从而导致程序出现不可预料的错误,如程序崩溃、死锁等。因此,在多线程场景中,需要对共享资源进行加锁保护,以避免数据竞争问题的出现。

【C++】关于多线程,你应该知道这些
上面的代码可以进行加锁保护,加锁的粒度一般是越小越好。对于一些简单的程序,如只有加加操作的,可能锁的释放和线程的上下文切换有可能是更大的消耗。

【C++】关于多线程,你应该知道这些

【C++】关于多线程,你应该知道这些
除了加锁保护,还可以通过原子操作来解决上面的问题。

C++11 引入了 atomic 类,它是一种特殊的数据类型,用于在多线程程序中保证数据的原子性操作。原子性操作是指操作不可被中断或分割,要么全部执行成功,要么全部执行失败,不会出现部分成功或者失败的情况。原子操作通常是在单条指令中完成的,因此在多线程环境下可以避免数据竞争和其他线程安全问题。

atomic 类的主要作用是提供一组原子操作函数来操作其内部封装的数据类型,这些原子操作函数包括:

  • load:原子读取当前值;
  • store:原子设置当前值;
  • exchange:原子交换当前值和给定值;
  • compare_exchange_weak 和compare_exchange_strong:原子比较并交换当前值和给定值;

通过使用这些原子操作函数,可以在多线程环境下对共享变量进行安全的读取和修改操作,避免了数据竞争和其他线程安全问题的出现。例如,可以使用atomic<int>来创建一个原子变量,然后使用 load 和 store 来进行安全的读写操作。

需要注意的是,atomic 类只能用于特定的数据类型,如int、long 等,而不能用于自定义类型。此外,原子操作的开销比普通的非原子操作要大,因此在性能敏感的场景下需要谨慎使用。

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

【C++】关于多线程,你应该知道这些

CAS(Compare-and-Swap)是一种原子操作,用于在多线程编程中实现无锁算法。无锁编程是一种并发编程技术,它不使用互斥锁和信号量等同步机制,而是使用原子操作和其他技术来保证数据的一致性和正确性。

CAS 操作是一种原子性的读取-修改-写入操作,它通常有三个参数:操作的内存地址、期望值和新值。它的执行流程如下:

  1. 读取内存地址中的值;
  2. 检查该值是否等于期望值;
  3. 如果相等,则将新值写入内存地址,并返回成功(true);
  4. 如果不相等,则将内存地址中的值更新为最新的值,然后重新执行 1 - 3 步骤,直到成功为止。

CAS 操作的原子性保证了在多线程环境下,同一时间只有一个线程可以成功地执行该操作,从而避免了数据竞争和其他线程安全问题。因此,CAS 操作通常被用于实现无锁算法,如无锁队列、无锁哈希表、无锁栈等。

无锁编程是一种比较高级的编程技术,它可以提高程序的并发性和性能,同时避免了锁冲突和死锁等问题。但是无锁编程也有一定的局限性,例如在处理复杂的共享数据结构时,可能需要使用比较复杂的算法来实现无锁操作,并且需要注意数据一致性和正确性等问题。因此,在实际编程中需要仔细评估使用无锁编程的可行性和适用范围。

condition_variable类的简单介绍

【C++】关于多线程,你应该知道这些

C++11 中的 condition_variable 是用于线程同步的一种机制,它能够协调多个线程之间的操作,以便它们能够有效地进行通信和同步。

condition_variable 通常与互斥锁一起使用,用于实现生产者-消费者模型、读者-写者模型等线程间同步的场景。

condition_variable 提供了两个主要的操作:wait 和notify_one 或 notify_all。

wait 操作会使当前线程阻塞,并释放关联的互斥锁,直到另外一个线程调用了 notify_one 或 notify_all 方法,通知该线程可以继续执行了。

notify_one 操作会唤醒一个正在等待的线程,而notify_all 操作会唤醒所有正在等待的线程。

使用 condition_variable 的步骤通常如下:

  1. 定义一个mutex 对象和一个 condition_variable 对象;
  2. 在需要等待的线程中,获取 mutex 对象的锁,调用 wait 方法,进入阻塞状态;
  3. 在其他线程中,当某个条件满足时,获取 mutex 对象的锁,调用 notify_one 或 notify_all 方法,通知等待的线程;
  4. 等待的线程被唤醒后,重新获取 mutex 对象的锁,进行处理。

一个线程打印奇数,另一个线程打印偶数

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

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable cv;
	bool ready = true;

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

			cout << "t1(" << this_thread::get_id() << ")" << " : " << i << endl;
			++i;
			ready = true;
			cv.notify_one();
		}
	});

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

			cout << "t2(" << this_thread::get_id() << ")" << " : " << i << endl;
			++i;
			ready = false;
			cv.notify_one();
		}
		});

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

	return 0;
}

【C++】关于多线程,你应该知道这些

判断谁先运行的关键是看 wait 时的可执行对象的返回值是否为 true,如果为 true,那么就是这个线程先运行。

👉智能指针👈

在 C++ 中,智能指针是一种指针类,它通过使用 RAII(资源获取即初始化)技术来自动管理内存资源。智能指针可以自动分配和释放内存,并且可以防止内存泄漏和悬挂指针等问题。

C++ 中有四种智能指针,分别为 auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。其中 auto_ptr 是 C++98 引入的智能指针,其余三个均是 C++11 引入的智能指针。

  • auto_ptr 对象之间的所有权转移是基于赋值操作或拷贝构造函数的,因此需要特别注意避免使用多个 auto_ptr 对象管理同一块内存。
#include <memory>
#include <iostream>

int main() 
{
    std::auto_ptr<int> p1(new int(10));
    std::cout << *p1 << std::endl; // 输出 10

    std::auto_ptr<int> p2(p1); // 所有权转移,p1 现在为空指针
    std::cout << p1.get() << std::endl; // 输出 0
    std::cout << *p2 << std::endl; // 输出 10

    std::auto_ptr<int> p3;
    p3 = p2; // 所有权转移,p2 现在为空指针
    std::cout << p2.get() << std::endl; // 输出 0
    std::cout << *p3 << std::endl; // 输出 10

    return 0; // 所有的 auto_ptr 对象在此处销毁,释放内存
}
  • unique_ptr:是一个独占智能指针,它拥有对堆上的对象的独占所有权,不能与其他指针共享对象所有权。当一个 unique_ptr 被销毁时,它所拥有的对象也会被销毁。可以通过移动语义将其所有权转移给其他 unique_ptr,但不能进行复制操作。因此,unique_ptr 更适合管理单个对象的所有权,如 RAII 资源管理。
  • shared_ptr:是一个共享智能指针,它可以与其他指针共享对同一对象的所有权。当一个 shared_ptr 被销毁时,只有当所有与之共享对象所有权的 shared_ptr 都被销毁时,对象才会被销毁。shared_ptr 支持复制操作,每次复制会增加对象的引用计数。当引用计数为 0 时,对象会被销毁。因此,shared_ptr 更适合管理多个指针共享对象所有权的情况,如在多个模块中共享对象。
  • weak_ptr:是一种特殊的智能指针,它指向 shared_ptr 管理的对象,但并不增加对象的引用计数。它主要用于解决 shared_ptr 的循环引用问题,提供了一种辅助手段。

在《智能指针》这篇博客中模拟实现的 shared_ptr 不是线程安全的,原因就是未对引用计数进行加锁保护,那我们现在来模拟实现一下线程安全的 shared_ptr。

#include <memory>
#include <vector>
#include <thread>
#include <mutex>
#include <iostream>

using namespace std;

namespace Joy
{
	template <class T>
	class shared_ptr
	{
	public:
		shared_ptr()
			: _ptr(nullptr)
			, _pRefCount(nullptr)
			, _pMutex(nullptr)
		{}

		shared_ptr(T* ptr)
			: _ptr(ptr)
			, _pRefCount(new int(1))
			, _pMutex(new mutex)
		{}

		shared_ptr(const shared_ptr<T>& other)
			: _ptr(other._ptr)
			, _pRefCount(other._pRefCount)
			, _pMutex(other._pMutex)
		{
			AddRef();
		}

		void AddRef()
		{
			_pMutex->lock();
			++(*_pRefCount);
			_pMutex->unlock();
		}

		void Release()
		{
			// 当前智能指针没有指向任何资源
			if (_ptr == nullptr)
				return;
			// 通过flag来判断是否需要释放释放锁资源
			bool flag = false;

			_pMutex->lock();
			if (--(*_pRefCount) == 0 && _ptr)
			{
				cout << "delete " << _ptr << endl;
				delete _ptr;
				delete _pRefCount;
				flag = true;
			}
			_pMutex->unlock();
			
			if (flag) delete _pMutex;
		}

		~shared_ptr()
		{
			Release();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& other)
		{
			// 不是自己给自己赋值才执行下面的流程
			if (_ptr != other._ptr)
			{
				Release();
				_ptr = other._ptr;
				_pRefCount = other._pRefCount;
				_pMutex = other._pMutex;
				AddRef();
			}
			return *this;
		}

		int use_count() const
		{
			return *_pRefCount;
		}

		T& operator*() const
		{
			return *_ptr;
		}

		T* operator->() const
		{
			return _ptr;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pRefCount;
		mutex* _pMutex;
	};
}

int Func(int n)
{
	cout << n << endl;
	return n;
}

int main()
{
	int m = 0;
	cin >> m;
	vector<thread> threads(m);
	
	int n = 10000;
	Joy::shared_ptr<double> sp1(new double(7.28));
	Joy::shared_ptr<double> sp2(sp1);

	for (auto& t : threads)
	{
		t = thread([&](){
			for (size_t i = 0; i < n; ++i)
			{
				Joy::shared_ptr<double> sp(sp1);
			}
		});
	}

	for (auto& t : threads)
		t.join();

	// 测试智能指针没有指向任何资源的情况
	{
		Joy::shared_ptr<int> sp3;
	}
	cout << sp1.use_count() << endl;
	return 0;
}

在 C++11 标准中,智能指针(如 unique_ptr、shared_ptr 和 weak_ptr)的引用计数是原子操作,因此在多线程环境下使用智能指针管理资源是线程安全的。多个线程可以同时访问和修改智能指针对象,而不会导致数据竞争和同步问题。

但是,智能指针管理的资源本身并不一定是线程安全的。如果多个线程同时访问和修改同一块内存或同一资源,仍然会发生竞争条件和数据竞争。因此,在使用智能指针管理资源时,需要特别注意多线程访问同一块内存或资源的情况。

如果需要在多线程环境下访问和修改资源,可以使用互斥锁等同步机制来保护资源的访问,以避免数据竞争和同步问题。

👉线程安全的单例👈

饿汉模式和懒汉模式都是单例模式的实现方式,用于保证一个类只有一个实例对象。如何保证一个类只有一个实例对象呢?不允许随便创建对象,删除拷贝构造函数和赋值运算符重载以及提供一个获取唯一实例对象的接口。

饿汉模式

饿汉模式是指在程序启动时或者类被加载时,就创建单例对象。饿汉模式的实现方式是在类定义中直接创建静态的单例对象,如下所示:

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		return &_instance;
	}

private:
	// 构造函数私有
	Singleton() {};
	// 防拷贝
	Singleton(const Singleton&) = delete;
	Singleton& operatot(const Singleton&) = delete;

private:
	static Singleton _instance;
};

Singleton Singleton::_instance;

在程序启动时或者类被加载时,就会创建 Singleton 类的静态实例对象 _instance。

饿汉模式的优点:

  • 实现简单且没有线程安全问题,因为在程序启动时就已经完成了单例对象的初始化,所以每个线程都会访问到同一个对象,而不会出现对象被多次实例化的情况。

饿汉模式的缺点:

  • 饿汉模式可能会影响服务的启动速度。因为饿汉模式在程序启动时就会立即实例化单例对象,如果这个对象的初始化需要较长的时间,那么就会影响服务的启动速度。
  • 饿汉模式可以解决单例类对象的线程安全性和全局唯一性,但并不能解决单例类对象具有实例化顺序的问题。如果单例类对象之间存在依赖关系,比如一个单例类对象的构造函数需要另一个单例类对象作为参数,那么就需要保证依赖的单例类对象先被实例化。在这种情况下,饿汉模式就无法满足需求,需要使用懒汉模式或其他方式来解决问题。

懒汉模式

懒汉模式是指在需要创建单例对象时才创建。懒汉模式的实现方式是在 GetInstance 方法中判断单例对象是否已经创建,如果没有就创建一个新的单例对象,如下所示:

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 保证对象创建好后,不需要申请锁和释放锁
		// 直接返回单例对象的指针
		if (_pInstance)
		{
			unique_lock<mutex> lock(_mtx);
			// 加锁保护和if判断保证第一次
			// 创建对象是线程安全的
			if (_pInstance == nullptr)
			{
				_pInstance = new Singleton();
			}
		}
		return _pInstance;
	}

private:
	// 构造函数私有
	Singleton() {};
	// 防拷贝
	Singleton(const Singleton&) = delete;
	Singleton& operatot(const Singleton&) = delete;

private:
	static Singleton* _pInstance;
	static mutex _mtx;
};

Singleton* Singleton::_pInstance = nullptr;
mutex Singleton::_mtx;

除了使用双检查加上锁保护的方式来实现懒汉模式,下面的代码也能实现懒汉模式。注:下面的代码只能在支持 C++11 的编译器里实现懒汉模式。

class Singleton
{
public:
	static Singleton* GetInstance()
	{
		static Singleton instance;
		return &instance;
	}

private:
	// 构造函数私有
	Singleton() {};
	// 防拷贝
	Singleton(const Singleton&) = delete;
	Singleton& operator(const Singleton&) = delete;
};

为什么上面的代码只在支持 C++11 的编译器里才能实现懒汉模式。

C++ 中局部静态对象的初始化在 C++11 之前是不线程安全的,因为其初始化不是原子操作,可能会出现竞态条件。在多线程场景下,不同线程在不同时间可能同时访问同一个静态变量,如果没有加锁保护,则有可能导致对象被多次初始化,从而破坏程序的正常运行。而 C++11 之后,从编译指令上保证了局部静态对象初始化的线程安全。

同时,C++11 引入了线程安全的局部静态变量初始化方式,即通过使用 call_once 函数和 once_flag 类型实现。call_once 函数可以保证在多线程环境下只初始化一次静态变量,而 once_flag 则是用于标记初始化是否完成的标志。

#include <iostream>
#include <mutex>

class Foo 
{
public:
    void doSomething() 
    {
        std::call_once(initFlag_, &Foo::init);
        std::cout << "do something..." << std::endl;
    }

    static Foo& getInstance() 
    {
        std::call_once(initFlag_, &Foo::init);
        return *instance_;
    }

private:
    static void init() 
    {
        instance_ = new Foo();
    }

    static Foo* instance_;
    static std::once_flag initFlag_;
};

Foo* Foo::instance_ = nullptr;
std::once_flag Foo::initFlag_;

int main() 
{
    Foo::getInstance().doSomething();
    return 0;
}

【C++】关于多线程,你应该知道这些
在上面的示例代码中,Foo 类中的 init 函数通过 new 操作符创建一个 Foo 对象并赋值给 instance_ 指针。在 doSomething和 getInstance 函数中,通过 std::call once 函数保证在多线程环境下只初始化一次静态变量。

懒汉模式的优点:

  • 懒汉可以实现延迟加载,只有在需要时才会去初始化单例对象,节省了内存空间且可以控制单例对象的实例化顺序。

懒汉模式的缺点:文章来源地址https://www.toymoban.com/news/detail-414526.html

  • 在多线程场景下,需要考虑加锁和解锁的问题,实现起来相对比较复杂。
  • 在多线程场景下,需要加锁来保证线程安全,这会影响懒汉模式的性能表现。

到了这里,关于【C++】关于多线程,你应该知道这些的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 关于SpringMVC的异常处理,这些细节你知道吗?

    前言 大家好,我是千锋文哥。今天文哥给大家讲解在SpringMVC中如何进行异常处理。在WEB应用中,如果程序内部出现了异常,我们不加处理的话,异常信息会直接抛到浏览器页面上,导致用户的体验感非常差。对用户来说,这样是非常不友好的,所以我们必须对应用抛出的异常

    2024年02月08日
    浏览(27)
  • 【数据结构】关于排序你应该知道的一切(下)

    和光同尘_我的个人主页 单程孤舟,出云入霞,如歌如吟。 --门孔 啊还是国庆快乐!上节介绍了较为简单的插入排序、选择排序,今天我们上强度,学习交换排序、归并排序还有计数排序,开冲😎 2.1.1. 基本思想 关于冒泡排序我们在C语言的学习中就有涉及 依次比较序列中相

    2024年02月05日
    浏览(29)
  • 关于AI智能生成(AIGC),整理一下你该知道这些

    生成式人工智能(Artificial Intelligence Generated Content) 百度百科 生成式人工智能AIGC(Artificial Intelligence Generated Content)是人工智能1.0时代进入2.0时代的重要标志。 GAN、CLIP、Transformer、Diffusion、预训练模型、多模态技术、生成算法等技术的 累积融合 , 催生了AIGC的爆发 。算

    2024年04月14日
    浏览(26)
  • 一文读懂AI框架——这些关于AI框架的事,你知道多少?

    随着数智化进程的加快 多元化、复杂化的场景持续涌现 大模型俨然成为 当下产 业、甚至整个智能时代 的支柱力量 越来越多的企业开始 构建AI大模型 以应对全新的业务需求和挑战 作为 实现大 模型的重要工具 AI框架也逐渐进入了发展繁荣期 开始收获越来越多的关注 在中国

    2024年02月04日
    浏览(30)
  • 数字藏品是不是NFT?关于数字藏品,这些硬核知识你需要知道

    数字藏品是不是NFT?关于数字藏品,这些硬核知识你需要知道 区块链海森堡 《 BTC海外水电0.38,3天上机》 区块链海森堡 创作声明:内容包含虚构创作 数字藏品是不是NFT?关于数字藏品,这些硬核知识你需要知道 2022-05-09 18:49 最近一段时间,Web3.0不断“刷屏”,而作为Web3

    2024年02月01日
    浏览(40)
  • 【C++航海王:追寻罗杰的编程之路】关于模板,你知道哪些?

    目录 1 - 非类型模板参数 2 - 模板的特化 2.1 - 概念 2.2 - 函数模板的特化 2.3 - 类模板的特化 2.3.1 - 全特化 2.3.2 - 偏特化 2.3.3 - 类模板特化应用实例 3 - 模板分离编译 3.1 - 什么是分离编译 3.2 - 模板的分离编译 3.3 - 解决方法 4 - 模板总结 模板参数分为类型形参与非类型形参。 类型

    2024年04月11日
    浏览(34)
  • 【C++航海王:追寻罗杰的编程之路】关于模板,你知道哪些?

    目录 1 - 泛型编程 2 - 函数模板 2.1 - 函数模板概念 2.2 - 函数模板格式 2.3 - 函数模板的原理 2.4 - 函数模板的实例化 2.5 - 函数参数的匹配原则 3 - 类模板 3.1 - 类模板的定义格式 3.2 - 类模板的实例化 怎样实现一个通用的交换函数? 使用函数重载虽然可以实现,但是有几个不好的

    2024年02月20日
    浏览(25)
  • 一个合格(优秀)的前端都应该阅读这些文章

    的确,有些标题党了。起因是微信群里,有哥们问我,你是怎么学习前端的呢?能不能共享一下学习方法。一句话也挺触动我的,我真的不算是什么大佬,对于学习前端知识,我也不能说是掌握了什么捷径。当然,我个人的学习方法这篇文章已经在写了,预计这周末会在我个

    2024年02月08日
    浏览(37)
  • C++ 缓存再排序,解决多线程处理后的乱序问题,不知道思路对不对[挠下巴]

    使用map默认会根据key排序的原理作缓存,队列满了依次推出,抛弃掉过时的数据

    2024年02月14日
    浏览(30)
  • k8s~你应该知道的ip和你应该知道的端口

    Node IP Cluster IP Pod IP Container IP node ip是指k8s节点的ip地址,这个ip是具体的服务器,它上面的端口是node port,是真实服务器上的端口。 在 Kubernetes 中,ClusterIP 是指 Service 类型中的一种,它为集群内部的其他资源提供了一个虚拟 IP 地址。这个虚拟 IP 只在集群内部可见,用于将请

    2024年02月04日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包