C++并发编程(六)并发数据结构的设计

C++并发编程 第1章:C++的并发世界 1.1 何谓并发 最简单和最基本的并发,是指两个或更多独立的活动同时发生。 并发在生活中随处可见,我们可以一边走路一边说话,也可以两只手同时作不同的动作,还有我们每个人都过着相互独立的生活——当我在游泳的时候,你可以看球赛,等等。 1.1.1 计算机系统中的并发 计算机领域的并发指的是在单个系统里同时执行多个独立的任务,而非顺序的进行一些活动。 计算机领域里,并发不是一个新事物:很多年前,一台计算机就能通过多任务操作系统的切换功能,同时运行多个应用程序;高端多处理器服务 阅读详情

目录

1.并发设计的内涵

1.1设计并发数据结构的要点

2基于锁的并发数据结构

2.1线程安全的栈(前面章节)

2.2线程安全的队列容器(锁和条件变量)

2.3精细粒度的锁和条件变量提高队列并发度

2.4等待数据弹出的安全栈

3.更复杂的基于锁的并发结构

3.1线程安全的查找表

3.2线程安全的链表

4.要点小结


如果多个线程访问同一数据结构,除了采用独立互斥和外部锁(第3、4章),我们还可以为并发访问专门设计数据结构以保护数据。

1.并发设计的内涵

线程安全的数据结构需要满足以下条件:

(1)多线程执行的操作无论异同,每个线程所见的数据结构自恰.

(2)数据不会丢失或破坏,不存在恶性数据竞争

并发设计的意义在于提供更高的并发程度,让各线程由更多机会按并发的方式访问数据结构。

互斥保护数据的方式是阻止真正的并发访问,实现串行化(serialization):每个线程轮流访问受互斥保护的数据,它们只能先后串行访问。保护的范围越小,并发程度越高。

1.1设计并发数据结构的要点

设计并发数据结构我们需要考虑:

(1)确保访问安全

构造函数完成以前和析构函数开始以后,访问不会发生。

我们需要明确哪些函数可以安全地跨线程调用。

还需要判断:这些函数与其它操作并发调用是否安全,函数是否要求以排他方式访问。

(2)实现真正的并发访问.

限制锁的作用域,让操作的某些部分在锁保护以外执行。

数据结构内部不同部分使用不同互斥。

不同操作使用不同程度的保护。

在不影响操作语义的情况下,提高数据结构并发度。

核心问题:保留必要的串行操作,最大限度实现并发访问。

2基于锁的并发数据结构

奥义:确保先锁住互斥,再访问数据,尽量缩短锁的持续时间。

2.1线程安全的栈(前面章节)

#include <exception>
#include <memory>
#include <mutex>
#include <stack>
struct empty_stack : std::exception
{
	const char* what() const throw();
};

template<typename T>
class threadsafe_stack
{
private:
	std::stack<T> data;
	mutable std::mutex m;
public:
	threadsafe_stack(){}
	threadsafe_stack(const threadsafe_stack& other)
	{
		std::lock_guard<std::mutex> lock(other.m);
		data = other.data;
	}

	threadsafe_stack& operator=(const threadsafe_stack&) = delete;//删除赋值运算操作

	void push(T new_val)
	{
		std::lock_guard<std::mutex> lock(m);
		data.push(std::move(new_val));
	}

	std::shared_ptr<T> pop()
	{
		std::lock_guard<std::mutex> lock(m);
		if (data.empty()) throw empty_stack();
		const std::shared_ptr<T> res(std::make_shared<T>(data.top()));
		data.pop();
		return res
	}

	void pop(T& val)
	{
		std::lock_guard<std::mutex> lock(m);
		if (data.empty()) throw empty_stack();
		val = data.top();
		data.pop();
	}

	bool empty()const
	{
		std::lock_guard<std::mutex> lock(m);
		return data.empty();
	}
};

安全性:

栈容器所有成员都使用std::lock_guard<>保护数据。

在可能抛出异常的操作完成之前,底层栈容器数据并未改动,保证了安全抛出行为。

仅构造和析构函数不是安全的成员函数,若对象未完成构造或销毁,转去调用成员函数绝非正确之举,必须使用者自己保证。

不足:

虽然成员函数都使用锁保护,但线程会更激烈地争夺栈容器,迫使它们串行化。

未提供等待操作,若容器满载而线程又等待加入数据,pop()函数线程耗费算力(调用empty())检查容器是否为空。

2.2线程安全的队列容器(锁和条件变量)

#include<queue>
#include <memory>
#include <mutex>
#include<condition_variable>
template <typename T>
class thread_safe_queue
{
private:
	std::queue<T> q;
	mutable std::mutex m;
	std::condition_variable cond;
public:
	//构造和复制构造
	thread_safe_queue() {}
	thread_safe_queue(const thread_safe_thread& other)
	{
		std::lock_guard<std::mutex> lk(other.m);
		q = other.q;
	}

