Существует ли шаблон проектирования C++, который реализует механизм или мьютекс, который контролирует количество времени, которое поток может владеть заблокированным ресурсом?

Я ищу способ гарантировать, что каждый раз, когда поток блокирует определенный ресурс, он вынужден освобождать этот ресурс через определенный период времени (если он еще не освободил его). Представьте себе соединение, в котором вам нужно ограничить время, в течение которого конкретный поток может владеть этим соединением.

Я представляю, как это можно использовать:

{
    std::lock_guard<std::TimeLimitedMutex> lock(this->myTimeLimitedMutex, timeout);
    try {
        // perform some operation with the resource that myTimeLimitedMutex guards. 
    }
    catch (MutexTimeoutException ex) {
        // perform cleanup
    }
}

Я вижу, что есть timed_mutex, который позволяет программе тайм-аут, если блокировка не может быть получена. Мне нужно, чтобы тайм-аут произошел после того, как блокировка получена.

Уже есть некоторые ситуации, когда вы получаете ресурс, который можно неожиданно забрать. Например, сокеты tcp - как только соединение сокета установлено, код на каждой стороне должен обработать случай, когда другая сторона разрывает соединение.

Я ищу шаблон, который обрабатывает типы ресурсов, которые обычно имеют тайм-аут самостоятельно, но когда они этого не делают, они должны быть сброшены. Это не должно обрабатывать каждый тип ресурса.

Ответ 1

Это не может работать, и это никогда не будет работать. Другими словами, это никогда не может быть сделано. Это идет вразрез со всей концепцией собственности и атомных транзакций. Потому что, когда поток получает блокировку и реализует две транзакции подряд, он ожидает, что они станут атомно видимыми для внешнего слова. В этом случае вполне возможно, что транзакция будет разорвана - первая ее часть будет выполнена, а вторая - нет.

Что еще хуже, поскольку блокировка будет принудительно удалена, частично выполненная транзакция станет видимой для внешнего слова, прежде чем прерванный поток сможет откатиться.

Эта идея идет вразрез со всей школой многопоточного мышления.

Ответ 2

Я поддерживаю Sergey как ответ. Освобождение заблокированного мьютекса после истечения времени ожидания является плохой идеей и не может работать. Mutex выступает за взаимное исключение, и это трудный контракт, который нельзя нарушать.

Но вы можете делать то, что вы хотите:

Проблема: Вы хотите гарантировать, что ваши потоки не удерживают мьютекс дольше, чем определенное время T.

Решение: Никогда не блокируйте мьютекс дольше, чем время Т. Вместо этого напишите свой код так, чтобы мьютекс блокировался только для абсолютно необходимых операций. Всегда можно дать такое время T (по модулю неопределенности и пределов, заданных моей многозадачной и многопользовательской операционной системой, конечно).

Для этого (примеры):

  • Никогда не делайте файловый ввод/вывод внутри заблокированного раздела.
  • Никогда не вызывайте системный вызов, пока мьютекс заблокирован.
  • Избегайте сортировки списка, когда мьютекс заблокирован (*).
  • Избегайте медленных операций с каждым элементом списка, когда мьютекс заблокирован (*).
  • Избегайте выделения/освобождения памяти, пока мьютекс заблокирован (*).

Есть исключения из этих правил, но общее руководство:

  • Сделайте ваш код немного менее оптимальным (например, сделайте некоторое избыточное копирование внутри критической секции), чтобы сделать критическую секцию максимально короткой. Это хорошее многопоточное программирование.

(*) Это просто примеры для операций, когда соблазнительно заблокировать весь список, выполнить операции, а затем разблокировать список. Вместо этого рекомендуется просто взять локальную копию списка и очистить исходный список, пока мьютекс заблокирован, в идеале с помощью операции swap() предлагаемой большинством контейнеров STL. А затем выполните медленную операцию с локальной копией вне критического раздела. Это не всегда возможно, но всегда стоит учитывать. В худшем случае сортировка имеет квадратную сложность и обычно требует произвольного доступа ко всему списку. Полезно отсортировать (копию) списка за пределами критического раздела, а затем проверить, нужно ли добавлять или удалять элементы. Распределение памяти также имеет некоторые сложности, поэтому следует избегать массового выделения/освобождения памяти.

Ответ 3

Вы не можете сделать это только с C++.

Если вы используете систему Posix, это можно сделать. Вам нужно будет активировать сигнал SIGALARM, который будет разоблачен только для потока, для которого истечет время ожидания. В обработчике сигнала вам нужно установить флаг и использовать longjmp для возврата к коду потока. В коде потока в позиции setjmp вы можете вызываться только в том случае, если сигнал сработал, поэтому вы можете вызвать исключение Timeout.

Пожалуйста, посмотрите этот ответ, чтобы узнать, как это сделать.

Кроме того, в Linux, похоже, вы можете напрямую генерировать обработчик сигнала (поэтому здесь нет longjmp/setjmp).

Кстати, на вашем месте, я бы закодировал обратное. Подумайте об этом: вы хотите сказать ветке: "Эй, ты слишком долго, так что давай отбросим всю (долгую) работу, которую ты проделал, чтобы я мог прогрессировать". В идеале, вы должны сделать свой длинный поток более совместным, выполняя что-то вроде: "Я сделал A задачи ABCD, позвольте освободить мьютекс, чтобы другие могли прогрессировать на A. Затем позвольте проверить, могу ли я взять его снова, чтобы выполнить B и скоро." Вы, вероятно, хотите быть более мелкозернистыми (иметь больше мьютекса на меньших объектах, но убедитесь, что вы блокируете в том же порядке) или использовать блокировки RW (чтобы другие потоки могли использовать объекты, если вы их не модифицируете), так далее...

Ответ 4

Такой подход не может быть реализован, потому что владельцу мьютекса нужна возможность очистить все, что осталось в недопустимом состоянии в процессе транзакции. Это может занять неизвестное произвольное количество времени.

Типичный подход состоит в том, чтобы снять блокировку при выполнении длинных задач и повторно вызвать ее при необходимости. Вы должны справиться с этим самостоятельно, так как у всех будет немного другой подход.

Единственная известная мне ситуация, когда подобные вещи принимаются на практике, - это на уровне ядра, особенно в отношении микроконтроллеров (которые либо не имеют ядра, либо являются ядром, в зависимости от того, кого вы спрашиваете). Вы можете установить прерывание, которое изменяет стек вызовов, так что при его запуске он раскручивает конкретные интересующие вас операции.

Ответ 5

Переменные "условия" могут иметь тайм-ауты. Это позволяет вам ждать, пока поток добровольно не освободит ресурс (с помощью notify_one() или notify_all()), но само ожидание будет превышено по истечении указанного фиксированного промежутка времени.

Примеры в документации Boost для "условий" могут прояснить это.

Если вы хотите форсировать релиз, вы должны написать код, который форсирует его. Это может быть опасно. Код, написанный на C++, может выполнять довольно близкие к делу вещи. Ресурс может иметь доступ к реальному оборудованию, и он может ждать, пока он что-то завершит. Может быть физически невозможно закончить то, на чем застряла программа.

Однако, если это возможно, вы можете обработать его в потоке, в котором время ожидания() истекло.