一、无锁数据结构体的优点和缺点
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了。
四、无锁队列
参考:《c++并发编程实战(第二版)》安东尼.威廉姆斯 著;吴天明 译;