1 多线程高级同步技术

C++ 多线程高级同步技术涉及到多种机制和方法,这些机制和方法用于协调和控制多个线程之间的执行顺序,确保线程安全地访问和修改共享资源,从而避免数据竞争、死锁和其他并发问题。
以下是一些C++中常用的高级同步技术:
原子操作( Atomic Operations )
原子操作是不可中断的操作,它们在多线程环境中执行时不会被其他线程打断。C++11 标准引入了 <atomic>库,提供了对原子类型的支持,包括整数、浮点数和指针等。
自旋锁( Spinlock )
自旋锁用于保护共享资源免受多个线程的同时访问。当一个线程尝试获取一个已经被其他线程持有的自旋锁时,该线程不会进入睡眠状态,而是会持续地(或自旋地)检查锁是否变得可用。如果锁是可用的,线程会立即获取锁并继续执行。
读写锁( Read-Write Lock )
读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。这提高了并发性,因为多个读取操作可以同时进行,而不会相互干扰。
信号量( Semaphores )
信号量是一种用于控制访问有限资源的同步原语。它维护一个计数器,表示可用资源的数量。线程在尝试获取资源之前必须获取信号量,如果资源不可用,则线程将被阻塞。当资源被释放时,信号量的计数器会增加,并可能唤醒等待的线程。

1.1 原子操作

原子操作是一种在多线程环境中可以安全执行的操作,它保证在执行过程中不会被其他线程打断,从而确保数据的一致性和完整性。原子操作通常用于同步和并发编程,以避免数据竞争和其他并发问题。
C++11 标准引入了 <atomic> 头文件,其中定义了一系列原子类型及其操作。这些原子类型包括基本数据类型(如 std::atomic<int> 、 std::atomic<bool> 等)以及指针类型。原子操作包括加载、存储、交换、比较交换等。
如下为样例代码:

#include <iostream>  
#include <thread>  
#include <atomic>  
#include <chrono>  

std::atomic<int> g_atomicNum(0);		// 定义一个原子整数变量,并初始化为0  
int g_num = 0;							// 定义一个普通整数变量,并初始化为0  

void incrementAtomicNum() 
{
	for (int i = 0; i < 10000000; i++)
	{
		// 使用原子操作增加计数器的值  
		g_atomicNum.fetch_add(1, std::memory_order_relaxed);
	}
}

void incrementNum()
{
	for (int i = 0; i < 10000000; i++)
	{
		g_num++;
	}
}

int main() 
{
	std::thread t1(incrementAtomicNum); // 创建第一个线程来增加原子计数器  
	std::thread t2(incrementAtomicNum); // 创建第二个线程来增加原子计数器  

	t1.join(); // 等待第一个线程完成  
	t2.join(); // 等待第二个线程完成  

	// 输出最终计数器的值,应该是20000000
	printf("atomic number: %d\n", g_atomicNum.load());

	std::thread t3(incrementNum); // 创建第三个线程来增加普通计数器  
	std::thread t4(incrementNum); // 创建第四个线程来增加普通计数器  

	t3.join(); // 等待第三个线程完成  
	t4.join(); // 等待第四个线程完成  

	// 输出最终计数器的值,结果是不确定的 
	printf("normal number: %d\n", g_num);

	return 0;
}

上面代码的输出为:

atomic number: 20000000
normal number: 13480371

