DoubleLi

qq: 517712484 wx: ldbgliet

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

 

 

一、无锁数据结构体的优点和缺点

1、优点

  • 最大限度实现并发

在基于锁的容器上,若某个线程还未完成操作,就大有可能阻塞另一线程,使之陷入等待而无法处理;而且,互斥锁的根本意图是杜绝并发功能。在无锁数据结构上,总是存在某个线程能执行下一操作。

  • 代码的健壮性

假设数据结构的写操作受锁保护,如果线程在持锁期间终止,那么该数据结构只完成了部分改动,且此后无从修补。但是(五锁数据结构),若某线程操作无锁数据时意外终结,则丢失的数据仅限于它持有的部分,其他数据依然完好,能被别的线程正常处理。
无锁数据结构不含锁,因此不会出现死锁

2、缺点

  • 难度大
    对线程安全的无锁数据结构执行写操作,难度远远高于对带锁的数据结构体执行写操作。

eg.需留心施加在各项操作上的内存次序约束。

  • 活锁

由于无锁数据结构完全不含锁,因此不可能出现死锁,但活锁反而有机会出现。假设两个线程同时修改同一份数据结构,若他们所做的改动都导致对方从头开始操作,那双方就会反复循环,不断重试,这种现象即为活锁。
活锁出现与否完全取决于线程的调度次序,故往往只会短暂存在。因此,它们不仅降低了程序性能,尚不至于造成严重的问题,但我们仍需小心防范。

这种模式的缺点在于,即便没有其他线程同时访问数据机头,也依然要执行更多步骤。

  • 降低整体性能
    上面说的这一缺点会降低无锁代码的效率:虽然它提高了操作同一个数据结构的并发程度,缩短了单个线程因等待而消耗的时间,却有可能降低整体性能。

首先对比原子操作,无锁代码所采用的原子操作要缓慢很多。对于基于锁的数据结构,其原子操作仅涉及互斥的加锁行为,相比之下,无锁数据结构种原子操作的数据可能更多。

二、无锁数据结构分类

无锁数据结构,我们只讲两种较为简单的数据结构:无锁栈容器、无锁队列

三、无锁栈容器

1、简介

  • 栈容器能加入数据,然后按逆序取出——先进后出(后进先出——last in,fist out,LIFO).
  • 因此,我们必须保证,一旦某线程将一项数据加入栈容器,就能立即安全的被另一个线程取出,同时还得保证,只有唯一一个线程能获取该项数据。
  • 最简单的栈容器可以通过链表的形式实现:指针head指向第一个节点,各节点内的next成员指针再依次指向后继节点。

2、无锁栈原理图

3、添加节点(push)

步骤1:创建新节点。
步骤2:令新节点的成员指针next指向当前的头节点。
步骤3:把head指针指向新节点。

备注:node也可以放到lock_free_stack的内部,就看你怎么使用了。

template<typename T>
struct node
{
	T value;
	node* next;
	node(T const& data_) :value(data_), next(nullptr) {}
};

template<typename T>
class lock_free_stack
{
private:
	/*struct node
	{
		T value;
		node* next;
		node(T const& data_):value(data_),next(nullptr){}
	};*/
private:
	//std::atomic<node*> m_head;
	std::atomic<node<T>*> m_head;
public:
	lock_free_stack() 
	{
		m_head.store(nullptr);
	};

	void push(T const& data)
	{
		node<T>* const new_node = new node(data);
		new_node->next = m_head.load();
		while (!m_head.compare_exchange_weak(new_node->next, new_node));
	}
};

 

4、添加数据(添加节点demo)

我们创建两个线程,分别往无锁栈中添加数据。

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

using namespace std;

template<typename T>
struct node
{
	T value;
	node* next;
	node(T const& data_) :value(data_), next(nullptr) {}
};

template<typename T>
class lock_free_stack
{
private:
	/*struct node
	{
		T value;
		node* next;
		node(T const& data_):value(data_),next(nullptr){}
	};*/
private:
	//std::atomic<node*> m_head;
	std::atomic<node<T>*> m_head;
public:
	lock_free_stack() 
	{
		m_head.store(nullptr);
	};

	void push(T const& data)
	{
		node<T>* const new_node = new node(data);
		new_node->next = m_head.load();
		while (!m_head.compare_exchange_weak(new_node->next, new_node));
	}
};

lock_free_stack<string> free_stack;

void task0() 
{
	for (int i = 0; i < 5; i++)
	{
		string temp = to_string(i) + ":i love chine";
		free_stack.push(temp);
	}
}
void task1()
{
	for (int i = 0; i < 5; i++)
	{
		string temp = to_string(i) + ":i love myself";
		free_stack.push(temp);
	}
}

int main()
{
	thread t0(task0);
	thread t1(task1);

	t0.join();
	t1.join();
}

5、弹出数据

  • 步骤1:读取head指针当前的值。
    步骤2:读取head->next
    步骤3:将head指针改为head->next的值
    步骤4:弹出原栈顶点node,获取其所含数据data作为返回值。
    步骤5:删除已弹出的节点。

备注:存在内存泄露,暂时不考虑内存泄露。

template<typename T>
class lock_free_stack
{
private:
	struct node
	{
		T value;
		node* next;
		node(T const& data_):value(data_),next(nullptr){}
	};
private:
	std::atomic<node*> m_head;
public:
	lock_free_stack() 
	{
		m_head.store(nullptr);
	};