	void push(T data)
	{
		std::lock_guard<std::mutex> lk(m);
		q.push(data);
		cond.notify_one();
	}

	void wait_and_pop(T& val)
	{
		std::unique_lock<std::mutex> lk(m);
		cond.wait(lk, [this] {return !q.empty(); });
		val = q.front();
		q.pop();
	}
	std::shared_ptr<T> wait_and_pop()
	{
		std::unique_lock<std::mutex> lk(m);
		cond.wait(lk, [this] {return !q.empty(); });
		std::shared_ptr<T> res = std::make_shared<T>(q.front());
		q.pop();
		return res;
	}
	bool try_pop(T& val)
	{
		std::lock_guard<std::mutex> lk(m);
		if (q.empty()) return false;
		val = q.front();
		q.pop();
		return true;
	}
	std::shared_ptr<T> try_pop()
	{
		std::lock_guard<std::mutex> lk(m);
		if (q.empty()) return std::shared_ptr<T>();
		std::shared_ptr<T> res = std::make_shared<T>(q.front());
		q.pop();
		return res;
	}

	bool empty() const
	{
		std::lock_guard<std::mutex> lk(m);
		return q.empty();
	}
};

本例使用了条件变量进行通知-等待,新增了两个wait_and_pop()函数,等待弹出的线程不必连续调用empty(),改为wait_and_pop()。

觉醒的线程在执行wait_and_pop()时,可能抛出异常(如新指针std::shared_ptr<>构建时),导致没有线程被唤醒,我们有几种方法:

1、改为cond.notify_all()通知所有线程,但只有一个线程能弹出,其余继续休眠,开销增大。

2、异常抛出后再次调用cond.notify_one()通知其它线程。

3、修改底层队列容器存储std::shared_ptr<>,并将std::shared_ptr初始化语句移到push()调用处。

使用std::shared_ptr<>存储好处:在push中为新的std::shared_ptr实例分配内存可以脱离保护,增强性能:

	//原版
    void push(T data)
	{
		std::lock_guard<std::mutex> lk(m);
		q.push(data);
		cond.notify_one();
	}
    //改进
    void push(T data)
	{
		std::shared_ptr<T> data(std::make_shared<T>(std::move(data)));
		std::lock_guard<std::mutex> lk(m);
		q.push(data);
		cond.notify_one();
	}

2.3精细粒度的锁和条件变量提高队列并发度

采用std::unique_ptr管控节点,不再需要时会自动删除。

问题:我们可以给head和tail各设置一个互斥,但push函数可以同时修改两个指针,需要两个互斥。且push和pop可能并发访问同一节点的next指针,tail->next和old_head->next有可能为同一节点(容器仅有一项数据)。

解决:我们设置一个虚位节点(dummy node),确保至少存在一个节点,区别头尾两个节点的访问。若队列为空,则head和tail指向虚位节点,此时向队列增添数据,head->next和tail->next指向不同节点。

template<typename T>
class queue
{
private:
	struct node
	{
		std::shared_ptr<T> data;//避免重复内存分配
		std::unique_ptr<node> next;
	};
	std::unique_ptr<node> head;//不需要某个节点时,它和所包含的数据被自动删除
	node* tail;
public:
	queue() :head(new node), tail(head.get()) {}
	//删除复制构造和赋值
	queue(const queue& other) = delete;
	queue& operator=(const queue& other) = delete;

	std::shared_ptr<T> try_pop()
	{
		if (head.get() == tail)//都指向虚位节点说明空 get函数从unique返回指针
		{
			return std::shared_ptr<T>();
		}
		const std::shared_ptr<T> res(head->data);
		std::unique_ptr<node> old_head = std::move(head);
		head = std::move(old_head->next);
		//此处修改完毕可解锁
		return res;
	}
	void push(T new_value)
	{
		std::shared_ptr<T> new_data(
			std::make_shared<T>(std::move(new_value))
		);
		std::unique_ptr<node> p(new node);//创建新节点
		tail->data = new_data;
		tail->next = std::move(p);
		const node* new_tail = p.get();//因为tail的类型为node*,next指针为unique_ptr,故创建新节点
		tail = new_tail;
	}
};

好处:push()只访问tail指针而不再触及head指针,pop()仅在最开始比较的访问tail指针,短暂持锁。虚位节点使得pop和push不再同时访问相同节点。

尝试加入互斥实现并行化:
 push()中新节点一旦创建完成,我们就锁住互斥,函数结束才释放。
 pop()的head指针需要一直持锁。一旦head改动完成就释放锁,返回操作无需持锁。
 pop()对tail的访问仅有一次,可以包装为一个函数。

template<typename T>
class queue
{
private:
	struct node
	{
		std::shared_ptr<T> data;//避免重复内存分配
		std::unique_ptr<node> next;
	};
	std::mutex head_mutex;
	std::unique_ptr<node> head;//不需要某个节点时,它和所包含的数据被自动删除
	node* tail;
	//将互斥封装
	//获取尾部
	node* get_tail()
	{
		std::lock_guard<std::mutex> tail_lock(tail_mutex);
		return tail;
	}
	//pop
	std::unique_ptr<node> pop_head()
	{
		//此处head_mutex在tail_mutex之前,很重要,如若不然,可能其它线程先调用pop锁住了head_mutex,导致本线程的pop停滞不前。
		std::lock_guard<std::mutex> head_lock(head_mutex);
		if (head.get() == get_tail()) return std::unique_ptr<node>();//get_tail需要在head_mutex保护范围内,若在作用域外,可能tail和head已经改变
		std::unique_ptr<node> old_head = std::move(head);
		head = std::move(old_head->next);
		return old_head;
	}
public:
	queue() :head(new node), tail(head.get()) {}

	//删除复制构造和赋值
	queue(const queue& other) = delete;
	queue& operator=(const queue& other) = delete;

	std::shared_ptr<T> try_pop()
	{
		std::unique_ptr<node> old_head = pop_head();
		return old_head ? old_head->data : std::shared_ptr<T>();
	}
	void push(T new_value)
	{
		std::shared_ptr<T> new_data(
			std::make_shared<T>(std::move(new_value))
		);
		std::unique_ptr<node> p(new node);//创建新尾部节点
		const node* new_tail = p.get();//因为tail的类型为node*,next指针为unique_ptr,故创建新节点
        std::lock_guard<std::mutex> tail_lock(tail_mutex);
		tail->data = new_data;
		tail->next = std::move(p);
		tail = new_tail;
	}
};

异常处理安全分析:
try_pop():只有在互斥加锁时才可能抛出异常,在获取锁后数据才会发生变动,异常安全。
push():在堆分配上创建了两个实例(T类型和node类型),因为对象是智能指针,万一有异常抛出,占用的内存会自动释放,获取锁后的操作不会抛出异常,异常安全。
并发度分析:相比以前的版本更精细的锁。
push():在没有持锁的状态下为新节点和数据分配内存,允许多个线程为新节点和数据并发分配内存。且每次仅有一个线程将新节点如队。
try_pop():仅在读取tail指针时短暂持锁,比较过程无需持锁,整个调用过程可以与push()并发执行。虽然每次允许一个线程调用pop_head但多个线程可以并发执行try_pop()其它部分。
另外,unique_ptr<node> old_head析构函数开销高,在互斥保护外执行。

2.4等待数据弹出的安全栈

上述代码仅提供try_pop()重载,是否能借助精细的锁实现wait_and_pop()?答案是肯定的,将断言设置为head!=get_tail,只需持有互斥head_mutex,调用cond.wait()时重新锁住互斥head_mutex。

try_pop()的另一重载接收value引用参数,再用拷贝赋值操作赋予它old_head的值,因为拷贝赋值可能抛出异常,所以需要在互斥保护范围内。

empty()只需要锁住head_mutex互斥,然后检查head==get_tail()即可。

我们可以把pop相关操作中重复的代码封装为函数,以下是完整版:

template<typename T>
class queue
{
private:
	struct node
	{
		std::shared_ptr<T> data;//避免重复内存分配
		std::unique_ptr<node> next;
	};
	std::mutex head_mutex;
	std::mutex tail_mutex;
	std::unique_ptr<node> head;//不需要某个节点时,它和所包含的数据被自动删除
	node* tail;
	std::condition_variable cond;
	//-------------------辅助函数---------------------------
	node* get_tail()
	{
		std::lock_guard<std::mutex> tail_lock(tail_mutex);
		return tail;
	}
	//pop返回old_head,需要再其它函数加锁
	std::unique_ptr<node> pop_head()
	{
		std::unique_ptr<node> old_head = std::move(head);
		head = std::move(old_head->next);
		return old_head;
	}
	//等待队列中有数据
	std::unique_lock<std::mutex> wait_for_data()
	{
		std::unique_lock<std::mutex> head_lock(head_mutex);
		cond.wait(head_lock, [&] {return head.get() != get_tail(); });
		return std::move(head_lock);//返回锁实例
	}
	//---------------------辅助函数---------------------------

	//wait_and_pop的重载
	std::unique_ptr<node> wait_pop_head()
	{
		std::unique_lock<std::mutex> head_lock(wait_for_data());//利用wait_for_data()的锁,保证头节点弹出的全过程都持有同一个锁
		return pop_head();
	}
	std::unique_ptr<node> wait_pop_head(T& value)
	{
		std::unique_lock<std::mutex> head_lock(wait_for_data());
		value = std::move(head.get()->data);//*head->data
		return pop_head();
	}
	//try_pop_head的重载
	std::unique_ptr<node> try_pop_head()
	{
		std::lock_guard<std::mutex> lk(head_mutex);//因为没有wait所以不调用wait_for_data()
		if (head.get() == get_tail())
		{
			return std::unique_ptr<node>();
		}
		return pop_head();
	}
	std::unique_ptr<node> try_pop_head(T& value)
	{
		std::lock_guard<node> lk(head_mutex);
		if (head.get() == get_tail())
		{
			return std::unique_ptr<node>();
		}
		value = std::move(head.get()->data);
		return pop_head();
	}

public:
	queue() :head(new node), tail(head.get()) {}