普通整型变量 g_num 在两个线程中的累加之所以是一个未知量,是因为在增加的过程中,可能会发生线程间的切换。这会导致数据竞争,最终的 g_num 值可能不是预期的 20000000,而是小于这个值。
在上面代码中,有一个 std::memory_order_relaxed 的参数,该参数是枚举 memory_order (内存顺序)的一个值。 memory_order (内存顺序)参数用于指定原子操作的内存顺序语义,它定义了操作前后内存访问的顺序关系。 memory_order 参数对于确保线程之间的正确同步至关重要,因为不正确的内存顺序可能导致意外的行为或性能下降。
C++11 标准定义了以下几种 memory_order :
std::memory_order_relaxed
最宽松的内存顺序要求。它只保证原子操作的原子性,不保证任何特定的内存顺序。使用这种顺序时,编译器和处理器可以对指令进行重排序,以提高性能。因此,除非你很清楚自己在做什么,否则通常不建议使用这种顺序。
std::memory_order_consume
这种顺序用于消费操作,它保证在消费操作之前的所有写入(对于同一个原子变量)都对执行消费操作的线程可见。它允许一些优化,但比 relaxed 更强一些。
std::memory_order_acquire
这种顺序用于获取操作,它保证在执行获取操作之前的所有写入(对于同一个原子变量)都对执行获取操作的线程可见。它确保了获取操作之前的内存访问不会被重排序到该操作之后。
std::memory_order_release
这种顺序用于释放操作,它保证在执行释放操作之后的所有读取(对于同一个原子变量)都会反映执行释放操作线程的最新状态。它确保了释放操作之后的内存访问不会被重排序到该操作之前。
std::memory_order_acq_rel
这种顺序是 acquire 和 release 的结合,它同时保证了获取和释放操作的内存顺序要求。这是最常用的内存顺序之一,因为它提供了足够强的保证,同时允许编译器和处理器进行某些优化。
std::memory_order_seq_cst
这是最严格的内存顺序要求,它提供了顺序一致性保证。它要求所有线程都按照相同的顺序看到所有原子操作的效果。这种顺序会禁止几乎所有的指令重排序,因此可能会降低性能。
在选择适当的memory_order时,需要根据具体需求和上下文来权衡性能和正确性。通常, std::memory_order_seq_cst 是最安全的选择,但如果不是必需的,使用更宽松的内存顺序可能会提高性能。
如下为内存顺序相关的样例代码:

#include <iostream>  
#include <thread>  
#include <atomic>  

std::atomic<bool> g_flag(false);
std::atomic<int> g_atomicNum(0);

void incrementAtomicNum()
{
	for (int i = 0; i < 100000; i++) 
	{
		while (!g_flag.load(std::memory_order_acquire))
		{
			// 等待flag变为true  
		}
		g_atomicNum.fetch_add(1, std::memory_order_relaxed);
	}
}

int main()
{
	std::thread t(incrementAtomicNum);

	// 做一些其他工作...  

	g_flag.store(true, std::memory_order_release);

	t.join();

	printf("atomic number: %d\n", g_atomicNum.load());

	return 0;
}

上面代码的输出为:

atomic number: 100000

在上面代码中,使用 std::memory_order_acquire 来加载 g_flag 变量,确保在 g_atomicNum 函数中看到 g_flag 变为 true 之前的所有写入。同时,使用 std::memory_order_relaxed 来增加 counter ,因为这段代码的逻辑不关心 counter 增加操作之前的内存顺序。最后,使用 std::memory_order_release 来存储 g_flag ,确保在此之前的所有写入对设置 g_flag 的线程可见。
std::atomic 类提供了一系列方法来执行原子操作。以下是一些主要的 std::atomic 成员函数和方法:
构造函数
atomic():默认构造函数,创建一个未初始化的原子对象。
atomic(T desired):用desired初始化原子对象。
load
T load(memory_order order = memory_order_seq_cst):以指定的内存顺序加载原子对象的值。
store
void store(T desired, memory_order order = memory_order_seq_cst):以指定的内存顺序存储值到原子对象。
exchange
T exchange(T desired, memory_order order = memory_order_seq_cst):以指定的内存顺序将desired值存储到原子对象,并返回旧值。
compare_exchange_strong
bool compare_exchange_strong(T& expected, T desired, memory_order success_order = memory_order_seq_cst, memory_order failure_order = memory_order_seq_cst):如果当前值与expected相同,则存储desired值并返回true;否则,将expected设置为当前值并返回false。
compare_exchange_weak
同上,但允许在某些情况下失败,即使当前值与expected相同。
fetch_add
T fetch_add(T arg, memory_order order = memory_order_seq_cst):以指定的内存顺序将arg加到原子对象的当前值,并返回旧值。
fetch_sub
T fetch_sub(T arg, memory_order order = memory_order_seq_cst):以指定的内存顺序从原子对象的当前值减去arg,并返回旧值。
fetch_and
T fetch_and(T arg, memory_order order = memory_order_seq_cst):以指定的内存顺序对原子对象的当前值执行位与操作(AND),并返回旧值。
fetch_or
T fetch_or(T arg, memory_order order = memory_order_seq_cst):以指定的内存顺序对原子对象的当前值执行位或操作(OR),并返回旧值。
fetch_xor
T fetch_xor(T arg, memory_order order = memory_order_seq_cst):以指定的内存顺序对原子对象的当前值执行位异或操作(XOR),并返回旧值。
operator++
T operator++():前缀递增操作,原子地增加对象值并返回新值。
T operator++(int):后缀递增操作,原子地增加对象值并返回旧值。
operator–
T operator–():前缀递减操作,原子地减少对象值并返回新值。
T operator–(int):后缀递减操作,原子地减少对象值并返回旧值。
operator+=
T& operator+=(T arg):原子地增加对象值。
operator-=
T& operator-=(T arg):原子地减少对象值。
operator&=
T& operator&=(T arg):原子地执行位与操作。
operator|=
T& operator|=(T arg):原子地执行位或操作。
operator^=
T& operator^=(T arg):原子地执行位异或操作。
operator=
T& operator=(T desired):非原子地设置对象值。注意,这不是原子操作。
std::atomic 还提供了 is_lock_free 成员函数,用于检查特定类型的原子操作是否支持无锁实现。
这些函数和方法提供了丰富的原子操作集,允许开发者在多线程环境中安全地执行共享数据的读写操作。需要注意的是, memory_order 参数的选择对于确保正确的同步和避免数据竞争至关重要。应该根据具体的应用场景和同步需求来选择合适的 memory_order 。

1.2 自旋锁

自旋锁( Spinlock )是一种线程同步机制,用于保护共享资源免受多个线程的同时访问。当一个线程尝试获取一个已经被其他线程持有的自旋锁时,该线程不会进入睡眠状态,而是会持续地(或自旋地)检查锁是否变得可用。如果锁是可用的,线程会立即获取锁并继续执行。这种机制适用于短期锁定的情况,因为它避免了线程切换的开销。如果预计等待锁的时间会很长,使用自旋锁可能会导致处理器资源的浪费,并降低系统的并行性能。
与互斥锁相比,自旋锁和互斥锁都是为了解决对某项资源的互斥使用。但两者在调度机制上有所不同。互斥锁在资源被占用时,会使资源申请者进入睡眠状态,而自旋锁则不会。这使得自旋锁在某些场景下更为高效,尤其是当锁被持有的时间很短,或者线程切换的代价相对较高时。
然而,自旋锁也有一些限制和缺点。首先,它无法保证公平性,即等待时间最长的线程不一定能首先获得锁。其次,自旋锁本身也不能保证可重入性,即同一个线程在持有锁的情况下再次尝试获取锁可能会导致问题。此外,在单核处理器上,使用自旋锁可能会导致性能下降,因为自旋的线程会不断消耗处理器资源。
在 C++ 中,没有内置的自旋锁支持,但可以使用标准库中的条件变量和互斥锁来实现一个自旋锁,或者使用特定的平台或第三方库提供的自旋锁实现。
如下为样例代码:

#include <iostream> 
#include <atomic>  
#include <thread>  
#include <chrono>  

class Spinlock 
{
public:
	void lock() {
		while (m_lockFlag.test_and_set(std::memory_order_acquire))
		{
			// 自旋等待,直到锁可用  
			// 这里可以加入一个短暂的延迟以减少CPU占用  
			// std::this_thread::yield();  
		}
	}

	bool tryLock() 
	{
		return !m_lockFlag.test_and_set(std::memory_order_acquire);
	}

	void unlock() 
	{
		m_lockFlag.clear(std::memory_order_release);
	}

private:
	std::atomic_flag m_lockFlag = ATOMIC_FLAG_INIT;
};

int g_num = 0;
Spinlock g_spinlock;

void incrementNum()
{
	for (int i = 0; i < 1000000; i++)
	{
		g_spinlock.lock();
		g_num++;
		g_spinlock.unlock();
	}
}

int main() 
{
	std::thread t1(incrementNum);
	std::thread t2(incrementNum);

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

	printf("number: %d\n", g_num);
	return 0;
}

上面代码的输出为:

number: 2000000

