34.6 shared ownership, reclamation és konkurens review

A konkurens C++ egyik leggyakoribb félreértése, hogy a std::shared_ptr „thread-safe okos pointer”. Ez a mondat két különböző dolgot mos össze: az ownership könyvelésének szinkronizációját és a mutatott objektum állapotának szinkronizációját. Ebben az alfejezetben egy connection registry példáján választjuk szét a kettőt, és megadjuk mindkettőre a szerződést. Az előfeltétel a semaphore, latch, barrier és szinkron stream részben rögzített szinkronizációs eszköztár, a shared_ptr, control block és weak_ptr részben bevezetett tulajdonlási modell, valamint a Konkurens élettartam és nehéz hibák részben tárgyalt happens-before és adatverseny definíció. Ezeket itt nem írjuk újra, csak felidézzük a szükséges mértékben.

A fejezet állításai a C++20 memóriamodell ismert követelményein alapulnak; a konkrét szabványparagrafusok és a C++26-os javaslatok státuszának ellenőrzése a Feature-test, standard library és build capability, illetve a C++26 és későbbi irányok ellenőrzött státusszal részek eszköztárával, a tanuló laborjában történik. A fejezet nem tartalmaz mért eredményt.

34.6.1 Két külön szerződés: ownership és pointee

Az ownership (tulajdonlás) szerződése megmondja, ki felel a mögöttes objektum megszüntetéséért, és mikor. A shared_ptr ezt osztott tulajdonlássá teszi: a control block referenciaszámlálója dönti el, mikor fut le a deleter. A pointee synchronization szerződése ezzel szemben azt mondja meg, hogy a mutatott objektum belső állapotához hogyan férünk hozzá párhuzamosan: melyik művelet olvas, melyik ír, milyen zár vagy atomi művelet védi, és mi a láthatósági viszony a szálak között.

A két szerződés ortogonális. A shared_ptr<P> azt garantálja, hogy a *p objektum élettartama alatt nem szűnik meg, amíg legalább egy shared_ptr példány rá mutat. Azt viszont nem garantálja, hogy p->mezo olvasása és írása két szálon nem versenyez. Ha a Connection objektumnak van egy std::uint64_t bytes_ tagja, és az egyik szál add_bytes-ot hív, a másik snapshot-ot, akkor a shared_ptr semmit nem tesz hozzá a helyességhez: a bytes_-re adatverseny van, ami nem definiált viselkedés, függetlenül attól, hogy a tulajdonlás rendben van.

A szerződés megfogalmazásához három feltételt rögzítünk. Egyrészt minden mutálható pointee-állapothoz külön védelmet rendelünk (mutex, atomikus változó vagy dokumentáltan egyszálú használat). Másrészt a shared_ptr példányokra vonatkozó műveletek köre pontosan az, amit a control block kezel, semmi több. Harmadrészt a két védelmi réteg sorrendje (lock ordering) rögzített, különben a két szerződés együtt deadlockot termel.

34.6.2 A control block szinkronizációja

A C++20 követelménye szerint a shared_ptr tagfüggvényei — beleértve a másoló konstruktort és a másoló értékadást — különböző shared_ptr példányokon külön szálakról is hívhatók további szinkronizáció nélkül, még akkor is, ha ezek a példányok ugyanannak az objektumnak a tulajdonjogát osztják. Ennek mechanizmusa a control blockban tartott referenciaszámláló atomikus növelése és csökkentése; az utolsóként csökkentő szál futtatja a deletet, és ez a szál a számláló csökkentésével szinkronizál minden korábbi tulajdonossal.

Két fontos korlátot kell ehhez hozzátenni. Az egyik, hogy a „thread-safe” itt kizárólag a könyvelésre vonatkozik: arra, hogy a számláló nem veszik el, és a destruktor pontosan egyszer fut le. A másik, hogy ez nem jelent lock-free implementációt. A gyakorlatban a legtöbb standard library implementáció atomikus műveletekkel kezeli a számlálót, de a shared_ptr mérete és a control block allokációja miatt a másolás költsége nem nulla; teljesítményérzékeny útvonalon ezt a Profilozás, benchmark és cost model rész módszertanával kell megmérni, nem feltételezni.

A gyakorlati következmény a registry esetében: a std::unordered_map<std::string, std::shared_ptr<Connection>> tartalmához való hozzáférést saját mutex védi, mert a map belső szerkezete nem thread-safe. A benne tárolt shared_ptr érték kimásolása viszont a zár alatt biztonságosan megtehető, és a kimásolt példány a zár elengedése után is érvényes tulajdonjogot ad.

