1

I have a struct containing two elements.

struct MyStruct {
  int first_element_;
  std::string second_element_;
}

The struct is shared between threads and therefore requires locking. My use case requires to lock access to the whole struct instead of just a specific member, so for example:

  // start of Thread 1's routine
  <Thread 1 "locks" struct>
  <Thread 1 gets first_element_>
  <Thread 1 sets second_elements_>
  <Thread 2 wants to access struct -> blocks>
  <Thread 1 sets first_element_>
  // end of Thread 1's routine
  <Thread 1 releases lock>
  <Thread 2 reads/sets ....>

What's the most elegant way of doing that?

EDIT: To clarify, basically this question is about how to enforce any thread using this struct to lock a mutex (stored wherever) at the start of its routine and unlock the mutex at the end of it.

EDIT2: My current (ugly) solution is to have a mutex inside MyStruct and lock that mutex at the start of each thread's routine which uses MyStruct. However, if one thread "forgets" to lock that mutex, I run into synchronization problems.

Lexusminds
  • 335
  • 2
  • 14
  • 1
    Does this answer your question? [What's the proper way to associate a mutex with its data?](https://stackoverflow.com/questions/15844972/whats-the-proper-way-to-associate-a-mutex-with-its-data) – Thomas Sablik Feb 05 '20 at 07:50
  • Interesting. I gotta admit that I don't fully understand the functionality quite yet, but I'll spend the next hour or so reading into it :D – Lexusminds Feb 05 '20 at 08:04
  • so you want to make "MyStruct" thread safe -- this is typically achieved by making data members private, provide access functions and use mutex/lock in the functions to enforce exclusive access. Does this answer your question? – SPD Feb 05 '20 at 10:50
  • Another solution: https://stackoverflow.com/a/57857672/412080 – Maxim Egorushkin Feb 05 '20 at 11:08
  • 1
    If you want the whole struct to be locked while a thread is using it, it must be done in the thread scope. Indeed, the structure has no way to know who is trying to modify it (which thread is allowed, which one is not), except if you pass the thread ids to the struct when accessing it which would be way uglier than locking/unlocking a mutex in the thread definition. – Fareanor Feb 05 '20 at 13:34
  • Moreover, your question sounds like "How to lock/unlock a mutex without locking/unlocking the mutex" ? I'm afraid it is impossible. _"...if one threads forgets to lock..."_ indeed, but if I "forget" to write the main function for example, it will fail to work as well. If you need thread synchronization, use the synchronization mechanisms, if you don't, you'll not get it, sounds reasonable :) Your solution is not ugly at all in my opinion, it is the proper way to do it. – Fareanor Feb 05 '20 at 13:42

2 Answers2

1

You can have a class instead of the struct and implement getters and setters for first_element_ and second_element_. Besides those class members, you will also need a member of type std::mutex.

Eventually, your class could look like this:

class Foo {
public:
    // ...

    int get_first() const noexcept {
        std::lock_guard<std::mutex> guard(my_mutex_);
        return first_element_;
    }

    std::string get_second() const noexcept {
        std::lock_guard<std::mutex> guard(my_mutex_);
        return second_element_;
    }

private:
    int first_element_;
    std::string second_element_;
    std::mutex my_mutex_;
};

Please, note that getters are returning copies of the data members. If you want to return references (like std::string const& get_second() const noexcept) then you need to be careful because the code that gets the reference to the second element has no lock guard and there might be a race condition in such case.

In any case, your way to go is using std::lock_guards and std::mutexes around code that can be used by more than one thread.

NutCracker
  • 11,485
  • 4
  • 44
  • 68
  • Thanks for your answer. However, this doesn't solve the problem shown in my little sequence shown above. I want to guarantee that nobody does access this structure and change its members for as long as I'm holding some lock. Protecting the getter/setter individually would allow Thread 1 to get A, then Thread 2 to set B, then Thread A to get B etc. Instead I want Thread 1 to hold a lock, get/set as many things as it wants and only THEN release the lock for Thread 2 to start working. – Lexusminds Feb 05 '20 at 07:56
  • @Lexusminds Ok then just have a global `std::mutex` lock it with `std::lock_guard` whenever you use your object (the last sentence in my answer) – NutCracker Feb 05 '20 at 08:02
  • That's my current solution. However, I think it's quite "ugly" because one can easily forget to lock that mutex. That is why I asked if there is a better solution which is less error prone. – Lexusminds Feb 05 '20 at 08:03
  • @Lexusminds well, I believe there is no prettier solution for you in that case. But remember that if you want to lock the whole object (as you say), then your code will be slow because threads will be waiting a lot more time – NutCracker Feb 05 '20 at 08:05
  • There is a much prettier solution to lock the whole object. It's already proposed for the standard but I currently can't find it. – Thomas Sablik Feb 05 '20 at 08:10
  • @ThomasSablik let me know if you find it – NutCracker Feb 05 '20 at 08:12
  • @ThomasSablik let me know too, please :D – Lexusminds Feb 05 '20 at 08:18
  • It's called [synchronized_value](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4033.html) and there is an experimental implementation in [Boost](https://www.boost.org/doc/libs/1_72_0/doc/html/thread/sds.html). @AlanBirtles answer is a simple version of it. – Thomas Sablik Feb 05 '20 at 09:30
0

You could implement something like this that combines the lock with the data:

class Wrapper
{
public:
   Wrapper(MyStruct& value, std::mutex& mutex)
   :value(value), lock(mutex) {}

   MyStruct& value;
private:
   std::unique_lock<std::mutex> lock;
};

class Container
{
public:
   Wrapper get()
   {
      return Wrapper(value, mutex);
    }
private:
   MyStruct value;
   std::mutex mutex;
};

The mutex is locked when you call get and unlocked automatically when Wrapper goes out of scope.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60