组成员函数都需要先隐式互斥锁吗?

时间:2019-04-22 20:14:17

标签: c++ function mutex functor simplify

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

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()调用对其中的所有功能都是“隐式”的群组。

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

有什么建议吗?

1 个答案:

答案 0 :(得分:4)

首先,如果互斥锁必须在执行其他任何操作之前已被锁定,则应致电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 构建一个反映多个线程应该协作方式的抽象。其他一切仅次于此。但是,在不了解您的应用程序实际是什么的情况下,实际上并没有什么可以说的……