C++ Concurrency in Action、2eで、著者はロックフリーのスレッドセーフリンクリストの実装について説明しています。現在、彼はpop()メソッドと、一種の「ガベージコレクター」のようなメソッドでノードを安全に削除して、同じインスタンスで他のスレッドがpopを呼び出さないようにする方法を説明しています。 popのコードの一部を次に示します。
#include <atomic>
#include <memory>
template<typename T>
class lock_free_stack
{
private:
std::atomic<unsigned> threads_in_pop;
void try_reclaim(node* old_head);
public:
std::shared_ptr<T> pop()
{
++threads_in_pop;
node* old_head=head.load();
while(old_head &&
!head.compare_exchange_weak(old_head,old_head->next));
std::shared_ptr<T> res;
if(old_head)
{
res.swap(old_head->data);
}
try_reclaim(old_head);
return res;
}
};
重要なことは、pop()が呼び出されるたびに、カウンターがアトミックにインクリメントされることです。次に、try_reclaim関数は、このカウンターをデクリメントします。次に、try_reclaimの実装を示します。
void try_reclaim(node* old_head)
{
if(threads_in_pop==1) //#1
{
node* nodes_to_delete=to_be_deleted.exchange(nullptr);
if(!--threads_in_pop) //#2
{
delete_nodes(nodes_to_delete);
}
else if(nodes_to_delete)
{
chain_pending_nodes(nodes_to_delete);//#3
}
delete old_head; //#4 THIS IS THE PART I AM CONFUSED ABOUT
}
else
{
chain_pending_node(old_head);
--threads_in_pop;
}
}
ここで呼び出される他の関数の実装は関係がないので(削除するノードのチェーンにノードを追加するだけです)、それらを省略しました。コードで混乱している部分は#4(マークされている)です。ここで、作成者は渡されたold_headでdeleteを呼び出します。しかし、old_headを削除する前に、この時点でthreads_in_popがまだゼロであるかどうかを確認しないのはなぜですか。彼は2行目と1行目をダブルチェックして、現在pop()に別のスレッドがないことを確認します。それで、old_headの削除に進む前に再度チェックしないのはなぜですか。別のスレッドが#3の直後にpop()を呼び出してカウンターをインクリメントし、最初のスレッドが#4に達するまでに、threads_in_popがゼロにならない可能性はありませんか?
つまり、コードが#4に達するまでに、threads_in_popがたとえば2になる可能性はありませんか?その場合、彼はどのようにold_headを安全に削除できますか?誰かが説明してもらえますか?
次の(簡略化された)シーケンスがあり、すべてのアトミック操作は順次一貫しています。++threads_in_pop -> head.cmpxchg -> threads_in_pop.load() -> delete old_head
したがって、最初に現在のヘッドを削除し、後で_threads_in_pop
_の数を確認します。スタックで動作しているT1とT2の2つのスレッドがあるとします。 T1が_try_reclaim
_でthreads_in_pop.load()
(#1)を実行し、1が表示される場合、これはT2がまだインクリメント(_++threads_in_pop
_)を実行していないことを意味します。つまり、T1が唯一のスレッドですその時点で_old_head
_への参照を持つことができます。ただし、T1はすでに_old_head
_をリストから削除しているため、popに入ったスレッドにはすでに更新されたヘッドが表示されているため、他のスレッドは_old_thread
_への参照を取得できません。したがって、_old_head
_を削除しても安全です。
チェック#2は、_to_be_released
_リストに追加されたばかりのノードを解放しないようにするために必要ですafterこのスレッドはポップを実行しましたが、他のスレッドはまだ参照を保持している可能性があります。次の状況を考慮してください。
nodes_to_delete=to_be_deleted.exchange(nullptr);
head
を読み取りますhead
を読み取り、T2と同じ値を確認しますold_head
_をリストに追加しますto_be_deleted
_リストの一部であるノードへの参照がまだありますnodes_to_delete=to_be_deleted.exchange(nullptr);
を実行するようになりましたT1には、T3から引き続きアクセスできるノードへの参照を含む_nodes_to_delete
_のリストが含まれています。そのため、T1がそのノードを解放しないようにするには、チェック#2が必要です。