简体   繁体   中英

Thread-safe vector implementation

I'm working on a thread-safe std::vector implementation and the following is a completed preliminary attempt:

    #ifndef THREADSAFEVECTOR_H
    #define THREADSAFEVECTOR_H
    #include <iostream>
    #include <vector>
    #include <mutex>
    #include <cstdlib>
    #include <memory>
    #include <iterator>
    #include <algorithm>
    #include <initializer_list>
    #include <functional>
    template <class T, class Alloc=std::allocator<T>>
    class ThreadSafeVector
    {
        private:
            std::vector<T> threadSafeVector;
            std::mutex vectorMutex;

        public:
            /*need to use typename here because std::allocator<T>::size_type, std::allocator<T>::value_type, std::vector<T>::iterator, 
            and std::vector<T>::const_reverse_iterator are 'dependent names' in that because we are working with a templated class, 
            these expressions may depend on types of type template parameters and values of non-template parameters*/

            typedef typename std::vector<T>::size_type size_type;

            typedef typename std::vector<T>::value_type value_type;

            typedef typename std::vector<T>::iterator iterator;

            typedef typename std::vector<T>::const_iterator const_iterator;

            typedef typename std::vector<T>::reverse_iterator reverse_iterator;

            typedef typename std::vector<T>::const_reverse_iterator const_reverse_iterator;

            typedef typename std::vector<T>::reference reference;

            typedef typename std::vector<T>::const_reference const_reference;

            /*wrappers for three different at() functions*/
            template <class InputIterator>
            void assign(InputIterator first, InputIterator last)
            {
                //using a local lock_guard to lock mutex guarantees that the mutex will be unlocked on destruction and in the case of an exception being thrown
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.assign(first, last);
            }

            void assign(size_type n, const value_type& val)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.assign(n, val);
            }

            void assign(std::initializer_list<value_type> il)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.assign(il.begin(), il.end());
            }

            /*wrappers for at() functions*/
            reference at(size_type n)
            {
                return threadSafeVector.at(n);
            }

            const_reference at(size_type n) const
            {
                return threadSafeVector.at(n);
            }   

            /*wrappers for back() functions*/
            reference back()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.back();
            }

            const reference back() const
            {
                return threadSafeVector.back();
            }

            /*wrappers for begin() functions*/
            iterator begin()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.begin();
            }

            const iterator begin() const noexcept
            {
                return threadSafeVector.begin();
            }

            /*wrapper for capacity() fucntion*/
            size_type capacity() const noexcept
            {
                return threadSafeVector.capacity();
            }

            /*wrapper for cbegin() function*/
            const iterator cbegin()
            {
                return threadSafeVector.cbegin();
            }

            /*wrapper for cend() function*/
            const iterator cend()
            {
                return threadSafeVector.cend();
            }

            /*wrapper for clear() function*/
            void clear()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.clear();
            }

            /*wrapper for crbegin() function*/
            const_reverse_iterator crbegin() const noexcept
            {
                return threadSafeVector.crbegin();
            }

            /*wrapper for crend() function*/
            const_reverse_iterator crend() const noexcept
            {
                return threadSafeVector.crend();
            }

            /*wrappers for data() functions*/
            value_type* data()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.data();
            }

            const value_type* data() const noexcept
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.data();
            }

            /*wrapper for emplace() function*/
            template <class... Args>
            void emplace(const iterator position, Args&&... args)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.emplace(position, args...);
            }

            /*wrapper for emplace_back() function*/
            template <class... Args>
            void emplace_back(Args&&... args)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.emplace_back(args...);
            }

            /*wrapper for empty() function*/
            bool empty() const noexcept
            {
                return threadSafeVector.empty();
            }

            /*wrappers for end() functions*/
            iterator end()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.end();
            }

            const iterator end() const noexcept
            {
                return threadSafeVector.end();
            }

            /*wrapper functions for erase()*/
            iterator erase(const_iterator position)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.erase(position);
            }

            iterator erase(const_iterator first, const_iterator last)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.erase(first, last);
            }

            /*wrapper functions for front()*/
            reference front()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.front();
            }

            const reference front() const
            {
                return threadSafeVector.front();
            }

            /*wrapper function for get_allocator()*/
            value_type get_allocator() const noexcept
            {
                return threadSafeVector.get_allocator();
            }

            /*wrapper functions for insert*/
            iterator insert(const_iterator position, const value_type& val)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.insert(position, val); 
            }

            iterator insert(const_iterator position, size_type n, const value_type& val)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.insert(position, n, val);
            }

            template <class InputIterator>
            iterator insert(const_iterator position, InputIterator first, InputIterator last)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.insert(position, first, last);
            }

            iterator insert(const_iterator position, value_type&& val)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.insert(position, val);
            }

            iterator insert(const_iterator position, std::initializer_list<value_type> il)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.insert(position, il.begin(), il.end());
            }

            /*wrapper function for max_size*/
            size_type max_size() const noexcept
            {
                return threadSafeVector.max_size();
            }

            /*wrapper functions for operator =*/
            std::vector<T>& operator= (const std::vector<T>& x)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.swap(x);
            }

            std::vector<T>& operator= (std::vector<T>&& x)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector=std::move(x);
            }

            std::vector<T>& operator= (std::initializer_list<value_type> il)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.assign(il.begin(), il.end());

                return *this; //is this safe to do?
            }

            /*wrapper functions for operator []*/
            reference operator[] (size_type n)
            {
                return std::ref(n);
            }

            const_reference operator[] (size_type n) const
            {
                return std::cref(n);
            }

            /*wrapper function for pop_back()*/
            void pop_back()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.pop_back();
            }

            /*wrapper functions for push_back*/
            void push_back(const value_type& val)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.push_back(val);
            }

            void push_back(value_type&& val)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.push_back(val);
            }

            /*wrapper functions for rbegin()*/
            reverse_iterator rbegin() noexcept
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.rbegin();
            }

            const_reverse_iterator rbegin() const noexcept
            {
                return threadSafeVector.rbegin();
            }

            /*wrapper functions for rend()*/
            reverse_iterator rend() noexcept
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                return threadSafeVector.rend();
            }

            const_reverse_iterator rend() const noexcept
            {
                return threadSafeVector.rend();
            }

            /*wrapper function for reserve()*/
            void reserve(size_type n)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.reserve(n);
            }

            /*wrapper functions for resize()*/      
            void resize(size_type n)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.resize(n);
            }

            void resize(size_type n, const value_type& val)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.resize(n, val);
            }

            void shrink_to_fit()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.shrink_to_fit();
            }

            //add function for size
            size_type size() const noexcept
            {
                return threadSafeVector.size();
            }

            /*wrapper function for swap()*/
            void swap(std::vector<T>& x)
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                threadSafeVector.swap(x);
            }

            void print()
            {
                std::lock_guard<std::mutex> vectorLockGuard(vectorMutex);

                for(const auto & element : threadSafeVector)
                {
                    std::cout << element << std::endl;
                }

                std::cout << std::endl;
            }
    };
    #endif

