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
This commit is contained in:
Matt Kline
2018-04-24 21:38:09 -07:00
committed by Lukas W
parent f7a0553e6a
commit ffccd6ddd2

View File

@@ -26,15 +26,13 @@
#ifndef SHARED_OBJECT_H
#define SHARED_OBJECT_H
#include <QtCore/QMutex>
#include <atomic>
class sharedObject
{
public:
sharedObject() :
m_referenceCount( 1 ),
m_lock()
m_referenceCount(1)
{
}
@@ -45,19 +43,34 @@ public:
template<class T>
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<class T>
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