Для всех функций членов группы требуется неявная блокировка мьютекса? - PullRequest
0 голосов
/ 22 апреля 2019

У меня есть класс «Device», представляющий подключение периферийного аппаратного устройства. Множество функций-членов («функций устройства») вызываются клиентами для каждого объекта 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 Ответ

4 голосов
/ 22 апреля 2019

Прежде всего, если мьютекс должен быть заблокирован, прежде чем делать что-либо еще, вы должны позвонить 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 абстракцию для поддержки этого варианта использования? Нет ли действительно лучшего интерфейса, который вы могли бы спроектировать для своего сценария приложения, зная о том, что на самом деле должны делать потоки. Вы действительно должны сделать такую ​​мелкозернистую блокировку? Подумайте, насколько неэффективно с вашей текущей абстракцией, например, вызывать несколько функций устройства подряд, поскольку это требует постоянной блокировки и разблокировки и блокировки и разблокировки этого мьютекса снова и снова повсюду & hellip;

С учетом всего вышесказанного может быть способ улучшить частоту блокировки, одновременно решая ваш первоначальный вопрос:

Мне интересно, существует ли конструкция 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), и вы также никогда не забуду разблокировать мьютекс & hellip;

Однако даже при этом самое важное - это не просто создать общий "потокобезопасный Device". Эти вещи обычно ни эффективны, ни действительно полезны. Создайте абстракцию, которая отражает способ взаимодействия нескольких потоков, используя Device в вашем конкретном приложении . Все остальное является вторым после этого. Но, ничего не зная о том, чем на самом деле является ваше приложение, на самом деле больше ничего нельзя сказать об этом & hellip;

...