在上面代码中,Spinlock 类包含一个 std::atomic_flag 成员,用于表示锁的状态。 lock() 函数会尝试设置这个标志,如果设置成功(即锁是可用的),则线程获取锁。如果设置失败(即锁被其他线程持有),则线程会持续尝试,直到获取锁为止。 tryLock() 函数尝试获取锁,如果锁不可用,它会立即返回 false 。 unlock() 函数清除标志,释放锁。
注意,这个简单的自旋锁实现不包含任何超时或回退机制。在实际情况中,可能需要为自旋锁实现添加更复杂的特性,例如超时、优先级继承等,以应对不同的应用场景和性能要求。此外,过度使用自旋锁(尤其是在锁持有时间较长的情况下)可能会导致 CPU 资源浪费,因此应该谨慎使用。
std::atomic_flag 的概念:
std::atomic_flag 是 C++11 引入的一种特殊的原子类型,它是一个简单的原子布尔类型,只支持两种操作:test_and_set 和 clear。
std::atomic_flag 的主要用途是作为一个轻量级的标志位,常用于自旋锁(spinlocks)或其他需要快速、简单的原子操作的场景。
std::atomic_flag 的构造函数是默认构造函数,它不能被拷贝或移动赋值。这意味着你不能从一个 std::atomic_flag 对象构造另一个,也不能将 std::atomic_flag 对象赋值给另一个。
test_and_set 操作是一个原子操作,它会检查 std::atomic_flag 的当前值,如果值为 false,则将其设置为 true,并返回先前的值(即 false)。如果值为 true,则不改变其值,并返回 true。
clear 操作也是一个原子操作,它会将 std::atomic_flag 的值设置为 false。
ATOMIC_FLAG_INIT 是一个宏,用于初始化 std::atomic_flag 对象,确保在创建时它处于 clear(即 false)状态。
std::this_thread::yield() 的概念:
std::this_thread::yield() 是 C++11 中的一个函数,它属于 <thread> 头文件。当调用这个函数时,当前执行的线程会主动放弃其时间片,将处理器资源让给其他线程运行。这通常用于实现线程间的协作,尤其是在多线程编程中,当当前线程已经完成了其当前的工作,但还没有到达操作系统调度器的时间片结束时,std::this_thread::yield() 可以被用来让其他线程有机会运行。

1.3 读写锁

C++11 引入的读写锁( std::shared_mutex 和 std::shared_timed_mutex )为并发编程提供了更细粒度的控制。这些锁允许多个线程同时读取共享资源,但只允许一个线程独占写入共享资源。当没有线程在写入时,多个线程可以同时读取共享数据,这提高了程序的并发性能。
std::shared_mutex
std::shared_mutex 是一种读写锁,它有两种类型的锁:
(1)共享锁( Shared Lock ):也称为读锁。当多个线程需要读取共享资源时,它们会获取共享锁。多个线程可以同时持有共享锁,因此可以同时读取共享资源。
(2)独占锁( Exclusive Lock ):也称为写锁。当线程需要写入共享资源时,它会获取独占锁。只有一个线程可以持有独占锁,因此写入操作是独占的。
使用 std::shared_mutex
要使用 std::shared_mutex ,需要包含 <shared_mutex> 头文件。然后可以创建 std::shared_mutex 对象,并使用 std::shared_lock 或 std::unique_lock 来获取锁。
如下为样例代码:

#include <iostream>  
#include <thread>  
#include <shared_mutex>  
#include <vector>  
#include <string>  

std::shared_mutex g_rwMutex;	// 读写锁  
std::vector<int> g_sharedDatas; // 共享数据  

void printStr(std::string str)
{
	// 获取当前时间点  
	auto now = std::chrono::system_clock::now();
	// 转换为自1970年1月1日以来的秒数(即UNIX时间戳)  
	auto duration = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch());
	// 获取时间戳  
	auto timestamp = duration.count();

	printf("[%lld] %s\n", timestamp, str.c_str());
}

void readData(int id) 
{
	std::shared_lock<std::shared_mutex> lock(g_rwMutex); // 获取共享锁 

	std::this_thread::sleep_for(std::chrono::seconds(2));	//等待 2 秒

	std::string strReadInfo = std::string("thread") + std::to_string(id) + std::string(" is reading data: ");
	for (int value : g_sharedDatas) 
	{
		strReadInfo.append(std::to_string(value) + std::string(" "));
	}
	printStr(strReadInfo);
}

void writeData(int id, int value)
{
	std::unique_lock<std::shared_mutex> lock(g_rwMutex); // 获取独占锁  

	std::this_thread::sleep_for(std::chrono::seconds(2));	//等待 2 秒

	std::string strWriteInfo = std::string("thread") + std::to_string(id) + std::string(" is writing data: ") + std::to_string(value);
	g_sharedDatas.push_back(value);
	printStr(strWriteInfo);
}