	//删除复制构造和赋值
	queue(const queue& other) = delete;
	queue& operator=(const queue& other) = delete;

	//try_pop函数
	std::shared_ptr<T> try_pop();
	bool try_pop(T& value);

	//wait_and_pop函数
	std::shared_ptr<T> wait_and_pop();
	void wait_and_pop(T& value);

	void push(T new_value);

	bool empty();
};

push()函数增加条件变量的通知:

void queue<T>::push(T new_value)
{
	std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));
	std::unique_ptr<node> p(new node);
	const node* new_tail = p.get();
	{
		std::lock_guard<std::mutex> tail_lock(tail_mutex);
		tail->data = new_data;
		tail->next = std::move(p);
		tail = new_tail;
	}
	cond.notify_one();
}

wait_and_pop()函数调用wait_pop_head(),而后者调用公用函数pop_head返回old_head:

//wait_and_pop
template<typename T>
std::shared_ptr<T> queue<T>::wait_and_pop()
{
	std::unique_ptr<node> const old_head = wait_pop_head();
	return old_head->data;
}

template<typename T>
void queue<T>::wait_and_pop(T& value)
{
	std::unique_ptr<node> const old_head = wait_pop_head(value);
}

try_pop()同理调用try_pop_head:

template<typename T>
std::shared_ptr<T> queue<T>::try_pop()
{
	std::unique_ptr<node> old_head = try_pop_head();
	return old_head ? old_head->data : std::shared_ptr<T> ();
}

template<typename T>
bool queue<T>::try_pop(T& value)
{
	const std::unique_ptr<node> old_head = wait_pop_head(value);
	return old_head;
}

empty():

template<typename T>
bool queue<T>::empty()
{
	std::lock_guard<std::mutex> lk(head_mutex);
	return (head.get()==get_tail());
}

以上例子是无限队列,只要存在空闲内存就能一直压入数据,通常我们需要限制容器中元素的数量,因为某些线程向队列添加任务的速度可能远超线程从队列领取任务的速度,可以在push函数中加以限制。 

3.更复杂的基于锁的并发结构

3.1线程安全的查找表

查找表又称“字典”,其元素由两部分组成:键值(key type)和值(mapped type)。标准库中的:std::map<>,std::multimap<>,std::unordered_map<>,std::unordered_multimap<>。

查找表的几项基本操作:

 增加配对的键值对。

 根据给定键值改变关联值。

 移除某个键及其关联的值。

 根据给定的键获取关联值。(若键值存在)

暂且搁置迭代器这一复杂的部分,还有一些有用的整体操作:复制整个容器,检查是否为空等。

注意的地方:
两个线程分别加入键值相同而值不同的元素,则先添加的操作成功,后添加的失败。

依据键值获取值的前提是“若键值存在”,我们可以准许用户设定默认值,若键值不存在则函数返回默认值充当结果,如:mapped type get_value(const key_type& key, mapped_type default)。我们也可以返回智能指针,指向查找目标的关联值,若指针为NULL说明查找失败,无值返回。

精细粒度锁操作的map数据结构

有三种方法可以实现关联容器:
二叉树(红黑树),有序数组,散列表

·二叉树,每次查找都要从根节点开始访问,根节点加锁,随着访问线程逐层向下移动,根节点上的锁随之释放。

·有序数组,无法预知所需查找目标的位置,唯有对整个数组使用单一的锁。

·散列表,散列表中每个键属于一个桶,桶内链表装有相同散列值,我们可以使用标准库提供的函数模版std::hash<>设置散列函数。

散列表是比较适合的底层容器,可以为每个桶设置独立的锁,若采用共享锁,则支持多个读线程或一个写线程,能有效提高并发度。

template<typename Key, typename Value, typename Hash = std::hash<Key>>//标准库提供的函数模版
class lookup_table 
{
private:

	class bucket_type {
		typedef std::pair<Key, Value> bucket_value;
		typedef std::list<bucket_value> bucket_data;//桶内部存储键值对的链表,每个链表装有键值(键值除以总size的余数)相同的键值对
		typedef typename bucket_data::itrator bucket_iterator;
		bucket_data data;
		mutable std::shared_mutex mutex;//共享锁