	void push(T const& data)
	{
		node* const new_node = new node(data);
		new_node->next = m_head.load();
		while (!m_head.compare_exchange_weak(new_node->next, new_node));
	}
	void pop(T& result)
	{
		node* old_head = m_head.load();
		while (old_head && !m_head.compare_exchange_weak(old_head, old_head->next));
		result = old_head?old_head->value:"";
	}
};

6、弹出数据demo

针对无锁栈,开三个线程,两个线程放入数据,一个线程读取数据。
线程0:往无锁栈放入数据——i love china
线程1:往无锁栈放入数据——ok
线程2:读取无锁栈中的数据,并输出

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

using namespace std;

template<typename T>
class lock_free_stack
{
private:
	struct node
	{
		T value;
		node* next;
		node(T const& data_):value(data_),next(nullptr){}
	};
private:
	std::atomic<node*> m_head;
public:
	lock_free_stack() 
	{
		m_head.store(nullptr);
	};

	void push(T const& data)
	{
		node* const new_node = new node(data);
		new_node->next = m_head.load();
		while (!m_head.compare_exchange_weak(new_node->next, new_node));
	}
	void pop(T& result)
	{
		node* old_head = m_head.load();
		while (old_head && !m_head.compare_exchange_weak(old_head, old_head->next));
		result = old_head?old_head->value:"";
	}
};

lock_free_stack<string> free_stack;

void task0() 
{
	for (int i = 0; i < 5; i++)
	{
		cout << "task0\n";
		string temp = " i love china_" + to_string(i) + "\n";
		free_stack.push(temp);
	}
}

void task1()
{
	for (int i = 0; i < 5; i++)
	{
		cout << "task1\n";

		string temp = " ok_"+to_string(i) +"\n";
		free_stack.push(temp);
	}
}

void task2()
{
	while (1)
	{
		string temp;
		free_stack.pop(temp);
		if (0 != temp.size())
		{
			cout << "task2:"+temp;
		}
	}
}

int main()
{
	thread t0(task0);
	thread t1(task1);
	thread t2(task2);

	t0.join();
	t1.join();
	t2.join();
}

输出如下

7、无锁栈和智能指针

  • 问题1:向pop函数传入引用以获取结果。
    仅当我们确认正在栈容器尚弹出节点的只有当前线程时,复制数据才是安全行为,矛盾之处在于,那样需要先移除栈顶节点。

比如,假如有两个线程同时pop,这样获取栈顶节点就有可能出问题。

解决:以值的形式返回对象。

  • 问题:以值的形式返回对象的问题。
    以值的形式返回对象存在异常安全的隐患:如果复制返回值导致异常抛出,变化造成返回值的丢失。
    解决:返回一个智能指针,指向所获取的值。

  • 实现

template<typename T>
class lock_free_stack
{
private:
	struct node
	{
		shared_ptr<T> value;
		node* next;
		node(T const& data_):value(make_shared<T>(data_)),//根据类型T重新分配一块内存
			next(nullptr){}
	};
private:
	std::atomic<node*> m_head;
public:
	lock_free_stack() 
	{
		m_head.store(nullptr);
	};

	void push(T const& data)
	{
		node* const new_node = new node(data);
		new_node->next = m_head.load();
		while (!m_head.compare_exchange_weak(new_node->next, new_node));
	}
	shared_ptr<T> pop()
	{
		node* old_head = m_head.load();
		while (old_head && !m_head.compare_exchange_weak(old_head, old_head->next));
		return old_head?old_head->value:shared_ptr<T>();
	}
};
  • demo
#include <iostream>
#include <atomic>
#include<thread>
#include<string>

using namespace std;

template<typename T>
class lock_free_stack
{
private:
	struct node
	{
		shared_ptr<T> value;
		node* next;
		node(T const& data_):value(make_shared<T>(data_)),//根据类型T重新分配一块内存
			next(nullptr){}
	};
private:
	std::atomic<node*> m_head;
public:
	lock_free_stack() 
	{
		m_head.store(nullptr);
	};

	void push(T const& data)
	{
		node* const new_node = new node(data);
		new_node->next = m_head.load();
		while (!m_head.compare_exchange_weak(new_node->next, new_node));
	}
	shared_ptr<T> pop()
	{
		node* old_head = m_head.load();
		while (old_head && !m_head.compare_exchange_weak(old_head, old_head->next));
		return old_head?old_head->value:shared_ptr<T>();
	}
};

lock_free_stack<string> free_stack;

void task0() 
{
	for (int i = 0; i < 5; i++)
	{
		cout << "task0\n";
		string temp = " i love china_" + to_string(i) + "\n";
		free_stack.push(temp);
	}
}

void task2()
{
	while (1)
	{
		shared_ptr<string> temp = free_stack.pop();
		string *a = temp.get();
		if (nullptr != a)
		{
			if (0 != a->size())
			{
				cout << "task2:" << *a << endl;
			}
		}
	}
}

int main()
{
	thread t0(task0);
	thread t2(task2);

	t0.join();
	t2.join();
}
  • demo输出

8、无锁栈内存泄露的解决

如果不是c++,其实不用考虑内存的问题。
但是c++就不行了,简单说,就是不管你怎么实现,new出的都是需要手动的释放的。方法蛮多的。
贴个书中的例子吧,也不写demo了。


四、无锁队列

参见【并发编程十六】无锁数据结构(2)——无锁队列

参考:《c++并发编程实战(第二版)》安东尼.威廉姆斯 著;吴天明 译;

 
posted on 2023-03-16 16:44  DoubleLi  阅读(279)  评论(2编辑  收藏  举报