Home > Net >  Thread-safe locking of instance with multiple member functions
Thread-safe locking of instance with multiple member functions

Time:10-12

I have a struct instance that gets used by multiple threads. Each thread contains an unknown amount of function calls that alter the struct member variable.

I have a dedicated function that tries to "reserve" the struct instance for the current thread and I would like to ensure no other thread can reserve the instance till the original thread allows it.

Mutexes come to mind as those can be used to guard resources, but I only know of std::lock_guard that are in the scope of a single function, but do not add protection for all function calls in between lock and unlock.

Is it possible to protect a resource like that, when I know it will always call reserve and release in that order?

Snippet that explains it better:

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex


struct information_t {
    std::mutex mtx;
    int importantValue = 0;

    // These should only be callable from the thread that currently holds the mutex
    void incrementIt() { importantValue  ; }
    void decrementIt() { importantValue--; }
    void reset() { importantValue = 0; }

} protectedResource; // We only have one instance of this that we need to work with

// Free the resource so other threads can reserve and use it
void release()
{
    std::cout << "Result: " << protectedResource.importantValue << '\n';
    protectedResource.reset();
    protectedResource.mtx.unlock(); // Will this work? Can I guarantee the mtx is locked?
}

// Supposed to make sure no other thread can reserve or use it now anymore!
void reserve()
{ 
    protectedResource.mtx.lock();
}

int main()
{
    std::thread threads[3];
    
    threads[0] = std::thread([]
    {
            reserve();
            protectedResource.incrementIt();
            protectedResource.incrementIt();
            release();
    });

    threads[1] = std::thread([]
        {
            reserve();
            // do nothing
            release();
        });

    threads[2] = std::thread([]
        {
            reserve();
            protectedResource.decrementIt();
            release();
        });

    for (auto& th : threads) th.join();

    return 0;
}

CodePudding user response:

My suggestion per comment:

A better idiom might be a monitor which keeps the lock of your resource and provides access to the owner. To obtain a resource, the reserve() could return such monitor object (something like a proxy to access the contents of the resource). Any competing access to reserve() would block now (as the mutex is locked). When the resource owning thread is done, it just destroys the monitor object which in turn unlocks the resource. (This allows to apply RAII to all this which makes your code safe and maintainable.)

I modified OPs code to sketch how this could look like:

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex

class information_t {

  private:
    std::mutex mtx;
    int importantValue = 0;

  public:
    class Monitor {
      private:
        information_t& resource;
        std::lock_guard<std::mutex> lock;
    
      friend class information_t; // to allow access to constructor.
      
      private:      
        Monitor(information_t& resource):
          resource(resource), lock(resource.mtx)
        { }
      public:
        ~Monitor()
        {
          std::cout << "Result: " << resource.importantValue << '\n';
          resource.reset();
        }
      
        Monitor(const Monitor&) = delete; // copying prohibited
        Monitor& operator=(const Monitor&) = delete; // copy assign prohibited
    
      public:
        // exposed resource API for monitor owner:
        void incrementIt() { resource.incrementIt(); }
        void decrementIt() { resource.decrementIt(); }
        void reset() { resource.reset(); }
    };
    friend class Monitor; // to allow access to private members
  
  public:
    Monitor aquire() { return Monitor(*this); }
    
  private:
    // These should only be callable from the thread that currently holds the mutex
    // Hence, they are private and accessible through a monitor instance only
    void incrementIt() { importantValue  ; }
    void decrementIt() { importantValue--; }
    void reset() { importantValue = 0; }

} protectedResource; // We only have one instance of this that we need to work with

#if 0 // OBSOLETE
// Free the resource so other threads can reserve and use it
void release()
{
    protectedResource.reset();
    protectedResource.mtx.unlock(); // Will this work? Can I guarantee the mtx is locked?
}
#endif // 0

// Supposed to make sure no other thread can reserve or use it now anymore!
information_t::Monitor reserve()
{ 
  return protectedResource.aquire();
}

using MyResource = information_t::Monitor;

int main()
{
    std::thread threads[3];
    
    threads[0]
      = std::thread([]
        {
          MyResource protectedResource = reserve();
          protectedResource.incrementIt();
          protectedResource.incrementIt();
          // scope end releases protectedResource
        });

    threads[1]
      = std::thread([]
        {
          try {
            MyResource protectedResource = reserve();
            throw "Haha!";
            protectedResource.incrementIt();
            // scope end releases protectedResource
          } catch(...) { }
        });

    threads[2]
      = std::thread([]
        {
          MyResource protectedResource = reserve();
          protectedResource.decrementIt();
            // scope end releases protectedResource
        });

    for (auto& th : threads) th.join();

    return 0;
}

Output:

Result: 2
Result: -1
Result: 0

Live Demo on coliru

Is it possible to protect a resource like that, when I know it will always call reserve and release in that order?

It's not anymore necessary to be concerned about this. The correct usage is burnt in:

  • To get access to the resource, you need a monitor.
  • If you get it you are the exclusive owner of the resource.
  • If you exit the scope (where you stored the monitor as local variable) the monitor is destroyed and thus the locked resource auto-released.

The latter will happen even for unexpected bail-outs (in the MCVE the throw "Haha!";).

Furthermore, I made the following functions private:

  • information_t::increment()
  • information_t::decrement()
  • information_t::reset()

So, no unauthorized access is possible. To use them properly, an information_t::Monitor instance must be acquired. It provides public wrappers to those functions which can be used in the scope where the monitor resides i.e. by the owner thread only.

  • Related