I have implemented a thread safe holder to safely pass data between threads.
User can set value many times, but only the first SetIfEmpty
call stores the value, then user may read the value many times.
template <typename T>
class ThreadSafeHolder {
public:
ThreadSafeHolder() : is_value_set_(false) {
}
void SetIfEmpty(const T& value) {
std::lock_guard<std::mutex> lock(mutex_);
// memory_order_relaxed is enough because storing to
// `is_value_set_` happens only in `SetIfEmpty` methods
// which are protected by mutex.
if (!is_value_set_.load(std::memory_order_relaxed)) {
new(GetPtr()) T(value);
is_value_set_.store(true, std::memory_order_release);
}
}
void SetIfEmpty(T&& value) {
std::lock_guard<std::mutex> lock(mutex_);
if (!is_value_set_.load(std::memory_order_relaxed)) {
new(GetPtr()) T(std::move(value));
is_value_set_.store(true, std::memory_order_release);
}
}
//! This method might be safely call only if previous `IsEmpty()`
//! call returned `false`.
const T& Get() const {
assert(!IsEmpty());
return *GetPtr();
}
bool IsEmpty() const {
// memory_order_acquire loading to become synchronize with
// memory_order_release storing in `SetIfEmpty` methods.
return !is_value_set_.load(std::memory_order_acquire);
}
~ThreadSafeHolder() {
if (!IsEmpty()) {
GetPtr()->~T();
}
}
private:
T* GetPtr() {
return reinterpret_cast<T*>(value_place_holder_);
}
const T* GetPtr() const {
return reinterpret_cast<const T*>(value_place_holder_);
}
// Reserved place for user data.
char value_place_holder_[sizeof(T)];
// Mutex for protecting writing access to placeholder.
std::mutex mutex_;
// Boolean indicator whether value was set or not.
std::atomic<bool> is_value_set_;
};
Questions
- Is the code correct in general?
- Is access to
is_value_set_
member properly synchronized?
- Might be access to
is_value_set_
member even more relaxed?
Application
I wanted to develop such holder to pass active exceptions from worker threads to main thread.
Main thread:
ThreadSafeHolder<std::exception_ptr> exceptionPtrHolder;
// Run many workers.
// Join workers.
if (!exceptionPtrHolder.IsEmpty()) {
std::rethrow_exception(exceptionPtrHolder.Get());
}
Worker thread:
try {
while (exceptionPtrHolder.IsEmpty()) {
// Do hard work...
}
} catch (...) {
exceptionPtrHolder.SetIfEmpty(std::current_exception());
}
Note about std::promise
std::promise
is not suitable here (despite the fact that std::promise::set_value
is thread safe) because
An exception is thrown if there is no shared state or the shared state already stores a value or exception.