34.6.3 Ugyanaz a shared_ptr példány és az atomic shared_ptr

A garantált biztonság kizárólag különböző shared_ptr példányokra vonatkozik. Ha két szál ugyanazon shared_ptr objektumon hív reset-et, értékadást vagy destruktort, az adatverseny a C memóriamodell értelmében, tehát nem definált viselkedés — nem „csak” logikai hiba, és nem javítja semmilyen retry. Ilyenkor a példányt magát kell szinkronizálni, vagy atomikus wrapperre kell váltani.

A C++20 bevezette a std::atomic<std::shared_ptr<T>> specializációt, amely egy shared_ptr objektumot tesz atomikusan olvashatóvá és írhatóvá. Mellette a shared_ptr-re specializált std::atomic_load, std::atomic_store szabad függvények C++11 óta léteznek, de a C++20ban deprecálttá váltak; új kódban a specializációt használjuk. Fontos korlát: az atomic<shared_ptr> jellemzően nem lock-free, belső zárat használ, tehát nem tekinthető a mutex kiváltásának nagy forgalmú esetben. Reális használati mintája a ritkán írt, gyakran olvasott konfiguráció publikálása:

// config_publish.cpp — ISO C++20
#include <atomic>
#include <memory>
#include <string>

struct Config {
    std::string endpoint;
    int timeout_ms;
};

std::atomic<std::shared_ptr<const Config>> g_config{nullptr};

void publish(std::shared_ptr<const Config> c) {
    g_config.store(std::move(c));          // release szemantika
}

std::shared_ptr<const Config> current() {
    return g_config.load();                // acquire szemantika
}

A store/load alapértelmezett memory_order_seq_cst, ami erősebb a szükségesnél; a release/acquire pár elegendő, és ezt a C++ atomics, atomic_ref és publication rész indokolja. A const Config választás szándékos: a publikált objektumot a kiadás után nem módosítjuk, így a pointee synchronization kérdése eltűnik. Ha módosítani kellene, az újabb publikálás új objektummal történik, nem a régi írásával.

34.6.4 Kidolgozott példa: connection registry

A példa három zárolási szintet mutat be: a registry mapjének zárját, a Connection belső állapotának zárját és a worker queue zárját. A cél, hogy a shared_ptr ownership műveletei soha ne fussanak felhasználói destruktor alatt, és hogy a taszk futtatása soha ne történjen queue-zár alatt.

// registry.cpp — ISO C++20, x86-64 System V, GCC 13 vagy Clang 17
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>

class Connection {
public:
    explicit Connection(std::string id) : id_(std::move(id)) {}

    const std::string& id() const noexcept { return id_; }

    // pointee state: saját szerződés, saját zár
    void add_bytes(std::uint64_t n) {
        std::lock_guard lk(state_mu_);
        bytes_ += n;
        ++ops_;
    }

    struct Snapshot {
        std::uint64_t bytes;
        std::uint64_t ops;
    };

    Snapshot snapshot() const {
        std::lock_guard lk(state_mu_);
        return Snapshot{bytes_, ops_};   // konzisztens másolat
    }

private:
    const std::string id_;
    mutable std::mutex state_mu_;
    std::uint64_t bytes_ = 0;
    std::uint64_t ops_ = 0;
};

class Registry {
public:
    // Szerződés: c nem üres; a kulcs c->id(). Azonos id esetén csere.
    void add(std::shared_ptr<Connection> c) {
        if (!c) throw std::invalid_argument("Registry::add: null connection");
        std::shared_ptr<Connection> victim;      // a zárblokkon KÍVÜL
        {
            std::lock_guard lk(mu_);
            // Ha a try_emplace kivételt dob (allokáció), c a hívó oldali
            // paraméterben marad, és a zár nélkül szabadul fel.
            auto [it, inserted] = map_.try_emplace(c->id());
            victim = std::exchange(it->second, std::move(c));   // noexcept
        }
        // A régi owner (ha volt) itt, a zár feloldása után szabadul fel:
        // a destruktora vagy deletere visszahívhat a regiszterbe.
    }

    std::shared_ptr<Connection> find(const std::string& id) const {
        std::lock_guard lk(mu_);
        auto it = map_.find(id);
        if (it == map_.end()) return nullptr;
        return it->second;               // refcount növelés a zár alatt
    }