		bucket_iterator find_entry_for(const Key& key) const//找到键值对应的桶的开始位置
		{
			return std::find_if(data.begin(), data.end(),//遍历数据链表,数据链表中的每个元素为bucket_value
				[&](const bucket_value& item) {return item.first == key; })
		}
	public:
		Value value_for(const Key& key, const Value& default_value)const//根据key查找值
		{
			std::shared_lock<std::shared_mutex> lock(mutex);//共享方式加锁(只读)
			const bucket_iterator found_entry = find_entry_for(key);
			return (found_entry == data.end()) ? default_value : found_entry->second;
		}
		void add_or_update_mapping(const Key& key, const Value& value)
		{
			std::unique_lock<std::shared_mutex> lock(mutex);//独占方式加锁(读/写)
			const bucket_iterator found_entry = find_entry_for(key);
			if (found_entry == data.end())//新的键值数据则直接push_back
			{
				data.push_back(bucket_value(key, value));
			}
			else //找到则更改键值对应的关联值
			{
				found_entry->second = value;
			}
		}
		void remove_mapping(const Key& key)
		{
			std::unique_lock<std::shared_mutex> lock(mutex);
			const bucket_iterator found_entry = find_entry_for(key);
			if (found_entry != data.end())
			{
				data.erase(found_entry);
			}
		}
	};
	
	std::vector<std::unique_ptr<bucket_type>> buckets;
	Hash hasher;
	bucket_type& get_bucket(const Key& key) const//找键值对应的桶,此函数无须用锁保护(buckets的size固定)
	{
		const std::size_t bucket_index = hasher(key) % buckets.size();//通过哈希函数找到桶向量中的索引
		return *buckets[bucket_index];//返回键值对应桶
	}

public:
	typedef Key key_value;
	typedef Value mapped_type;
	typedef Hash hash_type;
	//构造函数
	lookup_table(unsigned num_buckets = 19, const Hash& hasher_ = Hash()):buckets(num_buckets), hasher(hasher_)
	{
		for (unsigned i = 0; i < num_buckets; ++i)
		{
			buckets[i].reset(new bucket_type);
		}
	}

	lookup_table(const lookup_table& other) = delete;//删除复制构造
	//接口通过调用对应元素对象的函数实现
	Value value_for(const Key& key, const Value& default_value = Value())const
	{
		return get_bucket(key).value_for(key, default_value);
	}

	void add_or_update_mapping(const Key& key, const Value& value)
	{
		return get_bucket(key).add_or_update_mapping(key, value);
	}

	void remove_mapping(const Key& key)
	{
		return get_bucket(key).remove_mapping(key);
	}
};

异常安全分析:

value_for()函数不涉及改动,抛出异常也不会影响数据结构,所以没问题。

remove_mapping()通过调用erase()改动链表,不抛出异常。

add_or_update_mapping()含有if语句,有两个分支流程,条件成立则push_back(),异常安全操作;条件不成立(原已有该键值对),则赋值操作替换原有值,赋值操作抛出异常只能期望原有值没有改变,由使用者提供的类型负责。

加入“数据快照”功能

数据快照,以快照形式取得查找表的当前状态,如保存为std::map<>。该功能要求锁住整个数据结构,所有桶被锁住(唯一一项锁住整体的操作),这样做确保获取的副本数据与查找表的状态保持一致。

std::map<Key, Value> lookup_table::get_map() const
	{
		std::vector<std::unique_lock<std::shared_mutex>> locks;
		for (unisgned i = 0; i < buckets.size(); ++i)
		{
			locks.push_back(std::unique_lock<std::shared_mutex>(buckets[i].mutex));//使用std::shared_mutex允许对桶进行并发读操作
		}
		std::map<Key, Value> res;
		for (unsigned i = 0; i < buckets.size(); ++i)
		{
			for (bucket_iterator it = buckets[i].data.begin(); it != buckets[i].data.end(); ++it)
			{
				res.insert(*it);//把list中的键值对插入到map中
			}
		}
		return res;
	}

3.2线程安全的链表

链表需要对外提供的功能:
向链表加入数据;根据条件从链表移除数据;在链表中查找数据;更新符合条件的数据;复制另一链表数据;按位置插入数据(此处实例忽略)。

为了实现精细粒度的所操作,可以给每个节点一个互斥,每个操作仅锁住目标节点,操作转移到下一节点时,原来的锁解开,实现真正的并发操作。

template<typename T>
class threadsafe_list
{
	struct node
	{
		std::mutex m;
		std::shared_ptr<T> data;
		std::unique_ptr<node> next;
		node():next(){}
		node(const T& value) :data(std::make_shared<T>(value)) {}
	};
	node head;//虚节点
public:
	threadsafe_list(){}
	~threadsafe_list()
	{
		remove_if([](const& node) {return true; });
	}
	//删除复制构造和赋值函数
	threadsafe_list(const threadsafe_list& other) = delete;
	threadsafe_list& operator=(const thread_list& other) = delete;