int main()
{
	// 创建并启动多个读线程和写线程  
	std::vector<std::thread> readerThreads;
	std::vector<std::thread> writerThreads;

	for (int i = 0; i < 3; i++)
	{
		writerThreads.emplace_back(writeData, i, i);
	}

	for (int i = 0; i < 5; i++)
	{
		readerThreads.emplace_back(readData, i + 3);
	}

	// 执行线程  
	for (auto& thread : readerThreads)
	{
		thread.detach();
	}
	for (auto& thread : writerThreads)
	{
		thread.detach();
	}

	// 等待线程完成
	std::this_thread::sleep_for(std::chrono::seconds(10));

	return 0;
}

上面代码的输出为:

[1708065160] thread0 is writing data: 0
[1708065162] thread1 is writing data: 1
[1708065164] thread2 is writing data: 2
[1708065166] thread6 is reading data: 0 1 2
[1708065166] thread5 is reading data: 0 1 2
[1708065166] thread7 is reading data: 0 1 2
[1708065166] thread4 is reading data: 0 1 2
[1708065166] thread3 is reading data: 0 1 2

在上面代码中,创建了 3 个写线程和 5 个读线程。读线程使用 std::shared_lock 来获取共享锁,并读取 g_sharedDatas。写线程使用 std::unique_lock 来获取独占锁,并向 g_sharedDatas 中写入数据。
注意上面输出中的时间戳:写线程按照顺序逐一获取独占锁,写入数据,而读线程则同时获取共享锁,读出数据。
std::shared_timed_mutex
std::shared_timed_mutex 是 std::shared_mutex 的一个变种,它提供了基于时间的锁尝试功能。这意味着可以使用 try_lock_shared、try_lock_exclusive、try_lock_shared_for 和 try_lock_exclusive_for 等方法尝试获取锁,并在指定的时间内如果没有获取到锁则返回。
注意事项
死锁:尽管读写锁可以提高并发性能,但它们也可能导致死锁。例如,如果一个线程持有多个读锁并尝试获取一个写锁,而另一个线程持有写锁并尝试获取读锁,就可能发生死锁。
锁升级:一个线程在从读模式切换到写模式时,可能需要先释放所有读锁,然后再获取写锁。这被称为锁升级,并可能导致性能下降。
锁粒度:读写锁适用于保护整个数据结构或资源的场景。如果需要保护数据结构中的特定部分,可能需要考虑更细粒度的锁策略。
锁竞争:在锁竞争激烈的情况下,读写锁可能不如其他类型的锁(如自旋锁)性能好。

1.4 信号量

在多线程编程中,信号量的主要作用有以下几点:
互斥访问( Mutual Exclusion )
信号量可以防止多个线程同时访问某个共享资源,确保资源在任何时刻只被一个线程所拥有。这可以防止数据不一致和其他同步问题。
同步( Synchronization )
信号量可以用于同步线程的执行顺序。通过控制信号量的计数,线程可以等待其他线程完成特定任务后再继续执行。
限制资源访问数量
信号量可以用来限制对共享资源的并发访问数量。例如,如果有一个只能由固定数量线程同时访问的资源,可以设置一个初始计数等于该数量的信号量。这样,当信号量的计数降到零时,其他尝试获取信号量的线程将被阻塞,直到有线程释放信号量。
实现生产者-消费者的解决方案
信号量是解决生产者-消费者问题的经典方法之一。在这个问题中,生产者生成数据放入缓冲区,消费者从缓冲区中取出数据。信号量可以用来表示缓冲区中的空闲位置和已填充位置的数量,从而协调生产者和消费者的行为。
避免死锁和饥饿
通过合理地使用信号量,可以避免死锁(两个或更多线程无限期地等待对方释放资源)和饥饿(某个线程长时间得不到执行机会)问题。
实现有界缓冲区
在多线程环境中,信号量可以用于实现有界缓冲区,确保缓冲区不会被溢出或下溢。
C++ 标准库并没有直接提供信号量的实现。可以使用 <condition_variable>头文件中的 std::condition_variable 和 std::mutex 来模拟信号量的行为。如下为样例代码:

#include <iostream>  
#include <thread>  
#include <mutex>  
#include <condition_variable>  

// 模拟信号量的类  
class Semaphore
{
public:
	Semaphore(int count) : m_count(count), m_mutex(), m_cv() {}

