#c #function #mutex #functor #simplify
#c #функция #мьютекс #функтор #упростить
Вопрос:
У меня есть класс «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:
Прежде всего, если мьютекс должен быть заблокирован, прежде чем вы сделаете что-либо еще, тогда вы должны вызвать 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_mutexamp; 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_mutexamp; 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
{
Deviceamp; device;
the_locking_dance guard;
public:
DeviceFunctionSet1(Deviceamp; device)
: device(device), guard(device.mutex_)
{
}
void DeviceFunction1() { device.DeviceFunction1(); }
void DeviceFunction2() { device.DeviceFunction2(); }
};
class DeviceFunctionSet2
{
Deviceamp; device;
the_locking_dance guard;
public:
DeviceFunctionSet2(Deviceamp; 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
в вашем конкретном приложении. Все остальное на втором месте. Но, не зная ничего о том, что на самом деле представляет собой ваше приложение, на самом деле больше ничего нельзя сказать по этому поводу…
Комментарии:
1. Спасибо, Майкл! Да, я использовал
mutex_.try_lock()
в качестве сокращенного обозначения для всех вызовов, связанных с мьютексом, в начале функций устройства… включая, как вы упомянули, lock_guard и обработку исключений при получении времени ожидания блокировки.2. Шаблон проектирования, который вы рекомендуете, интересен — я постараюсь лучше понять его и посмотреть, работает ли он для меня в этом случае. К сожалению, методы устройства доступны (через интерфейс-оболочку C, о котором я не упоминал) внешним пользователям / заказчикам, поэтому я не могу контролировать, поступают ли вызовы из разных параллельных потоков. Мои мьютексы — это попытка предотвратить неправильное использование пользователем API и сбой приложения или подключенного устройства.
3. Учитывая требования моего базового приложения (фактически, в данном случае dll) к 1) предоставлению доступа к различным функциям устройства пользователю API и 2) обеспечению того, чтобы пользователь не мог одновременно вызывать две или более функций, которые обращаются к устройству, можете ли вы порекомендовать какие-либо абстракции, которые были бы более эффективными или полезными? (Я относительно новый программист на C , и мне интересно использовать эту конкретную возможность, чтобы узнать больше об общих шаблонах проектирования в целом). Еще раз спасибо за вашу помощь!
4. @NKatUT Ну, я действительно недостаточно знаю о вашей конкретной ситуации, чтобы звонить сюда. Я бы, вероятно, попытался просто определить несинхронизированное одновременное использование API на том же устройстве как неопределенное поведение. Низкоуровневый API не должен нести ответственность за защиту пользователя от неправильного использования API ценой пессимизации того, что может быть достигнуто с помощью правильного использования API … это противоречило бы основополагающим принципам C , а также C…
5. Еще раз спасибо, Майкл. Очень ценится. Я предполагаю, что я чрезмерно обеспокоен тем, чтобы пользователь не делал ничего «плохого», и должен полагаться на то, что они повторно читают документацию. безопасность потоков (un).