	void push_front(T& value)
	{
		std::unique_ptr<node> new_node(new node(value));//堆上分配内存
		std::lock_guard<std::mutex> lk(head.m);
		new_node->next = std::move(head.next);
		head.next = std::move(new_node);
	}
	//--------------迭代函数
	template<typename Function>
	void for_each(Function f)//每项数据都传入用户给定的函数
	{
		node* current = &head;
		std::unique_lock<std::mutex> lk(current.m);
		while (node* const next = current->next.get())//因为next为unique_ptr,直接赋值会夺走归属权
		{
			std::unique_lock<std::mutex> new_lk(next->m);

			lk.unlock();//先锁住新互斥再解锁,保证next指向的内容不被改变(const仅保证指向的地址不变)
			
			f(*(next->data));
			current = next;
			lk = std::move(new_lk);//转移归属权
		}
	}
	template<typename Predicate>
	std::shared_ptr<T> find_first_if(Predicate p)
	{
		node* current = &head;
		std::unique_lock<std::mutex> lk(current.m);
		while (node* const next = current->next.get())
		{
			std::unique_lock<std::mutex> new_lk(next->m);
			lk.unlock();
			if (p(*(next->data)))//满足条件则返回
			{
				return next->data;
			}
			current = next;
			lk = std::move(next_lk);
		}
		return std::shared_ptr<T>();
	}
	template<typename Predicate>
	void remove_if(Predicate p)
	{
		node* current = &head;
		std::unique_lock<std::mutex> lk(current.m);
		while (node* const next = current->next.get())
		{
			std::unique_lock<std::mutex> n_lk(next.m);
			if (p(*(next->data)))
			{
				std::unique_ptr<node> old_next = std::move(next);//智能指针指向next,if运行完成自动销毁
				current->next = std::move(next->next);
				n_lk.unlock();//current不需要改变
			}
			else
			{
				lk.unlock();
				current = next;
				lk = std::move(n_lk);
			}
		}
	}
};

迭代操作单向进行,都是先锁住下一节的互斥,再解锁当前节点的互斥,确保锁操作顺序不可逆。

条件竞争的可能:remove_if中智能指针销毁发生在解锁之后,因为销毁已锁互斥使未定义行为。此时current持有锁,没有线程能越过当前节点,故操作安全。

4.要点小结

1.使用虚头节点避免push(tail互斥)和pop(短暂tail互斥,head互斥)的数据竞争。

2.容器保存std::shared_ptr<>指向底层数据。优点:避免重复分配内存;可在锁住互斥前完成新数据的初始化,提升性能。


3.使用unique_ptr<>管控节点。不需要时自动销毁,普通指针指向时需要调用get(),否则夺走归属权。


4.设计辅助函数返回锁,在接口中可以直接利用辅助函数的锁。                                                如:std::unique_lock<std::mutex> head_lock(wait_for_data());


5.pop在返回弹出元素时可释放锁。


6.迭代函数中,在释放前一个节点的互斥前应锁住下一个节点的互斥。

[读书笔记]C++并发编程实战 [读书笔记]C++并发编程实战 原文地址:https://github.com/xuyicpp/multi_threading C++并发编程的内容每一章都从浅入深讲解的很详细,虽然第一遍有一些还不是很懂,例如无锁编程和线程池,以后遇到这些问题可以回来翻翻笔记加深印象。 multi_threading 《C++并发编程实战》的读书笔记,供以后工作中查阅。 第一章 何谓并发和多线程... 阅读详情

相关推荐

C++并发编程完全指南:从基础到实践

在当今多核处理器普及的时代,充分利用硬件并发能力已成为高性能编程的关键。C++11引入的现代并发编程支持使得开发者能够以标准化、可移植的方式编写多线程程序。本文将全面介绍C++并发编程的各个方面,从基础概念到实际应用,帮助您掌握这一重要技能。

vvilkin的学习备忘 2195

并发数据结构 : SpinWait

老实说,没有哪个开发人员愿意在其编码时还要考虑线程同步。更糟糕的情况是,编写线程同步代码一点也不好玩。稍一不慎,就会导致共享资源状态不一致,从而引发程序未预期行为。此外,当我们添加线程同步代码时还会导致程序运行变慢,损害性能和可伸缩性。从这点上来看,线程同步简直一无是处。可惜,这也是现实生活中必要的一部分。尤其在多核CPU成为主流的今天。 考虑下这种情况:只有一个线程试图访问某个资源。...

weixin_30387663的博客 408

【C++】并发编程

一、线程创建与管理 1.1 并发 1.1.1 并发与并行 并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。 并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生。 1.1.2 硬件并发与任务切换 单片机上的单核处理器支持并发多任务处理,依靠任务切换实现,与多核处理器上的多任务并发处理方式不同。 双核处理器并行执行(硬件并发)对比单核处理器并发执行(任务上下文切换) 双核处理器均并发执行(一般任务数远大于处理器核心数,多核并发更常见) 1.1.3 多线程并发与多进程并发 任务

踏雪寻梅 2861

Java中的并发数据结构与多线程优化技术

Java中的并发数据结构与多线程优化技术 大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿! 在多线程编程中,并发数据结构和优化技术是提高系统性能和可靠性的关键。Java提供了丰富的并发数据结构和多线程优化技术,本文将详细介绍常用的并发数据结构及其使用方法,并讨论如何进行多线程优化。 并发数据结构 Java的java.util.concurrent包中提供了多种并发数据结构,这些数据结构在多线程环境下能够提供更高的性能和安全性。 ConcurrentHashMap Concurr

