From ffccd6ddd2a4a09fd0ff5fa8b1ce7f204e5556bb Mon Sep 17 00:00:00 2001 From: Matt Kline Date: Tue, 24 Apr 2018 21:38:09 -0700 Subject: [PATCH] Use atomics to count shared_object without locks C++11 (and subsequent C++ standards) provide portable ways to issue atomic hardware instructions, which allow multiple threads to load, store, and modify integers without taking a lock. The standard also defines a memory model that lets you express the ordering guarantees around these atomic operations. (x86 is relatively strongly-ordered, but many other common architectures, such as ARM, are free to reorder loads and stores unless told not to.) This patch removes the lock from shared_object and replaces it with the standard thread-safe reference counting implementation used in C++'s std::shared_ptr, Rust's std::sync::Arc, and many others. Additional resources on the topic: https://assets.bitbashing.io/papers/concurrency-primer.pdf https://www.youtube.com/watch?v=ZQFzMfHIxng --- include/shared_object.h | 49 +++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/include/shared_object.h b/include/shared_object.h index fe7a3d8e0..efb8ba5d6 100644 --- a/include/shared_object.h +++ b/include/shared_object.h @@ -26,15 +26,13 @@ #ifndef SHARED_OBJECT_H #define SHARED_OBJECT_H -#include - +#include class sharedObject { public: sharedObject() : - m_referenceCount( 1 ), - m_lock() + m_referenceCount(1) { } @@ -45,19 +43,34 @@ public: template static T* ref( T* object ) { - object->m_lock.lock(); - // TODO: Use QShared - ++object->m_referenceCount; - object->m_lock.unlock(); + // Incrementing an atomic reference count can be relaxed since no action + // is ever taken as a result of increasing the count. + // Other loads and stores can be reordered around this without consequence. + object->m_referenceCount.fetch_add(1, std::memory_order_relaxed); return object; } template static void unref( T* object ) { - object->m_lock.lock(); - bool deleteObject = --object->m_referenceCount <= 0; - object->m_lock.unlock(); + // When decrementing an atomic reference count, we need to provide + // two ordering guarantees: + // 1. All reads and writes to the referenced object occur before + // the count reaches zero. + // 2. Deletion occurs after the count reaches zero. + // + // To accomplish this, each decrement must be store-released, + // and the final thread (which is deleting the referenced data) + // must load-acquire those stores. + // The simplest way to do this to give the decrement acquire-release + // semantics. + // + // See https://www.boost.org/doc/libs/1_67_0/doc/html/atomic/usage_examples.html + // for further discussion, along with a slightly more complicated + // (but possibly more performant on weakly-ordered hardware like ARM) + // approach. + const bool deleteObject = + object->m_referenceCount.fetch_sub(1, std::memory_order_acq_rel) == 1; if ( deleteObject ) { @@ -65,20 +78,8 @@ public: } } - // keep clang happy which complaines about unused member variable - void dummy() - { - m_referenceCount = 0; - } - private: - int m_referenceCount; - QMutex m_lock; - + std::atomic_int m_referenceCount; } ; - - - #endif -