    void remove(const std::string& id) {
        std::shared_ptr<Connection> victim;
        {
            std::lock_guard lk(mu_);
            auto it = map_.find(id);
            if (it == map_.end()) return;
            victim = std::move(it->second);
            map_.erase(it);
        }
        // victim itt szabadul fel: a registry zárja nélkül
    }

private:
    mutable std::mutex mu_;
    std::unordered_map<std::string, std::shared_ptr<Connection>> map_;
};

class Worker {
public:
    using Task = std::function<void()>;

    void start() { th_ = std::thread([this] { run(); }); }

    void submit(Task t) {
        {
            std::lock_guard lk(mu_);
            if (stop_) return;           // leállítás után elutasítunk
            q_.push_back(std::move(t));
        }
        cv_.notify_one();
    }

    void stop() {
        {
            std::lock_guard lk(mu_);
            stop_ = true;
        }
        cv_.notify_all();
        if (th_.joinable()) th_.join();

        std::deque<Task> leftover;
        {
            std::lock_guard lk(mu_);
            leftover.swap(q_);
        }
        leftover.clear();                // shared_ptr-ek zár nélkül szabadulnak
    }

    // A destruktor a hibaúton is szabályozottan leállít: a stop() idempotens.
    // Korlát: a Worker-t a tulajdonos szála destruálja, nem a saját munkaszála
    // (self-join esetén a join resource_deadlock_would_occur hibát dobna).
    ~Worker() { stop(); }

private:
    void run() {
        for (;;) {
            Task t;
            {
                std::unique_lock lk(mu_);
                cv_.wait(lk, [this] { return stop_ || !q_.empty(); });
                if (stop_) return;       // drop politika
                t = std::move(q_.front());
                q_.pop_front();
            }
            t();                         // taszk futtatása zár nélkül
        }
    }

    std::mutex mu_;
    std::condition_variable cv_;
    std::deque<Task> q_;
    bool stop_ = false;
    std::thread th_;
};

int main() {
    Registry reg;
    Worker w;
    w.start();

    auto c = std::make_shared<Connection>("db-1");
    reg.add(c);
    w.submit([c] { c->add_bytes(1024); });

    if (auto p = reg.find("db-1")) {
        (void)p->snapshot();
    }

    reg.remove("db-1");
    w.stop();
}

Az add és a remove ugyanazt az elvet követi: a map a zár alatt változik, de egy Connection utolsó tulajdonosának elengedése, vagyis a destruktor vagy egy egyéni deleter futása, mindig a zár feloldása után történik. Ha az add a régi elemet a zár alatt cserélné le (például insert_or_assign-nal), és a map volt a régi Connection utolsó tulajdonosa, a deleter a mu_ zár alatt futna. Egy olyan deleter, amely visszahív a regiszterbe (find), ugyanazt a nem rekurzív mutexet próbálná megszerezni: önmagával kerülne holtpontba, második szál nélkül. Az add ezért a régi ownert a zárblokkon kívül deklarált victim változóba mozgatja. A beszúrást try_emplace végzi: ha ez allokációs kivételt dob, az új owner még a hívó paraméterében van, nem a félkész map-bejegyzésben. A szerződés szerint a c nem lehet üres; erre az add std::invalid_argument-et dob.

Buildjegyzőkönyv (lab-gcc profil). A program figyelmeztetés nélkül fordult és 0 kóddal futott, TSan (setarch -R) alatt is. Egy azonos id-t lecserélő próba, amelynek deletere find-ot hív, a régi objektumot egyszer szabadította fel (freed=1), az eltávolítás után a másodikat (freed=2), ASan/UBSan alatt is. Az insert_or_assign-os eredeti változat ugyanebben a próbában holtpontra jutott (5 másodperces timeout, 124-es kód). Az üres pointert az add Registry::add: null connection üzenettel utasította el.

A buildparancs a tanuló laborjában futtatandó; a fejezet nem állítja, hogy a program lefutott vagy hogy a TSan tiszta eredményt adott:

g++ -std=c++20 -O1 -g -fsanitize=thread -pthread registry.cpp -o registry_tsan
./registry_tsan

