使用互斥量、条件变量,以及“期望”来同步“阻塞”(blocking)数据的算法和数据结构。使用原子操作的“内存序”特性,并使用这个特性来构建无锁数据结构。
不使用阻塞库的数据结构和算法,被称为“无阻塞”(nonblocking)结构。不过,“无阻塞”的数据
结构并非都是无锁的(lock-free)。
使用std::atomic_flag
实现了一个简单的自旋锁(读书笔记4):
class spinlock_mutex
{
std::atomic_flag flag;
public:
spinlock_mutex() :
flag(ATOMIC_FLAG_INIT)
{}
void lock()
{
while (flag.test_and_set(std::memory_order_acquire));
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
用此来保护数据:有锁的非阻塞。
无锁数据结构:线程可以并发的访问。具有“比较/交换”操作的数据结构,通常在“比较/交换”实现中都有一个循环。使用“比较/交换”操作的原因:当有其他线程同时对指定数据的修改时,代码将尝试恢复数据。
无锁算法中的循环会让一些线程处于“饥饿”状态。如有线程在“错误”时间执行,那么第一个线
程将会不停得尝试自己所要完成的操作(其他程序继续执行)。“无锁-无等待”数据结构,就为了
避免这种问题存在的。
无等待数据结构:首先,是无锁数据结构;并且,每个线程都能在有限的步数内完成操
作,暂且不管其他线程是如何工作的。
使用原因:将并发最大化(主要);鲁棒性(次要)。
活锁(live locks):两个线程同时尝试修改数据结构,但每个线程所做的修改操作都会让另一个线程重启,所以两个线程就会陷入循环,多次的尝试完成自己的操作。
根据定义,无等待的代码不会被活锁所困扰,因其操作执行步骤是有上限的。换个角度,无等待的算法要比等待算法的复杂度高,且即使没有其他线程访问数据结构,也可能需要更多步骤来完成对应操作。
“无锁-无等待”代码的缺点:虽然提高了并发访问的能力,减少了单个线程的等待时间,但是其可能会将整体性能拉低。首先,原子操作的无锁代码要慢于无原子操作的代码,原子操作就相当于无锁数据结构中的锁。不仅如此,硬件必须通过同一个原子变量对线程间的数据进行同步。
无锁结构依赖于原子操作和内存序及相关保证,以确保多线程以正确的顺序访问数据结构。
不用锁实现push():
template<typename T>
class lock_free_stack
{
private:
struct node
{
T data;
node* next;
node(T const& data_) : // 1
data(data_)
{}
};
std::atomic head;
public:
void push(T const& data)
{
node* const new_node = new node(data); // 2
new_node->next = head.load(); // 3
while (!head.compare_exchange_weak(new_node->next, new_node));
/*当new_node->next与head相等时,head=new_node,
*返回true(可能直接由于平台原因失败,而非strong版本的反复尝试);
*不等时,new_node->next=head(完美修改了新节点指针域)
*返回false,再次进行尝试。
*/
}
};
为了更好的理解compare_exchange_weak()
函数及“比较/交换”,查阅了相关资料,Understand std::atomic::compare_exchange_weak() in C++11。
compare-and-swap (CAS) :In short, it loads the value from memory, compares it with an expected value, and if equal, store a predefined desired value to that memory location. The important thing is that all these are performed in an atomic way, in the sense that if the value has been changed in the meanwhile by another thread, CAS will fail.
the weak version will return false even if the value of the object is equal to expected, which won’t be synced with the value in memory in this case.
pop()
雏形:
template<typename T>
class lock_free_stack
{
public:
void pop(T& result)
{
node* old_head = head.load();
while (!head.compare_exchange_weak(old_head, old_head->next));
result = old_head->data;
}
};
问题:1、当头指针为空的时候,程序将解引用空指针,这是未定义行为;2、异常安全(最后复制栈的数据时,若抛出异常,栈数据已经丢失,可以使用shared_ptr<>
解决)。
带有节点泄露的无锁栈(无锁——等待):
template<typename T>
class lock_free_stack
{
private:
struct node
{
std::shared_ptr data; // 1 指针获取数据
node* next;
node(T const& data_) :
data(std::make_shared(data_))
// 2 让std::shared_ptr指向新分配出来的T
{}
};
std::atomic head;
public:
void push(T const& data)
{
node* const new_node = new node(data);
new_node->next = head.load();
while (!head.compare_exchange_weak(new_node->next, new_node));
}
std::shared_ptr pop()
{
node* old_head = head.load();
while (old_head && // 3 在解引用前检查old_head是否为空指针
!head.compare_exchange_weak(old_head, old_head->next));
return old_head ? old_head->data : std::shared_ptr(); // 4
}
};
##停止内存泄露:使用无锁数据结构管理内存
没有线程通过pop()访问节点时,就对节点进行回收:
template<typename T>
class lock_free_stack
{
private:
std::atomic<unsigned> threads_in_pop;
// 1 原子变量,记录有多少线程试图弹出栈中的元素
void try_reclaim(node* old_head);
//计数器减一,当这个函数被节点调用时,说明这个节点已经被删除
public:
std::shared_ptr pop()
{
++threads_in_pop; // 2 在做事之前,计数值加1
node* old_head = head.load();
while (old_head &&
!head.compare_exchange_weak(old_head, old_head->next));
//check空指针,阻止内存泄漏
std::shared_ptr res;
if (old_head)
{
res.swap(old_head->data); // 3 从节点中直接提取数据,而非拷贝指针
}
try_reclaim(old_head); // 4 回收删除的节点
return res;
}
};
采用引用计数的回收机制(实现try_reclaim()
):
template<typename T>
class lock_free_stack
{
private:
std::atomic to_be_deleted;
static void delete_nodes(node* nodes)
{
while (nodes)
{
node* next = nodes->next;
delete nodes;
nodes = next;
}
}
void try_reclaim(node* old_head)//节点调用即删除(于原链表)
{
if (threads_in_pop == 1) // 1 当前线程正在对pop进行访问
{
node* nodes_to_delete = to_be_deleted.exchange(nullptr);
// 2 声明“可删除”列表,该函数返回:The contained value before the call.
//下面进行检查,决定删除等待链表,还是还原to_be_delted
if (!--threads_in_pop) // 3 是否只有一个线程调用pop()?
{
delete_nodes(nodes_to_delete); // 4 迭代删除等待链表
}
else if (nodes_to_delete) // 5 存在
{
chain_pending_nodes(nodes_to_delete);
// 6 nodes还原to_be_delted=nodes_to_delete
}
delete old_head; // 7 安全删除
}
else
{
chain_pending_node(old_head); // 8 向等待列表中继续添加节点node
//此时to_be_deleted为old_head
--threads_in_pop;
}
}
void chain_pending_nodes(node* nodes)//nodes
{
node* last = nodes;
while (node* const next = last->next) // 9 让next指针指向链表的末尾
{
last = next;
}
chain_pending_nodes(nodes, last);
}
void chain_pending_nodes(node* first, node* last)
{
last->next = to_be_deleted; // 10
while (!to_be_deleted.compare_exchange_weak(
// 11 用循环来保证last->next的正确性,存储第一个节点
last->next, first));
}
void chain_pending_node(node* n)//node
{
chain_pending_nodes(n, n);
/*添加单个节点是一种特殊情况,
因为这需要将这个节点作为第一个节点,
同时也是最后一个节点进行添加。*/
}
};
上面程序配合译文解释读了好几遍,终于理清了一点头绪。要注意几个对threads_in_pop
进行检查的地方,以及help函数功能。由于该程序在删除等待列表时候,寻求了一个合适的静态指针即某一时刻仅有一个pop。书中提到只有低负荷满足,但是高负荷时候不存在静态,文中提到采用风险指针。
风险指针(hazard pointer):删除一个节点可能会让其他引用该节点的线程处于危险之中。当有线程去访问要被(其他线程)删除的对象时,会先设置对这个对象设置一个风险指针,而后通知其他线程,删除这个指针是一个危险的行为。一旦这个对象不再被需要,那么就可以清除风险指针了。
使用风险指针实现的pop()
:
std::shared_ptr pop()
{
std::atomic<void*>& hp = get_hazard_pointer_for_current_thread();
//可以返回风险指针的引用
node* old_head = head.load();
//确保正确设置
do
{
//循环保证node不会在读取旧head指针时,以及在设置风险指针的时被删除
node* temp;
do // 1 直到将风险指针设为head指针
{
temp = old_head;
hp.store(old_head);
old_head = head.load();
} while (old_head != temp);
} while (old_head &&
!head.compare_exchange_strong(old_head, old_head->next));
hp.store(nullptr); // 2 当声明完成,清除风险指针
std::shared_ptr res;
if (old_head)
{
res.swap(old_head->data);
// 3 在删除之前对风险指针引用的节点进行检查,是否被引用
if (outstanding_hazard_pointers_for(old_head))
{
reclaim_later(old_head);// 4 有,存放链表中
}
else
{
delete old_head;// 5 没有,直接删除
}
delete_nodes_with_no_hazards();// 6 检查链表,是否有风险指针引用
}
return res;
}
get_hazard_pointer_for_current_thread()
函数的简单实现:
#include
#include
#include
unsigned const max_hazard_pointers = 100;
struct hazard_pointer
{
std::atomic id;
std::atomic<void*> pointer;
};
//固定长度的“线程ID-指针”数组
hazard_pointer hazard_pointers[max_hazard_pointers];
class hp_owner
{
hazard_pointer* hp;
public:
hp_owner(hp_owner const&) = delete;
hp_owner operator=(hp_owner const&) = delete;
hp_owner() :
hp(nullptr)
{
//查询“所有者/指针”表,寻找没有所有者的记录
for (unsigned i = 0;iif (hazard_pointers[i].id.compare_exchange_strong(
// 6 尝试声明风险指针的所有权
old_id, std::this_thread::get_id()))
{
hp = &hazard_pointers[i];
break; // 7 交换成功,当前线程拥有,停止搜索
}
}
if (!hp) // 1 若没有找到,则很多线程在使用风险指针,报错
{
throw std::runtime_error("No hazard pointers available");
}
}
std::atomic<void*>& get_pointer()
{
return hp->pointer;
}
~hp_owner() // 2 析构,使得记录可以被复用
{
hp->pointer.store(nullptr); // 8
hp->id.store(std::thread::id()); // 9
}
};
std::atomic<void*>& get_hazard_pointer_for_current_thread() // 3
{
thread_local static hp_owner hazard;
// 4 每个线程都有自己的风险指针,thread_local:本线程
return hazard.get_pointer(); // 5
}
outstanding_hazard_pointer_for()
实现:
//在删除之前对风险指针引用的节点进行检查,是否被引用
bool outstanding_hazard_pointers_for(void* p)
{
for (unsigned i = 0;iif (hazard_pointers[i].pointer.load() == p)
{
return true;
}
}
return false;
}
回收函数的简单实现:
template<typename T>
void do_delete(void* p)
{
delete static_cast(p);//删除时只能对实际类型,故转换
}
struct data_to_reclaim
{
void* data;
std::function<void(void*)> deleter;
data_to_reclaim* next;
template<typename T>
data_to_reclaim(T* p) : // 1
data(p),
deleter(&do_delete),
next(nullptr)
{}
~data_to_reclaim()
{
deleter(data); // 2
}
};
std::atomic nodes_to_reclaim;
void add_to_reclaim_list(data_to_reclaim* node) // 3
{
node->next = nodes_to_reclaim.load();
while (!nodes_to_reclaim.compare_exchange_weak(node->next, node));
//更新nodes_to_reclaim 链表头
}
template<typename T>
void reclaim_later(T* data) // 4 风险指针是一个通用解决方案
{
add_to_reclaim_list(new data_to_reclaim(data)); // 5 添加到链表头部
}
//将已声明的链表节点进行回收
void delete_nodes_with_no_hazards()
{
data_to_reclaim* current = nodes_to_reclaim.exchange(nullptr);
// 6 保证只有一个线程回收这些节点
while (current)
{
data_to_reclaim* const next = current->next;
if (!outstanding_hazard_pointers_for(current->data))
// 7 检查节点是否被风险指针引用
{
delete current; // 8
}
else
{
add_to_reclaim_list(current); // 9 添加到链表
}
current = next;
}
}
对风险指针(较好)的回收策略:当有2max_hazard_pointers个节点在列表中时,就能保证至少有max_hazard_pointers可以被回收,在再次尝试回收任意节点前,至少会对pop()有max_hazard_pointers次调用。缺点(有增加内存使用的情况):就是得对回收链表上的节点进行计数,这就意味着要使用原子变量,并且还有很多线程争相对回收链表进行访问。如果还有多余的内存,可以增加内存的使用来实现更好的回收策略:每个线程中的都拥有其自己的回收链表,作为线程的本地变量。这样就不需要原子变量进行计数了。这样的话,就需要分配max_hazard_pointers x max_hazard_pointers个节点。所有节点被回收完毕前时,有线程退出,那么其本地链表可以像之前一样保存在全局中,并且添加到下一个线程的回收链表中,让下一个线程对这些节点进行回收。另一个缺点:是IBM的专利,使用要有合适的许可证。
引用计数是通过对每个节点上访问的线程数量进行统计,解决问题。
无锁栈——使用无锁 std::shared_ptr<>
的实现:
template<typename T>
class lock_free_stack
{
private:
struct node
{
std::shared_ptr data;
std::shared_ptr next;
node(T const& data_) :
data(std::make_shared(data_))
{}
};
std::shared_ptr head;
public:
void push(T const& data)
{
std::shared_ptr const new_node =
std::make_shared(data);
new_node->next = head.load();
while (!std::atomic_compare_exchange_weak(&head,
&new_node->next, new_node));
}
std::shared_ptr pop()
{
std::shared_ptr old_head = std::atomic_load(&head);
while (old_head && !std::atomic_compare_exchange_weak(&head,
&old_head, old_head->next));
return old_head ? old_head->data : std::shared_ptr();
}
};
某些情况下,shared_ptr并非无锁,这就需要手动管理引用计数。
一种方式是对每个节点使用两个引用计数:内部计数和外部计数。两个值的总和就是对这个节点的引用数。外部计数记录有多少指针指向节点,即在指针每次进行读取的时候,外部计数加一。当线程结束对节点的访问时,内部计数减一。指针在读取时,外部计数加一;在读取结束时,内部计数减一。当不需要“外部计数-指针”对时(该节点就不能被多线程所访问了),在外部计数减一和在被弃用的时候,内部计数将会增加。当内部计数等于0,那么就没有指针对该节点进行引用,就可以将该节点安全的删除。
使用分离引用计数的方式推送一个节点到无锁栈中:
template<typename T>
class lock_free_stack
{
private:
struct node;
//体积较小使4无锁,平台支持双字比较和交换可直接操作结构体
struct counted_node_ptr // 1 外部计数
{
int external_count;
node* ptr;//指向node
};
struct node
{
std::shared_ptr data;
std::atomic<int> internal_count; // 2 内部计数,原子
counted_node_ptr next; // 3 指向下一个外部计数节点
node(T const& data_) :
data(std::make_shared(data_)),
internal_count(0)
{}
};
std::atomic head; // 4
public:
~lock_free_stack()
{
while (pop());
}
void push(T const& data) // 5
{
counted_node_ptr new_node;
new_node.ptr = new node(data);
new_node.external_count = 1;
new_node.ptr->next = head.load();
while (!head.compare_exchange_weak(new_node.ptr->next, new_node));
}
};
使用分离引用计数从无锁栈中弹出一个节点:
template<typename T>
class lock_free_stack
{
private:
void increase_head_count(counted_node_ptr& old_counter)
{
counted_node_ptr new_counter;
do
{
new_counter = old_counter;
++new_counter.external_count;
} while (!head.compare_exchange_strong(old_counter, new_counter));
// 1 保证指针不会在同一时间内被其他线程修改
old_counter.external_count = new_counter.external_count;
}
public:
std::shared_ptr pop()
{
counted_node_ptr old_head = head.load();
for (;;)
{
increase_head_count(old_head);
node* const ptr = old_head.ptr; // 2 当计数增加,就能安全的解引用
if (!ptr)
{
return std::shared_ptr();
}
if (head.compare_exchange_strong(old_head, ptr->next)) // 3 删除节点
{
//拥有对应节点的所有权
std::shared_ptr res;
res.swap(ptr->data); // 4
int const count_increase = old_head.external_count - 2;
/* 5 相加的值要比外部引用计数少2。
*当节点已经从链表中删除,就要减少一次计数
*(这是基本的一次减去,相当于减在内部计数),
*并且这个线程无法再次访问指定节点,所以还要再减一
*/
if (ptr->internal_count.fetch_add(count_increase)
==-count_increase)
{//返回相加前的值,注意判断
delete ptr;
}
return res; // 7
}
else if (ptr->internal_count.fetch_sub(1) == 1)
{//返回相减前的值,如果当前线程是最后一个持有引用线程
delete ptr; // 8
}
}
}
};
目前,使用默认 std::memory_order_seq_cst
内存序来规定原子操作的执行顺序。在大多数系统中,这种操作方式都很耗时,且同步操作的开销要高于内存序。现在,就可以考虑对数据结构的逻辑进行修改,对数据结构的部分放宽内存序要求;就没有必要在栈上增加过度的开销了。
做push()
的线程,会先构造数据项和节点,再设置head。做pop()
的线程,会先加载head的值,再做在循环中对head做“比较/交换”操作,并增加引用计数,再读取对应的node节点,获取next的指向的值,现在就可以看到一组需求关系。next的值是普通的非原子对象,所以为了保证读取安全,这里必须确定存储(推送线程)和加载(弹出线程)的先行关系。
唯一的原子操作是push()
函数中的compare_exchange_weak()
,这里需要释放操作来获取两个线程间的先行关系,这里compare_exchange_weak()
必须是std::memory_order_release
或更严格的内存序。当compare_exchange_weak()
调用失败,什么都不会改变,并且可以持续循环下去,所以使用std::memory_order_relaxed
就足够了。
void push(T const& data)
{
counted_node_ptr new_node;
new_node.ptr = new node(data);
new_node.external_count = 1;
new_node.ptr->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(new_node.ptr->next, new_node,
std::memory_order_release, std::memory_order_relaxed));
//std::memory_order_release:释放
}
pop()
:为了确定先行关系,必须在访问next值之前使用std::memory_order_acquire
或更严格内存序的操作。因为,在increase_head_count()
中使用compare_exchange_strong()
就获取next指针指向的旧值,所以想要其获取成功就需要确定内存序。如同调用push()那样,当交换失败,循环会继续,所以在失败的时候使用松散的内存序:
void increase_head_count(counted_node_ptr& old_counter)
{
counted_node_ptr new_counter;
do
{
new_counter = old_counter;
++new_counter.external_count;
} while (!head.compare_exchange_strong(old_counter, new_counter,
std::memory_order_acquire, std::memory_order_relaxed));
//std::memory_order_acquire:获取
old_counter.external_count = new_counter.external_count;
}
释放-获取操作,先行关系确定。
基于引用计数和松散原子操作的无锁栈:
#include
#include
template<typename T>
class lock_free_stack
{
private:
struct node;
struct counted_node_ptr
{
int external_count;
node* ptr;
};
struct node {
std::shared_ptr data;
std::atomic<int> internal_count;
counted_node_ptr next;
node(T const& data_) :
data(std::make_shared(data_)),
internal_count(0)
{}
};
std::atomic head;
void increase_head_count(counted_node_ptr& old_counter)
{
counted_node_ptr new_counter;
do
{
new_counter = old_counter;
++new_counter.external_count;
} while (!head.compare_exchange_strong(old_counter, new_counter,
std::memory_order_acquire,//获取
std::memory_order_relaxed));
old_counter.external_count = new_counter.external_count;
}
public:
~lock_free_stack()
{
while (pop());
}
void push(T const& data)
{
counted_node_ptr new_node;
new_node.ptr = new node(data);
new_node.external_count = 1;
new_node.ptr->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(new_node.ptr->next, new_node,
std::memory_order_release,//释放
std::memory_order_relaxed));
}
std::shared_ptr pop()
{
counted_node_ptr old_head =head.load(std::memory_order_relaxed);
for (;;)
{
increase_head_count(old_head);
node* const ptr = old_head.ptr;
if (!ptr)
{
return std::shared_ptr();
}
if (head.compare_exchange_strong(old_head, ptr->next,
std::memory_order_relaxed))
{//为了避免数据竞争,需要保证swap()先行于delete操作。
std::shared_ptr res;
res.swap(ptr->data);
int const count_increase = old_head.external_count - 2;
if (ptr->internal_count.fetch_add(count_increase,
std::memory_order_release) == -count_increase)
{
delete ptr;
}
return res;
}
else if(ptr->internal_count.fetch_add
(-1,std::memory_order_relaxed) == 1)
{
ptr->internal_count.load(std::memory_order_acquire);
delete ptr;
}
}
}
};
这里要保证swap()先行于delete操作,其余问题已于前面阐述。
同步的需求不一样了。需要保证对一端的修改是正确的,且对另一端是可见的。
单生产者/单消费者(single-producer, single-consumer,SPSC)模型下的无锁队列:
template<typename T>
class lock_free_queue
{
private:
struct node
{
std::shared_ptr data;
node* next;
node() :
next(nullptr)
{}
};
std::atomic head;
std::atomic tail;
node* pop_head()
{
node* const old_head = head.load();
if (old_head == tail.load()) // 1 tail加载
{
return nullptr;
}
head.store(old_head->next);
return old_head;
}
public:
lock_free_queue() :
head(new node), tail(head.load())
{}
lock_free_queue(const lock_free_queue& other) = delete;
lock_free_queue& operator=(const lock_free_queue& other) = delete;
~lock_free_queue()
{
while (node* const old_head = head.load())
{
head.store(old_head->next);
delete old_head;
}
}
std::shared_ptr pop()
{
node* old_head = pop_head();
if (!old_head)
{
return std::shared_ptr();
}
std::shared_ptr const res(old_head->data); // 2 data加载
delete old_head;
return res;
}
void push(T new_value)
{
std::shared_ptr new_data(std::make_shared(new_value));
node* p = new node; // 3
node* const old_tail = tail.load(); // 4
old_tail->data.swap(new_data); // 5 data存储
old_tail->next = p; // 6
tail.store(p); // 7 tail存储
}
};
多线程下的push()
。两种方案:一是在两个真实节点中添加一个虚拟节点,二是让data指针原子化,并通过“比较/交换”操作对其进行设置(书中讲了两种方法,一是原子操作对于std::shared_ptr<>
是无锁的可以这样,否则替代方法:让pop()函数返回一个std::unique_ptr<>
(毕竟,这个指针指针只能引用指定对象),并且将数据作为一个普通指针存储在队列中的方案。这就需要队列支持存储 std::atomic
类型,对于compare_exchange_strong()
的调用就很有必要了)。下面是引用计数模式的尝试。
push()
的第一次修订(不正确的):
void push(T new_value)
{
std::unique_ptr new_data(new T(new_value));
counted_node_ptr new_next;
new_next.ptr = new node;
new_next.external_count = 1;
for (;;)
{
node* const old_tail = tail.load(); // 1 加载一个原子指针
T* old_data = nullptr;
if (old_tail->data.compare_exchange_strong(
old_data, new_data.get())) // 2 解引用,如果被回收,此处将未定义
{
old_tail->next = new_next;
tail.store(new_next.ptr); // 3 其他线程的更新
new_data.release();
break;
}
}
}
书中讲到了他学习到的一种优秀方法:就是给tail也添加计数器,就像给head做的那样,不过队列中的节点的next指针中都已经拥有了一个外部计数。在同一个节点上有两个外部计数,为了避免过早的删除节点,这就是对之前引用计数方案的修改。通过对node结构中外部计数器数量的统计,解决这个问题。当外部计数器销毁时,统计值减一(将对应的外部计数添加到内部)。当内部计数是0,且没有外部计数器时,对应节点就可以被安全删除了。
使用带有引用计数tail,实现的无锁队列中的push()
:
template<typename T>
class lock_free_queue
{
private:
struct node;
struct counted_node_ptr {
int external_count;
node* ptr;
};
std::atomic head;//带有计数器
std::atomic tail; // 1 原子类型
/*重要的是,为的就是避免条件竞争,
*将结构体作为一个单独的实体来更新。
*让结构体的大小保持在一个机器字内,对其的操作就如同原子操作一样,
*还可以在多个平台上使用。
*/
struct node_counter//计数器整体大小是32bit
{
unsigned internal_count : 30;
unsigned external_counters : 2; // 2 表示多少指针指向
};
struct node
{
std::atomic data;
std::atomic count; // 3 原子类型
counted_node_ptr next;
node()
{
node_counter new_count;
new_count.internal_count = 0;
new_count.external_counters = 2; // 4 node初始化
count.store(new_count);//存入
next.ptr = nullptr;
next.external_count = 0;
}
};
public:
void push(T new_value)
{
std::unique_ptr new_data(new T(new_value));
counted_node_ptr new_next;
new_next.ptr = new node;//虚拟节点
new_next.external_count = 1;
counted_node_ptr old_tail = tail.load();
for (;;)
{
increase_external_count(tail, old_tail); // 5 增加计数器的计数
T* old_data = nullptr;
if (old_tail.ptr->data.compare_exchange_strong( // 6
old_data, new_data.get()))
{
old_tail.ptr->next = new_next;
old_tail = tail.exchange(new_next);
free_external_counter(old_tail);
// 7 尾部旧值,计数器更新(external_counters等)
new_data.release();//new_data再无所有权
break;
}
old_tail.ptr->release_ref();
}
}
};
使用尾部引用计数,将节点从无锁队列中弹出:
template<typename T>
class lock_free_queue
{
private:
struct node
{
void release_ref();
};
public:
std::unique_ptr pop()
{
counted_node_ptr old_head = head.load(std::memory_order_relaxed); // 1 加载head
for (;;)
{
increase_external_count(head, old_head); // 2 增加计数
node* const ptr = old_head.ptr;
if (ptr == tail.load().ptr)
{
ptr->release_ref(); // 3 当head与tail节点相同的时候,就能对引用进行释放
return std::unique_ptr();
}
if (head.compare_exchange_strong(old_head, ptr->next)) // 4
{
T* const res = ptr->data.exchange(nullptr);
free_external_counter(old_head); // 5
return std::unique_ptr(res);
}
ptr->release_ref();
// 6 当外部计数或指针有所变化时,需要将引用释放后,再次进行循环
}
}
};
在无锁队列中释放一个节点引用:
template<typename T>
class lock_free_queue
{
private:
struct node
{
void release_ref()
{
node_counter old_counter =
count.load(std::memory_order_relaxed);
node_counter new_counter;
do
{
new_counter = old_counter;
--new_counter.internal_count; // 1
} while (!count.compare_exchange_strong( // 2保证更新成功
old_counter, new_counter,
std::memory_order_acquire, std::memory_order_relaxed));
/*降低internal_count时,在内外部计数都为0时,
就代表这是最后一次引用,之后就可以将这个节点删除。*/
if (!new_counter.internal_count &&
!new_counter.external_counters)
{
delete this; // 3
}
}
};
};
从无锁队列中获取一个节点的引用:
template<typename T>
class lock_free_queue
{
private:
/*静态成员函数,通过将外部计数器作为第一个参数传入函
*数,对其进行更新,而非只操作一个固定的计数器(传入了两个参数)。
*/
static void increase_external_count(
std::atomic& counter,
counted_node_ptr& old_counter)
{
counted_node_ptr new_counter;
do
{
new_counter = old_counter;
++new_counter.external_count;
} while (!counter.compare_exchange_strong(
old_counter, new_counter,
std::memory_order_acquire, std::memory_order_relaxed));
old_counter.external_count = new_counter.external_count;
//old_counter非原子类型,直接操作
}
};
无锁队列中释放节点外部计数器:
template<typename T>
class lock_free_queue
{
private:
static void free_external_counter(counted_node_ptr &old_node_ptr)
{
node* const ptr = old_node_ptr.ptr;
int const count_increase = old_node_ptr.external_count - 2;
//初始化为1,pop时候increase后为2
node_counter old_counter =
ptr->count.load(std::memory_order_relaxed);
node_counter new_counter;
do
{
new_counter = old_counter;
--new_counter.external_counters; // 1 外部计数减一
new_counter.internal_count += count_increase; // 2 内部计数更新
} while (!ptr->count.compare_exchange_strong( // 3
old_counter, new_counter,
std::memory_order_acquire, std::memory_order_relaxed));
if (!new_counter.internal_count &&
!new_counter.external_counters)
{
delete ptr; // 4
}
}
};
push()
的首次调用,是要在其他线程完成后,将阻塞去除后才能完成,所以这里的实现只是“半无锁”(no longer lock-free)结构(书中讲到该函数在交换失败后的循环)。处理的技巧出自于“无锁技巧包”:等待线程可以帮助push()
线程完成操作。
无锁队列中的线程间互助:为了恢复代码无锁的属性,就需要让等待线程,在push()线程没什么进展时,做一些事情,就是帮进展缓慢的线程完成其工作。
修改pop()
用来帮助push()
完成工作:
template<typename T>
class lock_free_queue
{
private:
struct node
{
std::atomic data;
std::atomic count;
std::atomic next; // 1 原子
};
public:
std::unique_ptr pop()
{
counted_node_ptr old_head = head.load(std::memory_order_relaxed);
for (;;)
{
increase_external_count(head, old_head);
node* const ptr = old_head.ptr;
if (ptr == tail.load().ptr)
{
return std::unique_ptr();
}
counted_node_ptr next = ptr->next.load();
// 2 显示调用(这里是在提示内存序memory_order_seq_cst的使用),隐式转换
if (head.compare_exchange_strong(old_head, next))
{
T* const res = ptr->data.exchange(nullptr);
free_external_counter(old_head);
return std::unique_ptr(res);
}
ptr->release_ref();
}
}
};
无锁队列中简单的帮助性push()
的实现:
template<typename T>
class lock_free_queue
{
private:
// 1 tail指针更新:tail=new_tail
void set_new_tail(counted_node_ptr &old_tail,
counted_node_ptr const &new_tail)
{
node* const current_tail_ptr = old_tail.ptr;
//更新成功,即刻退出循环
while (!tail.compare_exchange_weak(old_tail, new_tail) &&
old_tail.ptr == current_tail_ptr);// 2 更新失败,保证ptr指向相同
if (old_tail.ptr == current_tail_ptr) // 3 新旧相同,退出循环
free_external_counter(old_tail); // 4 释放旧外部计数器
else // 不相同,旧外部计数器可能已经被其他线程释放
current_tail_ptr->release_ref();
// 5 只需要对该线程持有的单次引用进行释放即可
}
public:
void push(T new_value)
{
std::unique_ptr new_data(new T(new_value));
counted_node_ptr new_next;
new_next.ptr = new node;
new_next.external_count = 1;
counted_node_ptr old_tail = tail.load();
for (;;)
{
increase_external_count(tail, old_tail);
T* old_data = nullptr;
if (old_tail.ptr->data.compare_exchange_strong(
old_data, new_data.get()))// 6 对data进行设置
{
counted_node_ptr old_next = { 0 };
if (!old_tail.ptr->next.compare_exchange_strong(
old_next, new_next))// 7 next指针更新
{//失败:已有其他线程进行了修改
//old_next=old_tail.ptr->next指向新的尾节点
delete new_next.ptr; // 8 删除新分配的虚拟节点
new_next = old_next; // 9 更新后对tail设置
}
set_new_tail(old_tail, new_next);
new_data.release();
break;
}
else // 10 可以帮助成功的线程完成更新
{
counted_node_ptr old_next = { 0 };
// 11 尝试更新next指针,让其指向该线程分配出来的新节点
if (old_tail.ptr->next.compare_exchange_strong(
old_next, new_next))
{
old_next = new_next;
// 12 当指针更新成功,就可以将这个新节点作为新的tail节点(14)
new_next.ptr = new node;
// 13 需要分配另一个新节点,
//用来管理队列中新推送的数据项(其他线程会删除旧的ptr)。
}
set_new_tail(old_tail, old_next); // 14 设置tail节点
}
}
}
};
测试以及衡量分配器效率最好的办法,就是对使用前和使用后进行比较。为优化内存分配,包括每个线程有自己的分配器,以及使用回收列表对节点进行回收,而非将这些节点返回给分配器。
std::memory_order_seq_cst
的原型通常,当你看整套代码对数据结构的操作后,才能决定是否要放宽该操作的内存序选择。所以,尝试放宽选择,可能会让你轻松一些。在测试后的时候,工作的代码可能会很复杂(不过,不能完全保证内存序正确)。除非你有一个算法检查器,可以系统的测试,线程能看到的所有可能性组合,这样就能保证指定内存序的正确性(这样的测试的确存在),仅是执行实现代码是远远不够的。
主要的想法都是使用一种方式去跟踪指定对象上的线程访问数量,当没有现成对对象进行引用的时候,将对象删除。当然,在无锁数据结构中,还有很多方式可以用来回收内存。例如,理想情况下使用一个垃圾收集器。比起算法来说,其实现更容易一些。只需要让回收器知道,当节点没被引用的时候,回收节点,就可以了。其他替代方案就是循环使用节点,只在数据结构被销毁的时候才将节点完全删除。因为节点能被复用,那么就不会有非法的内存,所以这就能避免未定义行为的发生。这种方式的缺点:产生“ABA问题”。
该问题在书中有详细的描述,主要是三个线程对原子变量x的持,先td1读取为A,而后被td2改为B,最后td3改回A。此处td1继续运行“比较/交换”成功,但是已经是一个错误的A。
书中建议的解决方法:让变量x中包含一个ABA计数器。“比较/交换”会对加入计数器的x进行操作。每次的值都不一样,计数随之增长,所以在x还是原值的前提下,即使有线程对x进行修改,“比较/交换”还是会失败。“ABA问题”在使用释放链表和循环使用节点的算法中很是普遍,而将节点返回给分配器,则不会引起这个问题。
此处,我们已经体会到了最后程序中对push的修改带来的福祉。通过对算法的修改,当之前的线程还没有完成操作前,让等待线程执行未完成的步骤,就能让忙等待的线程不再被阻塞。在队列例中,需要将一个数据成员转换为一个原子变量,而不是使用非原子变量和使用“比较/交换”操作来做这件事;要是在更加复杂的数据结构中,这将需要更加多的变化来满足需求。