【问题标题】:Group member functions to all require implicit mutex lock first?组成员函数都需要先隐式互斥锁?
【发布时间】:2019-04-22 20:14:17
【问题描述】:

我有一个表示外围硬件设备连接的“设备”类。客户端在每个设备对象上调用许多成员函数(“设备函数”)。

class Device {
public:
    std::timed_mutex mutex_;

    void DeviceFunction1();
    void DeviceFunction2();
    void DeviceFunction3();
    void DeviceFunction4();
    // void DeviceFunctionXXX();  lots and lots of device functions

    // other stuff 
    // ...
};

Device 类有一个成员 std::timed_mutex mutex_ 在与设备通信之前必须由每个设备函数锁定,以防止同时从并发线程与设备通信。

一种明显但重复且麻烦的方法是在每个设备函数的执行顶部复制/粘贴mutex_.try_lock() 代码。

void Device::DeviceFunction1() {
    mutex_.try_lock();        // this is repeated in ALL functions

    // communicate with device
    // other stuff 
    // ...
 }

但是,我想知道是否有 C++ 构造或设计模式或范例可用于“分组”这些函数,使得 mutex_.try_lock() 调用对于组中的所有函数都是“隐式”的.

换句话说:类似于派生类可以在基类构造函数中隐式调用公共代码的方式,我想对函数调用做类似的事情(而不是类继承)。

有什么建议吗?

【问题讨论】:

    标签: c++ function mutex functor simplify


    【解决方案1】:

    首先,如果互斥体必须在你做任何其他事情之前被锁定,那么你应该调用mutex_.lock(),或者至少不要忽略try_lock实际上可能无法锁定的事实互斥体。此外,手动调用来锁定和解锁互斥体非常容易出错,并且比您想象的要正确得多。不要这样做。改用std::lock_guard

    您使用std::timed_mutex 的事实表明您的真实代码中实际发生的事情可能会涉及更多(否则您将使用std::timed_mutex 做什么)。假设您真正在做的事情比仅仅调用 try_lock 并忽略其返回值更复杂,请考虑将您的复杂锁定过程(无论它可能是什么)封装在自定义锁定保护类型中,例如:

    class the_locking_dance
    {
        auto do_the_locking_dance(std::timed_mutex& mutex)
        {
            while (!mutex.try_lock_for(100ms))
                /* do whatever it is that you wanna do */;
            return std::lock_guard { mutex, std::adopt_lock_t };
        }
    
        std::lock_guard<std::timed_mutex> guard;
    
    public:
        the_locking_dance(std::timed_mutex& mutex)
            : guard(do_the_locking_dance(mutex))
        {
        }
    };
    

    然后创建一个局部变量

    the_locking_dance guard(mutex_);
    

    获取并持有您的锁。这也将在退出块时自动释放锁。

    除此之外,请注意,您在这里所做的一般来说很可能不是一个好主意。真正的问题是:为什么有这么多不同的方法都需要由同一个互斥锁开始保护?你真的必须支持任意数量的你一无所知的线程,这些线程可以在任意时间以任意顺序对同一个设备对象做任意事情吗?如果不是,那你为什么要构建你的Device 抽象来支持这个用例?真的没有更好的界面可以为您的应用程序场景设计,知道线程应该做什么。你真的需要做这种细粒度的锁定吗?考虑一下你当前的抽象是多么低效,例如,连续调用多个设备函数,因为这需要不断地锁定和解锁以及在整个地方一次又一次地锁定和解锁这个互斥体......

    话虽如此,可能有一种方法可以提高锁定频率,同时解决您最初的问题:

    我想知道是否有 C++ 构造或设计模式或范例可用于“分组”这些函数,使 mutex_.try_lock() 调用对于组中的所有函数都是“隐式”的。

    您可以将这些函数分组,方法是不将它们直接公开为 Device 对象的方法,而是作为另一种锁保护类型的方法,例如

    class Device
    {
        …
    
        void DeviceFunction1();
        void DeviceFunction2();
        void DeviceFunction3();
        void DeviceFunction4();
    
    public:
        class DeviceFunctionSet1
        {
            Device& device;
            the_locking_dance guard;
    
        public:
            DeviceFunctionSet1(Device& device)
                : device(device), guard(device.mutex_)
            {
            }
    
            void DeviceFunction1() { device.DeviceFunction1(); }
            void DeviceFunction2() { device.DeviceFunction2(); }
        };
    
        class DeviceFunctionSet2
        {
            Device& device;
            the_locking_dance guard;
    
        public:
            DeviceFunctionSet2(Device& device)
                : device(device), guard(device.mutex_)
            {
            }
    
            void DeviceFunction3() { device.DeviceFunction4(); }
            void DeviceFunction4() { device.DeviceFunction3(); }
        };
    };
    

    现在,要在给定块范围内访问您设备的方法,您首先获取相应的DeviceFunctionSet,然后您可以调用这些方法:

    {
        DeviceFunctionSet1 dev(my_device);
    
        dev.DeviceFunction1();
        dev.DeviceFunction2();
    }
    

    这样做的好处是锁定会自动为整个函数组发生一次(希望它们在逻辑上属于一组函数,用于通过您的Device 完成特定任务),然后您也永远不会忘记解锁互斥锁……

    不过,即便如此,最重要的是不要仅仅构建一个通用的“线程安全Device”。这些东西通常既没有效率也没有真正有用。 在您的特定应用程序中构建一个抽象,以反映多个线程应该使用Device 协作的方式。其他一切都是其次的。但是在不知道您的应用程序实际上是什么的情况下,没有什么可以说的了……

    【讨论】:

    • 谢谢迈克尔!是的,我在设备函数开始时使用mutex_.try_lock() 作为所有与互斥锁相关的调用的简写...包括你提到的 lock_guard 和获取锁超时时的异常处理。
    • 你推荐的设计模式很有趣——我会试着更好地理解它,看看它在这种情况下是否适合我。不幸的是,设备方法(通过我没有提到的 C 包装器接口)暴露给外部用户/客户,所以我无法控制调用是否来自不同的并发线程。我的互斥锁是为了防止用户滥用 API 并使应用程序或连接的设备崩溃。
    • 鉴于我的基本应用程序(实际上是 dll)要求 1) 向 API 的用户提供对各种设备功能的访问,以及 2) 确保用户不能同时调用两个或更多访问设备的功能,您能推荐任何更有效或更有用的抽象吗? (我是一个相对较新的 C++ 程序员,并且有兴趣利用这个特定的机会来了解更多关于一般设计模式的信息)。再次感谢您的帮助!
    • @NKatUT 好吧,我不太了解您的具体情况,无法在这里打电话。我可能会寻求简单地将同一设备上 API 的非同步并发使用定义为未定义的行为。低级 API 的责任不应该是保护用户不正确使用 API,代价是对通过正确使用 API 可以实现的目标感到悲观……这将违背 C++ 的基本原则,并且C…
    • 再次感谢迈克尔。非常感激。我想我过分担心防止用户做任何“坏事”,应该依靠他们阅读文档。线程(非)安全。
    猜你喜欢
    • 1970-01-01
    • 2012-02-23
    • 1970-01-01
    • 1970-01-01
    • 2011-08-22
    • 1970-01-01
    • 1970-01-01
    • 2012-03-25
    • 2010-09-12
    相关资源
    最近更新 更多