【问题标题】:How to handle mutexed member functions in C++11如何在 C++11 中处理互斥的成员函数
【发布时间】:2012-10-14 00:14:40
【问题描述】:

这主要是一个设计问题,但由于我使用 C++11 编程,所以我更喜欢使用 C++11 的解决方案/建议。

基本上问题是我想让我的课程易于维护,同时避免使用 过度锁定。

问题很简单:

我可以避免在private 类函数a 的开头锁定,如果它仅由public 类函数b 在开始时锁定类互斥锁。 维护地狱是,将来很难看到什么方法正在调用什么,并且有可能有人会将 b 公开,并使类线程不安全。

那么最优解是什么:

  • 评论b 说它不锁定std::mutex 因为它只被a 调用
  • 使用recursive_mutex并锁定所有方法:似乎很浪费
  • 别的东西

【问题讨论】:

  • 我会说使用recursive_mutex 更安全。您应该只在已经锁定的情况下发生计数器递增/递减,因此如果您已经锁定,浪费可能不是一个大因素。评论合同可能会回来咬你,所以我个人会对此保持警惕。当然,最好的解决方案是通过重新设计来避免锁定(例如,让线程将操作入队到负责对象的线程)。

标签: c++ c++11 thread-safety mutex


【解决方案1】:

有可能有人会将b公开

您不必担心这一点。如果有人在不了解后果或如何正确使用该功能的情况下将私有实现细节变为公共成员,那么您已经有维护的噩梦了。您是否还担心他们会公开您的所有数据成员?担心这些事情似乎是在浪费时间。如果不明显该函数不是公开的,则重命名它并添加明确的 cmets。

然而……

经常使用的另一种选择是要求将锁定对象传递给函数,这样在不锁定互斥锁的情况下调用它会更加困难,无论锁定是由另一个成员函数还是任何其他类完成:

class X
{
public:
    void frobnicate()
    {
        std::unique_lock<std::mutex> lock(this->mutex);
        frob_impl(lock);
    }

private:
    void frob_impl(const std::unique_lock<std::mutex>&)
    {
        // do it
    }

    std::mutex mutex;
};

如果您真的想对人们更改代码做愚蠢的事情感到非常偏执,您可以添加一个检查正确的互斥锁是否已锁定:

    void frob_impl(const std::unique_lock<std::mutex>& lock)
    {
        assert( &this->mutex == lock.mutex() );
        // do it
    }

但是,如果您不信任后来的维护程序员,您怎么知道他们不会删除该检查?

【讨论】:

    【解决方案2】:

    最简单的方法是有一个实现类来完成工作,但没有互斥体成员,以及一个包含实现类和互斥体的包装类。然后包装器将互斥锁锁定在每个成员函数中,然后调用实现类。如果实现调用自身的另一个成员函数,则它知道互斥锁始终处于锁定状态。由于包装器成员函数只是简单的包装器,因此很容易验证它们始终锁定互斥锁。

    class X{
        class Impl{
        private:
            int i,j;
            int bar(){ return i;}
            int baz(){ return j;}
        public:
            Impl():i(36),j(6){}
            int foo(){
                return bar()+baz();
            }
        };
    
        Impl impl;
        std::mutex m;
    public:
        int foo(){
            std::lock_guard<std::mutex> guard(m);
            return impl.foo();
        }
    };
    

    【讨论】:

    • 啊,我希望 cpp 有办法表达这一点,而无需为每个函数编写包装器。就像“类 Wrapper 包装类 Impl,并在它执行的每个方法条目上:std::lock_guard<:mutex> guard(m);”
    • @NoSenseEtAl,看看EXECUTE-AROUND POINTER 模式
    • 酷,现在当我看到它时,它是显而易见的......但我永远不会自己想出来。 :D
    【解决方案3】:

    这是个好问题。将公共接口与实现分开以及将功能与多线程问题分开通常是个好主意。

    我一直在使用递归互斥锁,然后我的方法名称带有和不带有“_locked”后缀等。维护和扩展仍然是一个负担,每次锁定都很慢(递归互斥锁也很烂),而且我经常必须跟踪数据/调用路径才能弄清楚到底发生了什么。好吧,调试死锁很容易——不受保护的访问更有趣。

    这些天来,我通常实现一个类而不用担心互斥体,然后通过将它隐藏在实现为“代理”模式的墙后面来保护它免受“解锁”访问。这对我来说至少工作了几年,到目前为止没有任何抱怨。这段代码应该会给你一个想法:

    #include <cstdio>
    #include <mutex>
    #include <utility>
    
    class SomeClass {
      public:
        explicit SomeClass(int v) : v(v) {}
    
        void foo() { printf("\t\tCALLED foo(%d)\n", v); }
        void bar() { foo(); printf("\t\tCALL bar(%d)\n", v); }
    
      private:
        int v;
    };
    
    template <typename T>
    class Protector {
        std::mutex m_;
        T          c_;
      public:
        template <typename ...Args>
        Protector(Args && ...args)
            : m_(), c_(std::forward<Args>(args)...)
        {}
    
        class Interface {
            Protector *p_;
    
            Interface(const Interface &) = delete;
            Interface & operator = (const Interface &) = delete;
    
          public:
            Interface(Protector *p) : p_(p) {
                printf("\t+++++ Lock! +++++\n");
                p_->m_.lock();
            }
    
            Interface(Interface && rhs) : p_(rhs.p_) { rhs.p_ = nullptr; }
            T *operator->() { return p_ ? &p_->c_ : nullptr; }
            ~Interface() {
                if (p_) {
                    printf("\t----- Unlock! -----\n");
                    p_->m_.unlock();
                }
            }
        };
    
        Interface lock() { return Interface(this); }
        Interface operator ->() { return lock(); }
    
        Protector(const Protector &) = delete;
        Protector & operator = (const Protector &) = delete;
    };
    
    int main()
    {
        Protector<SomeClass> p(12345);
        printf("--*-- Doing batch access! --*--\n");
        {
            auto c = p.lock();
            c->foo();
            c->bar();
        }
        printf("--*-- Doing silly access! --*--\n");
        p->foo();
        p->bar();
        printf("Done!\n");
    }
    

    示例运行:

    $ clang++ -std=c++11 -stdlib=libc++ -O4 -Wall -pedantic -o test ./test.cpp 
    $ ./test 
    --*-- Doing batch access! --*--
        +++++ Lock! +++++
            CALLED foo(12345)
            CALLED foo(12345)
            CALL bar(12345)
        ----- Unlock! -----
    --*-- Doing silly access! --*--
        +++++ Lock! +++++
            CALLED foo(12345)
        ----- Unlock! -----
        +++++ Lock! +++++
            CALLED foo(12345)
            CALL bar(12345)
        ----- Unlock! -----
    Done!
    $ 
    

    希望对你有帮助。

    【讨论】:

      猜你喜欢
      • 2012-03-27
      • 1970-01-01
      • 1970-01-01
      • 2022-01-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-10
      • 2014-09-19
      相关资源
      最近更新 更多