I have based my implementation on the description of the vector class and its member functions found on cplusplus.com and with help from the implementation of the vector class from the STL . Now, a few questions I have about the code I've written so far:

  1. When returning iterators, I wasn't sure if I should lock the mutex and then return the iterator because of the possibility that the validity of the iterator(s) might change due to multiple threads trying to access it, so I went ahead and locked the mutex for all non-const iterators. Is this the right approach?

  2. It is my understanding that one should not return pointers from functions when dealing with multithreaded code since this provides a "backdoor" (for lack of a better term) for user code to preform some potentially questionable activity. So, for the implementation of the assignment operator , is there another way to write these functions so that it doesn't return *this?

  3. I opted to use all local instances of lock_guard instead of having one as a private data member. Would it be better if I had one as a private data member instead?

Many thanks in advance :-)

Synchronization between threads is a global problem; it can't be solved locally. So the right answer is to unask the question.

This approach is simply the wrong level of granularity. Preventing conflicting simultaneous calls to member functions does not make a container thread-safe in any useful sense; users still have to ensure that sequences of operations are thread-safe, and that means holding a lock while a sequence of operations is going on.

For a simple example, consider

void swap(vector<int>& v, int idx0, int idx1) {
    int temp = v[idx0];
    v[idx0] = v[idx1];
    v[idx1] = temp;
}

Now, what happens if, after copying v[idx1] into v[idx0] , some other thread comes along and erases all the data in the vector? The assignment to v[idx1] writes into random memory. That's not a good thing. To prevent this, user code must ensure that throughout the execution of swap no other thread is messing with the vector. The implementation of vector can't do that.

If you want a consistent implementation with not too much complication, you need at least two new member functions, for example disable_write() and enable_write() .

Code which is using your vector can opt if it wants consistent state during some reading code block or not. It will call disable_write() at the beginning of read block and call enable_write() when it finishes block where consistent vector state was needed.

Add another "write_lock" mutex and in each member function which makes changes use this mutex.

While the write lock is active, read operations should be freely permitted so there's no need for "write_lock" mutex for member functions which only read data. Mutex you already have is enough.

Also, you could add another class for write-locking you vector, for example some equivalent of lock_guard and even make disable_write() and enable_write() private and friend with this class, to prevent accidental write lock which is never released.

Vector seem inherently not thread safe. That can seem brutal, but the simplest way to synchronize a vector for me is to encapsulate it in another object who is thread safe...

struct guardedvector {
   std::mutex guard;
   std::vector myvector;
}

guardedvector v;
v.guard.lock();
... use v.myvector
v.guard.unlock();

In win32 you can also use slim reader/writer lock (SRW). They are light and fast mutexes, that can work with multiple readers/one writer. In this case, you replace the guard by a srwlock. The caller code has the responsability to call the correct locking methods. Finally you can template the struct and inline it.

After discussing with myself, the best way to thread safe a vector is to inherit from vector and add a guard.

enter code here
template <class t> class guardedvector : public std::vector<t> {
    std::mutex guard;
};

guardedvector v;

v.lock();
//...iterate, add, remove...
v.unlock();

You can use a swrlock in place of the mutex. But if you have a lot of threads that read the vector, that can introduce a latency for the writer thread. In certain case you can recreate a custom threading model using just atomics operations with a priority model for the writer, but this require a deep know how the processor work. For simplicity, you can change the priority level of the writer thread.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM