web-dev-qa-db-ja.com

C ++でのロックフリーのスタックポップ実装

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を安全に削除できますか?誰かが説明してもらえますか?

9
user22333

次の(簡略化された)シーケンスがあり、すべてのアトミック操作は順次一貫しています。
++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このスレッドはポップを実行しましたが、他のスレッドはまだ参照を保持している可能性があります。次の状況を考慮してください。

  • T1がポップを実行し、実行しようとしていますnodes_to_delete=to_be_deleted.exchange(nullptr);
  • T2がポップを開始し、headを読み取ります
  • T3はpopを開始してheadを読み取り、T2と同じ値を確認します
  • T2はポップを終了し、_old_head_をリストに追加します
  • 注:T3には、現在_to_be_deleted_リストの一部であるノードへの参照がまだあります
  • T1がnodes_to_delete=to_be_deleted.exchange(nullptr);を実行するようになりました

T1には、T3から引き続きアクセスできるノードへの参照を含む_nodes_to_delete_のリストが含まれています。そのため、T1がそのノードを解放しないようにするには、チェック#2が必要です。

1
mpoeter