【问题标题】:Thread-safe locking of instance with multiple member functions具有多个成员函数的实例的线程安全锁定
【发布时间】:2021-11-29 12:06:53
【问题描述】:

我有一个被多个线程使用的结构实例。每个线程都包含未知数量的函数调用,这些函数调用会改变结构成员变量。

我有一个专用函数尝试为当前线程“保留”结构实例,我想确保在原始线程允许之前没有其他线程可以保留该实例。

我想到了互斥锁,因为它们可以用来保护资源,但我只知道 std::lock_guard 在单个函数的范围内,但不会为锁定和解锁之间的所有函数调用添加保护。

当我知道它总是按该顺序调用reserve和release时,是否可以保护这样的资源?

更好地解释它的片段:

#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;
}

【问题讨论】:

  • 一个更好的习惯用法可能是monitor,它可以保持资源的锁定并提供对所有者的访问权限。要获取资源,reserve() 可以返回这样的监视器对象(类似于访问资源内容的代理)。任何对reserve() 的竞争访问现在都将被阻止(因为互斥锁已被锁定)。当资源拥有线程完成时,它只会破坏监视器对象,进而解锁资源。 (这允许将 RAII 应用于所有这些,从而使您的代码安全且可维护。)
  • “但不要为锁定和解锁之间的所有函数调用添加保护。” -- 这个前提是错误的,或者至少非常具有误导性。
  • 我同意。措辞不好。你会如何改写它,所以它更清楚?你明白我想说什么吗?
  • 互斥锁不保护对象。它们保护代码不被同时执行。您希望保护以免彼此同时运行的所有代码片段(即您的案例中的所有关键成员函数)都需要锁定相同的互斥锁。如果这些代码可以相互调用,这并不理想,您需要递归互斥锁或另一层成员函数。显示器可能是更好的解决方案。

标签: c++ multithreading


【解决方案1】:

我对每条评论的建议:

一个更好的习惯用法可能是monitor,它可以保持资源的锁定并提供对所有者的访问权限。要获取资源,reserve() 可以返回这样的监视器对象(类似于访问资源内容的代理)。任何对reserve() 的竞争访问现在都将被阻止(因为互斥锁已被锁定)。当资源拥有线程完成后,它只会破坏监视器对象,进而解锁资源。 (这允许将 RAII 应用于所有这些,从而使您的代码安全且可维护。)

我修改了 OPs 代码来勾勒它的样子:

#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;
}

输出:

Result: 2
Result: -1
Result: 0

Live Demo on coliru

当我知道它总是按该顺序调用reserve和release时,是否可以保护这样的资源?

没有必要再担心这个了。正确用法烧入:

  • 要访问资源,您需要一个监视器。
  • 如果你得到它,你就是资源的唯一所有者。
  • 如果退出范围(将监视器作为局部变量存储的位置),监视器将被销毁,因此锁定的资源会自动释放。

即使出现意外的救助(在 MCVE 中为 throw "Haha!";),后者也会发生。

另外,我做了以下函数private

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

因此,未经授权的访问是不可能的。要正确使用它们,必须获取information_t::Monitor 实例。它为那些可以在监视器所在的范围内使用的函数提供public 包装器,即仅由所有者线程使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多