微赚淘客开发者博客 475

C++多线程--线程安全的哈希表(基于锁)

此篇为本专栏的第一篇文章。本专栏主要讲解C++并发编程相关的实践。包括但不限于首先,我们从基于锁的数据结构讲起。前段时间,我某个同事面试字节某业务时,面试官让其实现一个线程安全的哈希表。本文便以此开始,分几篇来讲解基于锁的线程安全的一些数据结构。

qls315的博客 1731

探索C++并发容器:std::vector的线程安全性

std::vector是C++标准库中的一个动态数组,它可以存储任何类型的元素,并且可以在运行时动态地增加或减少其大小。std::vector提供了一种高效的方式来管理和操作数组,它提供了一组丰富的函数,如push_back、pop_back、insert、erase等,用于添加、删除和修改元素。

夏天的狂风的专栏 3125

c++并发编程(六)—— 基于锁的并发数据结构设计

在我们进行并发编程的时候,如果多线程使用到了数据结构,那么程序设计过程中需要保证此数据结构的正确同步。有两种方法: 1、选择单独的互斥元与外部锁来保护数据 2、设计一个可以同时访问的数据结构 其中前几节我们涉及到了第一种方法,现在我们重点描述下第二种方法。 使用锁和条件变量的线程安全队列 template&lt;typename T&gt; class threadsafe_queue...

potxxx的博客 586

C++ 并发编程(C++11 到 C++17 )

为什么要并发编程大型的软件项目常常包含非常多的任务需要处理。例如:对于大量数据的数据流处理,或者是包含复杂GUI界面的应用程序。如果将所有的任务都以串行的方式执行,则整个系统的效率将会非常...

编程与实战的博客 4256

C++ 并发编程总结

C++ 的并发编程支持库包含了线程、原子操作、互斥、条件变量和 future 的内建支持。

专注计算机视觉算法训练,算法优化部署以及SDK开发的知识分享。 1854

C++并行与分布式编程_肖和平2004译.pdf

C++并行与分布式编程_肖和平2004译.pdf

[V-10]ARMv8/v9-CPU Virtualization-CPU虚拟化架构简介(vCPU/vPE)

本文简要介绍了CPU虚拟化架构的基础知识。

基层架构师的分享 1267

Multithreading-Cpp-Course:掌握现代C++并发编程的不二选择

Multithreading-Cpp-Course:掌握现代C++并发编程的不二选择 项目介绍 在现代软件开发领域,多线程编程是一个至关重要的技能。它不仅能提升程序的性能和响应速度,还能有效利用多核处理器的能力。Multithreading-Cpp-Course 是一套针对现代C++并发编程的教程源码,源自知名的在线教育平台Udemy上的课程 "Learn Multithreading ...

gitblog_01108的博客 705

巨量千川M-API实战:5分钟搞定短视频计划数据自动获取(Python版)

本文详细介绍了如何利用巨量千川M-API和Python快速搭建自动化工具,实现短视频计划数据的自动获取。通过实战代码演示了从环境准备、Token管理到核心数据接口调用的完整流程,帮助电商运营和技术开发者告别手动导出,高效完成数据汇总与分析。

weixin_29325515的博客 1359

C++并发编程 -7. 并发代码设计

由于counter是全局变量,当多个线程同时调用processing_loop去修改同一个变量,这就要求counter在缓存内做一份拷贝,再改变自己的值,或其他线程以发布的方式对缓存中的拷贝副本进行更新。虽然为每个线程分配不同的数据块,但工作的划分(无论是之前就划分好,还是使用递归的方式划分)仍然在理论阶段,因为这里每个线程对每个数据块的操作是相同的。根据线程的操作性质,以及使用到的内存序,这样的修改可能会让第二个处理器停下来,等待硬件内存更新缓存中的数据。这样,每个线程访问的数据项就不会互相干扰。

qq_45604814的博客 823

MultiOTP Credential Provider避坑指南:从安装到RDP双因素认证的全流程解析

本文详细解析了MultiOTP Credential Provider从安装到实现RDP双因素认证(MFA)的全流程,重点分享了Docker部署、Active Directory集成及网络级认证(NLA)兼容性等环节的实战避坑经验,旨在帮助运维人员高效、稳定地部署离线双因素认证方案,提升Windows远程桌面安全性。

weixin_29255731的博客 642

Ubuntu 24.04 极速部署 Dify:从零到一的保姆级实践

本文详细介绍了在Ubuntu 24.04系统上快速部署Dify的完整教程,从环境准备到部署验证,再到深度优化和安全加固,提供了一套保姆级的实践指南。通过使用Docker和Docker Compose,结合国内镜像源加速,帮助用户高效完成Dify的安装和配置,确保系统稳定运行。

weixin_42662293的博客 246

C++并发编程:高级同步机制与并发数据结构