A példa három tanulsága. Először: a find a shared_ptr másolatát a registry zárja alatt készíti el, így a visszaadott példány tulajdonjoga akkor is érvényes, ha közben egy másik szál remove-ot hajt végre. Másodszor: a remove a victim-et a zár elengedése után dobja el, így a Connection destruktora nem a registry zárja alatt fut — ez a lock ordering védelme, mert a destruktor elvileg más zárat is foghatna. Harmadszor: a Worker::run a taszkot a queue zárján kívül hívja meg, különben a taszk submit-hívása önmagával szemben deadlockolna.

34.6.5 Reclamation, ABA és a C++26 kitekintés

A shared_ptr referenciaszámlálója egy reclamation stratégia: az objektum akkor szűnik meg, amikor az utolsó tulajdonos elengedi. Ez kényelmes, de két ismert gyenge pontja van. Az egyik a ciklikus hivatkozás, amelyet a weak_ptr tör meg — erről a shared_ptr, control block és weak_ptr rész szólt. A másik az, hogy a reclamation nem védi meg a nyers pointert vagy referenciát, amelyet a shared_ptr mellől adunk át: ha egy másik szál közben elengedi az utolsó tulajdonost, a nyers hozzáférés danglinggá válik.

Az ABA jelenség akkor lép fel, ha egy atomikusan tárolt címre CAS-t hajtunk végre, de a cím időközben felszabadult és ugyanarra a címre új allokáció került. A címérték megegyezik, a jelentés nem. shared_ptr esetén ez akkor kerülhető el, ha a tulajdonjogot is atomikusan visszük magunkkal — például weak_ptr::lock()-kal vagy atomic<shared_ptr>-rel —, nem pedig nyers pointerrel. A lock-free struktúrák ehhez epoch- vagy hazard pointer alapú reclamationt használnak; ezek szabványosítása a C++26 felé irányuló javaslatok témája, amelynek elfogadási és implementálási státuszát itt nem állítjuk, hanem a C++26 és későbbi irányok ellenőrzött státusszal részre utalunk.

34.6.6 TSan, stressz és invariáns snapshot

A ThreadSanitizer (-fsanitize=thread) a happens-before gráfot építi fel a memóriahozzáférésekből és a szinkronizációs eseményekből, és jelzi a versenyző párokat. Nem keverhető egy binárison belül AddressSanitizerrel, és a -O1 -g kombináció ajánlott hozzá; a futásidejű költség jelentős. A TSan megtalálja az adatversenyt, de nem találja meg a logikai race-t (például azt, hogy a stop_ beállítása és a taszk futtatása között elveszik egy értesítés), és nem bizonyítja a deadlock-mentességet sem — legfeljebb a lock-set analízis jelezhet ciklust.

A stresszpróba sok szálat és rövid kritikus szakaszokat használ; jó indikátor, de nem bizonyíték: a hiányzó hiba nem bizonyítja a helyességet. Ami bizonyítható, az az invariáns. A példában a snapshot() egyetlen zár alatt adja vissza a bytes_ és ops_ értékét. Ha két külön gettert írnánk, a két érték két különböző időállapotból származna, és az ops_ == 0 && bytes_ > 0 kombináció hamisan jelenhetne meg. A konzisztens pillanatkép nem teljesítményoptimalizálás, hanem szerződés: a fogyasztó egyetlen állapotra következtethet.

34.6.7 Leállítás: shutdown deadlock és resource lifetime

A leggyakoribb shutdown deadlock nem a shared_ptr-ben, hanem a leállítás sorrendjében keletkezik. Ha a stop() a queue zárját tartva vár a join-ra, a worker nem tud belépni a run zárblokkjába, és a két szál egymásra vár. Ha a worker a queue zárja alatt futtatja a taszkot, a taszk submit-ja ugyanarra a zárra vár — önmagával szemben. Ha a stop_ írása nem a queue zárja alatt történik, a wait predikátuma adatversenyes, és az értesítés elveszhet.

