SmartPointer revisited

Unsere ursprüngliche Smartpointer-Implementierung war nicht thread-safe. Für die Demonstration des Konzeptes ist sowas ok, aber natürlich nicht im realen Einsatz. Das Problem ist der Instanzzähler, der sich schlicht verzählt. std::atomic<> kann uns hier helfen.

Video

Quelltext

#include <string>
#include <iostream>
#include <atomic>
#include <thread>
#include <stdexcept>

template <typename T, typename CounterType> class SmartPointer
{
  struct Storage
  {
    T* mPointer;
    CounterType mCounter;
  };

  Storage *mStorage;

  void release()
  {
    mStorage->mCounter--;
    if (mStorage->mCounter == 0) {
      delete mStorage->mPointer;
      delete mStorage;
    }
  }

public:

  explicit SmartPointer(T *p)
    : mStorage(new Storage)
  {
    mStorage->mPointer = p;
    mStorage->mCounter = 1;
  }

  SmartPointer(SmartPointer const &other)
    : mStorage(other.mStorage)
  {
      mStorage->mCounter++;
  }

  ~SmartPointer()
  {
    release();
  }

  unsigned int useCount() const
  {
    return mStorage->mCounter;
  }
};

typedef SmartPointer<std::string, unsigned int> UnsafePtr;
typedef SmartPointer<std::string, std::atomic<unsigned int>> SafePtr;

template <typename PtrType> void stressThread(PtrType ptr)
{
    for (unsigned int i = 0; i < 10000000; i++) {
        PtrType newPtr(ptr);
    }
}

template <typename PtrType> void run()
{
    PtrType ptr(new std::string("Test"));
    try {
        std::thread t1([&ptr]() { stressThread(ptr); });
        std::thread t2([&ptr]() { stressThread(ptr); });
        t1.join();
        t2.join();

        std::cout << "Erfolgreich durchlaufen!\n";
	std::cout << "Count: " << ptr.useCount() << std::endl;
    }
    catch (std::runtime_error &e) {
        std::cerr << e.what() << std::endl;
    }
}

int main()
{
    //run<UnsafePtr>();
    run<SafePtr>();
}

Erklärung

Alle Kopien eines SmartPointers teilen sich die gleiche Storage-Struktur (das ist ja die Grundlage des Instanzzählers, der für alle Kopien gleich sichtbar sein muss). Verwendet man nun Kopien des Pointers in verschiedenen Threads, haben wir wieder das Problem der nicht-atomaren Zählung. Sowohl der operator++(), als auch der operator--() können Werte verlieren und damit natürlich die Instanzzählung nutzlos machen. Das Video demonstriert das durch zwei Threads, die parallel den Pointer kopieren und wieder löschen. Der Counter ist eigentlich immer falsch und in manchen Fällen kommt es auch zum Absturz des Programms, da wir auf freigegebenen Speicher zugreifen. Das sind eigentlich sogar die günstigeren Fälle, denn ein Absturz ist ein definierter Zustand. In einem richtigen Programm würden wir ggf. auf Speicher zugreifen, der mittlerweile anderen Objekten zugewiesen ist und deren interne Struktur überschreiben. Solche Fehler, die sich tief im Land des undefinierten Verhaltens befinden, sind extrem schwer zu finden.

Korrekterweise muss das Zählen also atomar implementiert sein. An dieser Stelle kommt std::atomic<> ins Spiel. Die gesamte Implementierung bleibt gleich, nur der Datentyp des Counters wird ausgetauscht. Nun können die Threads fleißig kopieren und löschen und trotzdem bleibt das ganze korrekt. Am Ende, nachdem alle Kopien vernichtet sind und nur der Ursprungszeiger übrig bleibt, hat dieser völlig korrekterweise einen useCount() von 1.