	// 等待信号量  
	void wait() 
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		while (0 == m_count)
		{
			m_cv.wait(lock);
		}
		m_count--;
	}

	// 释放信号量  
	void post()
	{
		std::unique_lock<std::mutex> lock(m_mutex);
		m_count++;
		m_cv.notify_one();
	}

private:
	int m_count;
	std::mutex m_mutex;
	std::condition_variable m_cv;
};

std::mutex g_coutMutex;

// 线程函数  
void threadFunc(Semaphore& sem) 
{
	sem.wait();

	{
		std::unique_lock<std::mutex> lock(g_coutMutex);
		std::cout << "thread " << std::this_thread::get_id() << " is accessing the shared resource." << std::endl;
	}

	// 假设访问共享资源需要一些时间  
	std::this_thread::sleep_for(std::chrono::milliseconds(100));
	sem.post();
}

int main() {
	Semaphore sem(1); // 初始化信号量,设置初始值为1  

	// 创建多个线程  
	std::thread threads[3];
	for (int i = 0; i < 3; i++)
	{
		threads[i] = std::thread(threadFunc, std::ref(sem));
	}

	// 等待所有线程完成  
	for (int i = 0; i < 3; i++)
	{
		threads[i].join();
	}

	return 0;
}

上面代码的输出为:

thread 14004 is accessing the shared resource.
thread 13436 is accessing the shared resource.
thread 2384 is accessing the shared resource.

在上面代码中,创建了一个名为 Semaphore 的类,它使用 std::mutex 来保护对 m_count 变量的访问,并使用 std::condition_variable 来阻塞和唤醒等待的线程。 wait() 方法会尝试获取信号量,如果信号量的值为零,则线程会被阻塞,直到其他线程调用 post() 方法释放信号量。 post() 方法会增加信号量的值,并通知一个等待的线程。

2 线程安全与并发容器

线程安全通常指的是在并发环境中,代码能够正确地执行,并且不会产生不可预见的结果。要实现线程安全,需要确保在访问和修改共享数据时,使用适当的同步机制来避免数据竞争。C++ 标准库没有直接提供线程安全的并发容器。C++17 引入了一些与并发相关的工具,如 <execution>头文件中的执行策略,这些策略可以与标准库中的算法结合使用,以支持并行计算。尽管如此,这些执行策略并不直接提供容器的并发访问安全。

2.1 死锁

死锁产生的条件
死锁产生的条件主要有四个:
互斥
一个资源每次只能被一个进程使用。如果此时还有其他进程请求资源,则请求者只能等待,直到占有资源的进程使用完毕释放资源。
请求和保持
一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不可剥夺
进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
循环等待
在发生死锁时,必然存在一个进程-资源的环形链。即进程集合 {P0,P1,P2,…,Pn} 中的 P0 正在等待一个 P1 占用的资源; P1 正在等待 P2 占用的资源,…,Pn 正在等待已被 P0 占用的资源。
只要系统发生死锁,这些条件必然成立。因此,预防死锁的策略主要是破坏上述四个条件中的一个或多个。
死锁的预防与检测
为了预防死锁,可以采取以下策略:
(1)避免循环等待:确保资源请求的顺序是一致的。如果每个线程都按照相同的顺序请求资源,那么就不可能出现循环等待的情况,从而避免了死锁。
(2)一次只请求一个资源:如果可能的话,尽量让线程一次只请求一个资源。这样可以减少资源争用的可能性,从而减少了死锁的风险。
(3)使用资源层次结构:将资源组织成层次结构,并要求线程按照层次顺序请求资源。这样,即使发生了资源争用,也不会出现循环等待。
(4)使用超时机制:在尝试获取资源时设置超时时间。如果线程在超时时间内无法获取资源,则放弃该资源并尝试其他策略。
(5)使用锁顺序协议:确保所有线程都按照相同的顺序获取锁。这可以通过为每个锁分配一个唯一的标识符,并要求线程按照标识符的顺序获取锁来实现。
为了检测死锁,可以采取以下策略:
(1)使用死锁检测算法:实现一个死锁检测算法,定期检查线程和资源的状态,以确定是否存在死锁。如果检测到死锁,可以采取适当的措施来恢复,例如通过中断一个线程来解除死锁。
(2)使用资源监视器:实现一个资源监视器,跟踪每个资源的使用情况。如果资源监视器发现某个资源被多个线程同时请求并等待,那么可能存在死锁。在这种情况下,资源监视器可以采取措施来解除死锁。
(3)使用操作系统提供的死锁检测工具:一些操作系统提供了死锁检测工具,可以帮助开发人员检测和解决死锁问题。这些工具可以监视线程和资源的状态,并在检测到死锁时发出警告或采取其他措施。
需要注意的是,死锁预防和检测策略的选择取决于具体的应用场景和需求。在选择合适的策略时,需要权衡性能、复杂性和可靠性等因素。