A resource lifetime ugyanilyen fontos. A queue-ban maradó taszkok shared_ptr kópiái meghosszabbítják a Connection élettartamát a registry elpusztítása után is. A helyes sorrend tehát: stop flag beállítása zár alatt, notify_all, join, majd a maradék taszkok elengedése zár nélkül — ahogy a példa Worker::stop-ja teszi. A fogalmak pontosan: egy std::thread objektum joinable, amíg nem hívtunk rá join-t vagy detach-et — függetlenül attól, hogy a szál már befejezte-e a futását. A join futó szálra is hívható: addig blokkol, amíg a szál befejeződik. Egy joinable std::thread destruktora std::terminate-et hív. Ezért a ~Worker is meghívja a stop()-ot: ha a tulajdonos a start() után, de a stop() előtt kivétel miatt hagyja el a hatókört, a worker így is leáll és összeolvad, mielőtt a q_, a cv_ és a mu_ megszűnne (a th_ az utolsó tag, és a destruktor törzse a tagok előtt fut). A detach nem megoldás, mert a munkaszál a this-t kölcsönzi, és túlélné az objektumot. A stop idempotens, tehát a normál úton hívott explicit stop() után a destruktor ismételt hívása ártalmatlan.

Buildjegyzőkönyv (lab-gcc profil). Negatív próba: a main a w.start() után, a w.stop() előtt kivételt dob, amelyet egy külső try/catch kap el (a std::set_terminate a 42-es kóddal jelezné a terminate-et). A javítás előtti, üres destruktorú változat terminate-tel, 42-es kóddal állt le; a javított változat elkapva: ... üzenettel, 1-es kóddal lépett ki, TSan és ASan/UBSan alatt is jelzés nélkül.

34.6.8 Tipikus hibák

Az első hiba, hogy a shared_ptr-t automatikus pointee-védelemnek tekintjük. A javítás: minden mutálható taghoz külön védelmet rendelünk, és ezt a fejlécben dokumentáljuk. A második, hogy ugyanazon shared_ptr példányt két szálon reset-eljük; itt a példányt kell védeni vagy atomic<shared_ptr>-re váltani. A harmadik, hogy a find nyers referenciát ad vissza a map elemére a zár elengedése után; helyette shared_ptr kópiát kell visszaadni. A negyedik, hogy a destruktor zár alatt fut, és lock ordering ciklust okoz; a javítás a victim kiemelése a záron kívülre. Az ötödik, hogy a wait predikátum nélküli, és hamis ébresztésre is továbblép; a wait(lk, pred) alak kötelező. A hatodik, hogy a stressz- vagy TSan-futás hiányát helyességi bizonyítéknak tekintjük.

34.6.9 Feladatok

1. feladat (magyarázó). Fogalmazd meg pontosan, miért nem adatverseny, ha két szál két különböző shared_ptr<Connection> példányon hajt végre másolást ugyanarra a control blockra, és miért adatverseny, ha ugyanazon a példányon hajt végre reset-et. Nevezd meg a happens-before éleket.

2. feladat (kódolási). Egészítsd ki a Registry-t clear_all() művelettel, amely az összes connectiont eltávolítja, és garantálja, hogy a Connection destruktorok nem a registry zárja alatt futnak.

3. feladat (diagnosztikai). A Worker::run alábbi változata deadlockot okozhat. Azonosítsd a ciklust, add meg a javítást, és mondd meg, melyik TSan-eredmény nem jelezné a hibát.

void run() {
    for (;;) {
        std::unique_lock lk(mu_);
        cv_.wait(lk, [this] { return stop_ || !q_.empty(); });
        if (stop_) return;
        auto t = std::move(q_.front());
        q_.pop_front();
        t();                    // a zár még mindig tartva
    }
}

4. feladat (kódolási, nehezebb). A Worker::stop jelenleg drop politikát követ. Írj olyan stop(bool drain) változatot, amely drain == true esetén a maradék taszkokat lefuttatja, de biztosítja, hogy a leállítás alatt érkező új submit elutasításra kerüljön, és a ciklus véges maradjon.

34.6.10 Megoldások

1. megoldás. Két különböző shared_ptr példány esetén a másolás a control block referenciaszámlálóján atomikus növelést végez. Az atomikus read-modify-write művelet a szekvenciális konzisztencia miatt teljes sorrendet ad a két szál műveletei között, így a számláló értéke nem vész el, és az utolsó csökkentő szál pontosan egyszer futtatja a deletet. A két szál között a synchronizes-with él az atomikus változón keresztül jön létre, tehát a két példány objektumára vonatkozó műveletek nem versenyeznek. Ugyanazon shared_ptr példányon végzett reset ezzel szemben a példány belső pointerére és a kontrollblokk-hivatkozására is ír, és ezt a szabvány nem teszi atomikussá; két szál egyidejű írása a C memóriamodell szerint adatverseny, tehát nem definiált viselkedés. Az ellenőrzési feltétel: a válasz nevezze meg a referenciaszámláló atomikus műveletét, a synchronizes-with élt és az adatverseny definícióját.

