I have a question regarding the term thread-safety. Let me give an example:
#include
#include
/// A thread-safe vector
class Th
Though your vector may look like thread safe as soon as you start to use it you will see that it is not. For example, I want to add tasks to a vector if it is smaller than 5 (keep it not bigger than 5)
ThreadSafeVector tv;
if( tv.length() < 5 ) tv.add( 10.0 );
would this properly work in multi-thread environment? No. As you would add more logic to your vector, that will become more and more complicated.
We have an on-going internal team discussion about the meaning of thread-safety. Slavas comment "Though length() is technically "thread safe" in reality it is not" boils it down to the ambivalent essence. Probably, it is impossible to answer my simple question with "yes" or "no"?
Here, is my view: Thread-safety only requires clean semantics regarding PARALLEL execution of its operations. The class ThreadSafeVector is thread-safe because its functions guarantee the following for parallel execution of its operations:
Calling a class thread-safe does not require that any possible aggregated usage of it has to be thread-safe on its own, i.e. serial execution of methods on the class does not have to be thread-safe. Example:
if (a.length() == 0) a.add(42);
Of course this line is not thread-safe because it's not atomic on its own and the class does not even provide "tools" to do something like this. But just because I can construct a non-thread-safe sequence from thread-safe operations, it does not mean that the thread-safe operations are really not thread-safe.
While this is thread-safe, it's not efficient. You could easily make it more efficient by using a shared_mutex
(either C++14 or Boost, it's not in C++11). This is because if two threads ask for the size, this should not be a problem. However, if a thread asks for the size and another wanted to add an element, then only one of them should be allowed access.
So I would change your code like this:
#include <mutex>
#include <vector>
#include <shared_mutex>
/// A thread-safe vector
class ThreadSafeVector {
private:
mutable std::shared_timed_mutex m; //notice the mutable
std::vector<double> v;
public:
// add double to vector
void add(double d) {
std::unique_lock<std::shared_timed_mutex> lg(m); //just shared_mutex doesn't exist in C++14, you're welcome to use boost::shared_mutex, it's the same
v.emplace_back(d);
}
// return length of vector
//notice the const, because this function is not supposed to modify your class
int length() const {
std::shared_lock<std::shared_timed_mutex> lg(m);
return v.size();
}
};
A few things to keep in mind:
std::mutex
(and all other mutexes) are non-copyable. This means that your class is now non-copyable. To make it copyable, you have to implement the copy-constructor yourself and bypass copying the mutex.
always make your mutexes mutable
in containers. This is because modifying a mutex doesn't mean you're modifying the content of the class, which is compatible with the const
I added to the length()
method. That const
means that this method doesn't modify anything within the class. It's a good practice to use it.
Yes, I would. Both public methods are protected by locks, and all the special member functions (copy/move constructor/assignment) are implicitly deleted because std::mutex
is neither copyable nor movable.
What you have provided is thread-safe. However the problem with this is that you can't add methods that allow access to the elements without loosing thread safety. Also this kind of thread-safety is very inefficient. Sometimes you want to iterate over the whole container and sometimes you want to add many elements one after another.
As an alternative you can put the responsibility for locking on the caller. This is much more efficient.
/// A lockable vector
class LockableVector
{
public:
using mutex_type = std::shared_timed_mutex;
using read_lock = std::shared_lock<mutex_type>;
using write_lock = std::unique_lock<mutex_type>;
// default ctor
LockableVector() {}
// move-ctor
LockableVector(LockableVector&& lv)
: LockableVector(std::move(lv), lv.lock_for_writing()) {}
// move-assign
LockableVector& operator=(LockableVector&& lv)
{
lv.lock_for_writing();
v = std::move(lv.v);
return *this;
}
read_lock lock_for_reading() { return read_lock(m); }
write_lock lock_for_writing() { return write_lock(m); }
// add double to vector
void add(double d) {
v.emplace_back(d);
}
// return length of vector
int length() {
return v.size();
}
// iteration
auto begin() { return v.begin(); }
auto end() { return v.end(); }
private:
// hidden, locked move-ctor
LockableVector(LockableVector&& lv, write_lock)
: v(std::move(lv.v)) {}
mutex_type m;
std::vector<double> v;
};
int main()
{
LockableVector v;
// add lots of elements
{ /// create a scope for the lock
auto lock = v.lock_for_writing();
for(int i = 0; i < 10; ++i)
v.add(i);
}
// print elements
{ /// create a scope for the lock
auto lock = v.lock_for_reading();
std::cout << std::fixed << std::setprecision(3);
for(auto d: v)
std::cout << d << '\n';
}
}
Also by having both read and write locks you increase efficiency because you can have multiple readers at the same time when no thread is currently writing.
IMHO:
This is both safe and useful:
void add(double d) {
std::lock_guard<std::mutex> lg(m);
v.emplace_back(d);
}
This is safe but useless:
// return length of vector
int length() {
std::lock_guard<std::mutex> lg(m);
return v.size();
}
Because by the time you've got your length it may well have changed, so reasoning about it is unlikely to be useful.
How about this?
template<class Func>
decltype(auto) do_safely(Func&& f)
{
std::lock_guard<std::mutex> lock(m);
return f(v);
}
called like this:
myv.do_safely([](auto& vec) {
// do something with the vector
return true; // or anything you like
});