2.2 线程安全的概念

线程安全( Thread Safety )是并发编程中的一个重要概念,它涉及到多个线程同时访问和修改共享数据时如何确保数据的一致性和程序的正确性。线程安全主要关注的是在并发环境下,代码的执行不会因为多个线程的交错执行而导致错误或不可预期的行为。
在C++中,线程安全通常涉及到以下几个方面:
原子操作
原子操作是指一个操作在执行过程中不会被其他线程打断的操作。在多线程环境中,如果一个操作不是原子的,那么它可能会在执行过程中被其他线程打断,导致数据的不一致。C++提供了std::atomic模板类来支持原子操作。
互斥锁( Mutexes )
互斥锁是一种同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问这些资源。当线程尝试获取一个已经被其他线程持有的锁时,该线程将被阻塞,直到锁被释放。C++中可以使用std::mutex来实现互斥锁。
条件变量( Condition Variables )
条件变量通常与互斥锁一起使用,用于在多个线程之间传递信号。一个线程可以在条件变量上等待,直到另一个线程发出通知,表示某个条件已经满足。 C++ 中可以使用 std::condition_variable 来实现条件变量。
线程安全的容器和函数
C++标准库中的某些容器和函数是线程安全的,可以在多个线程之间共享和访问。例如, std::vector 和 std::list 等容器在单个线程中是安全的,但在多线程环境中,如果没有适当的同步机制,它们可能会导致数据竞争。为了在多线程环境中安全地使用这些容器,可以使用前面提到的互斥锁、条件变量等同步机制。
线程局部存储( Thread-Local Storage )
线程局部存储是一种机制,允许每个线程拥有其自己的变量副本。这样,每个线程都可以独立地修改自己的变量,而不会影响到其他线程。 C++ 中可以使用 thread_local 关键字来声明线程局部变量。
总之,线程安全是并发编程中的一个重要概念,它要求开发者在编写代码时考虑到多个线程可能同时访问和修改共享数据的情况,并采取适当的同步机制来确保数据的一致性和程序的正确性。在 C++ 中,可以通过使用原子操作、互斥锁、条件变量、线程安全的容器和函数以及线程局部存储等机制来实现线程安全。

2.3 自定义并发容器

对于线程安全的并发容器,C++ 标准库依赖于用户自行实现同步机制,如使用互斥锁( std::mutex )或原子操作( std::atomic )来保护对容器的访问。如下是一个简单的例子,展示了如何使用 std::mutex 来封装一个线程安全的 std::vector :

#include <vector>  
#include <mutex>  

template <typename T>
class ThreadSafeVector 
{
public:
	ThreadSafeVector() = default;
	ThreadSafeVector(const ThreadSafeVector &other) 
	{
		std::lock_guard<MutableMutex> lockGuard(this->lock);
		datas = other.datas;
	}

	ThreadSafeVector &operator=(const ThreadSafeVector &other) 
	{
		std::lock_guard<MutableMutex> lockGuard(this->lock);
		datas = other.datas;
		return *this;
	}

	void push_back(const T &value) 
	{
		std::lock_guard<MutableMutex> lockGuard(this->lock);
		datas.push_back(value);
	}

	T &operator[](size_t index) 
	{
		std::lock_guard<MutableMutex> lockGuard(this->lock);
		return datas[index];
	}

	const T &operator[](size_t index) const 
	{
		std::lock_guard<MutableMutex> lockGuard(this->lock);
		return datas[index];
	}

	size_t size() const 
	{
		std::lock_guard<MutableMutex> lockGuard(this->lock);
		return datas.size();
	}

	// 其他需要的成员函数...  

private:
	std::vector<T> datas;
	mutableMutex lock; // 可使用 std::mutex 或其他互斥锁类型  

};
02-16 18:42