2. megoldás. A kulcs, hogy a zárolt szakasz csak a map módosítását és a shared_ptr-ek kiemelését végezze; a felszabadítás a zár elengedése után történjen.

void clear_all() {
    std::unordered_map<std::string, std::shared_ptr<Connection>> doomed;
    {
        std::lock_guard lk(mu_);
        doomed.swap(map_);
    }
    // doomed itt szabadul fel: a registry zárja nélkül
}

A swap után a map_ üres, a doomed pedig a régi tartalmat birtokolja. A doomed destruktora a mu_ elengedése után fut, így a Connection destruktorok nem a registry zárja alatt futnak. Az ellenőrzési feltétel: a clear_all után a find minden kulcsra nullptr-t adjon, és a destruktorok hívási sorrendje ne tartalmazzon registry-zárat (TSan + naplózott destruktor mellett ellenőrizhető).

3. megoldás. A ciklus ciklusa: a worker a mu_ zár alatt futtatja a taszkot; ha a taszk submit-ot hív, az ugyanarra a mu_-re vár, amelyet a worker már tart, és soha nem enged el — önmagával szembeni deadlock. Ugyanez a stop oldaláról is jelentkezhet: ha a taszk stop-ot hív, a stop a mu_-re vár, miközben a join a worker szálra vár, amely a taszkban van. A javítás a taszk futtatásának kiemelése a zár alól: a kritikus szakasz csak a queue-ból való kiemelés, a t() hívás a unique_lock megszűnése után történik. A TSan ezt a hibát nem feltétlenül jelzi, mert nincs benne adatverseny: a queue minden hozzáférése zár alatt van, a holtpont logikai, nem memóriamodell-sértés. Az ellenőrzési feltétel: a válasz nevezze meg a zár birtoklását a t() hívás alatt, és mondja ki, hogy a deadlock nem adatverseny.

4. megoldás. A drain politikánál a worker a stop_ beállítása után is feldolgozza a sorban lévő elemeket, de új elemet nem fogad el, így a ciklus véges. A submit a zár alatt ellenőrzi a stop_-ot; a run a zár alatt csak akkor lép ki, ha stop_ && q_.empty().

void submit(Task t) {
    {
        std::lock_guard lk(mu_);
        if (stop_) return;
        q_.push_back(std::move(t));
    }
    cv_.notify_one();
}

void stop_drain() {
    {
        std::lock_guard lk(mu_);
        stop_ = true;
    }
    cv_.notify_all();
    if (th_.joinable()) th_.join();
}

void run() {
    for (;;) {
        Task t;
        {
            std::unique_lock lk(mu_);
            cv_.wait(lk, [this] { return stop_ || !q_.empty(); });
            if (q_.empty()) {          // stop_ && üres: kilépés
                if (stop_) return;
                continue;
            }
            t = std::move(q_.front());
            q_.pop_front();
        }
        t();
    }
}

A végesség indoklása: a stop_ beállítása után minden submit a zár alatt elutasításra kerül, tehát a queue hossza monoton csökken, és a ciklus legfeljebb a stop_ pillanatában a sorban lévő elemek számáig fut. Az ellenőrzési feltétel: stop_drain után a submit ne módosítsa a queue-t, a join pedig előbb térjen vissza, mint a stop_drain visszatérése; stressztesztben a taszkok száma legyen nagyobb a szálakénál.

34.6.11 Összefoglaló

A shared_ptr két külön szerződést fed: a control block szintjén az ownership könyvelése különböző példányokon szálbiztos, a pointee állapotához viszont semmit nem ad. Ugyanazon shared_ptr példány egyidejű módosítása adatverseny; erre a C++20 atomic<shared_ptr> specializációja kínál megoldást, jellemzően nem lock-free áron. A registry példa megmutatta, hogy a rétegek szétválasztása — registry-zár, pointee-zár, queue-zár — és a felhasználói destruktorok záron kívüli futtatása adja a helyes sorrendet. A reclamation shared_ptr-nél referenciaszámlálás, nyers pointerrel kombinálva nem véd; az ABA és a lock-free reclamation a C++26-os kitekintés témája. A helyesség eszköze a TSan és a stressz mellett az invariáns pillanatkép és a leállítási sorrend explicit szerződése, nem a „thread-safe okos pointer” feltételezése.