本文介绍了C++并发编程的高级同步机制与并发数据结构。主要内容包括:1)高级同步机制如共享互斥锁(shared_mutex)、屏障(barrier)和门闩(latch)的使用方法;2)并发数据结构如concurrent_vector和concurrent_queue的实现与应用;3)通过代码示例演示了多线程环境下读写锁、线程同步以及线程安全容器的实现方式。这些技术能够帮助开发者构建高性能、可扩展的并发应用程序,解决复杂的并发编程问题。

编程世界如山海,探索不止。 8169

ZYNQ学习:PL端DDR3 读写测试

DDR SDRAM 的最大特点是双沿触发,即在时钟的上升沿和下降沿都能进行数据采集和发送,同样的工作时钟, DDR SDRAM 的读写速度可以比传统的 SDRAM 快一倍。本次实验使用的 DDR3 芯片是南亚的 NT5CC256M16ER-EKI, NT5CC256M16ER-EKI 的 bank 位宽为 3,行位宽为 15,列位宽为 10,所以它的地址大小等于 2^3*2^15*2^10(即 2^28=256M),数据位宽为 16bit,所以容量大小为 256M*16bit,也就是 512MByte。

shuidiQAQ的博客 3031

C++语言的并发编程

随着现代计算需求的不断增加,特别是在大数据、人工智能和实时系统等领域,并发编程变得越来越重要。C++作为一种高性能的编程语言,提供了多种并发编程的机制,使得开发人员能够充分利用多核CPU的优势,实现高效的程序设计。在本篇文章中,我们将深入探索C++的并发编程,包括其基本概念、线程的使用、同步机制以及编写高效并发代码的技巧。

2401_90031868的博客 889

JavaSpringBoot + VUE 相关开发环境配置合集

1.Java环境-JDK安装 参照:Java基础1-环境篇:JDK安装与环境变量配置 Java基础1-环境篇:JDK安装与环境变量配置_第一段代码的博客-优快云博客_jdk 2.Web服务器-Tomcat安装 原创Java服务器Tomcat安装和配置 3.Maven(后面为了项目兼容性,使用了3.0.5版本) 原创Maven安装和配置(Maven3.0.5) 4.nginx 原创nginx轻量级Web服务器安装和配置 5 数据库mysql 原创数据库Mysql安装和配置 ...

第一段代码的博客 3164

【原理图PCB专题】Allegro模块化移动器件报...has the LOCKED property怎么解锁?

正常来说做好一个模块化,如果不需要再动,最好还是lock,防止不明白的工程师异动导致问题。如果复用布局设计时,因为有些器件和拓扑并不完全一致,那需要调整部分器件,被锁之后只能全选,无法修改。所以解开锁定后,就能正常编辑了。

一点硬件 2391

C++ 并发编程实战 学习笔记

C++ 并发编程实战学习记录

myc13381的博客 1707

基于知识图谱的档案智慧问答系统:构建智能化档案管理与知识服务新范式

基于知识图谱的档案智慧问答系统是档案管理领域的一次重要创新,它通过将档案数据转化为结构化的知识图谱,并结合自然语言处理和智能问答技术,实现了档案信息的智能化管理与服务。该系统不仅能够提升档案管理的效率,还能为用户提供更全面、更便捷的知识服务,具有广泛的应用前景和深远的社会意义。未来,随着技术的不断进步和应用场景的不断扩展,基于知识图谱的档案智慧问答系统将在更多领域发挥其价值,为档案管理领域的智能化转型提供强有力的支持。

qq_62634009的博客 1578

C++并发编程 -6. 无锁并发数据结构设计

风险指针”是指Maged Michael发明的一种技法, 后来被IBM申请为专利。简言之可以将要删除的节点进行特殊处理,如果有线程正在使用我们将这个节点的指针标记为风险指针,其他线程不可删除。我们实现hazard_pointer类,管理风险指针和线程id。id为正在使用该风险指针的id,pointer为指针类型,存储的节点数据地址。当一个线程从风险数组中查找某个闲置节点作为风险节点,则需要将pointer指向节点的数据,并且将id设置为当前的线程id。定义一个全局的风险节点数组,用来存储风险节点。

qq_45604814的博客 1168

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

《c++并发编程实战》 笔记

这个新的可调用对象在调用时,会调用原始的可调用对象,并传递给它预先绑定的参数(如果有的话),以及调用新可调用对象时提供的任何额外参数。使用std::shared_future::wait,std::shared_future 与 std::future 类似,但主要区别在于 std::shared_future 可以被多个线程或对象共享,而 std::future 一旦被移动或拷贝后,原始对象将不再持有任何结果,变成空状态。std::call_once 是 C++11 引入的一个函数,它属于 头文件。

qq_42974718的博客 1475

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

上一篇: C++并发编程(五)内存模型与原子操作
下一篇: C++并发编程(七)无锁数据结构
Cvincent976
博客等级 码龄6年 34粉丝 20原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值