Как именно работает функция ожидания (в отношении переменных условия)
Я немного запутался в своем понимании того, как работают переменные условия (в отношении одновременного доступа к общим данным)
Ниже приведен псевдо-код C для иллюстрации моей текущей проблемы.
// Thread 1: Producer
void cakeMaker()
{
lock(some_lock);
while(number_of_cakes == MAX_CAKES)
wait(rack_has_space);
number_of_cakes++;
signal(rack_has_cakes);
unlock(some_lock);
}
// Thread 2: Consumer
void cakeEater()
{
lock(some_lock);
while(number_of_cakes == 0)
wait(rack_has_cakes);
number_of_cakes--;
signal(rack_has_space);
unlock(some_lock);
}
Допустим, у нас в настоящее время есть number_of_cakes = 0
, так Thread 2
в настоящее время застрял на wait(rack_has_cakes)
, Thread 1
пробеги и приращения number_of_cakes
на 1. Затем он вызывает signal(rack_has_cakes)
- это просыпается Thread 2
, к несчастью Thread 2
просыпается раньше Thread 1
звонки unlock(some_lock)
, так что он снова ложится спать, и сигнал был пропущен.
Я очень смущен внутренней работой wait
а также signal
? Что именно с ними происходит? Они похожи на некоторый логический флаг, который автоматически устанавливается в 1, когда мы вызываем сигнал, и снова устанавливается в 0, когда ожидание успешно? Что здесь происходит?
Может ли кто-нибудь провести меня по шагам 1 итерации вышеприведенного кода, уделив особое внимание тому, что происходит во время сигнала и ожидания?
2 ответа
поток 2 просыпается до того, как поток 1 вызывает unlock(some_lock), поэтому он снова переходит в спящий режим, и сигнал пропускается.
Нет, это не так. Я буду использовать C++ std::condition_variable
для моего цитирования, но потоки POSIX и большинство заурядных реализаций мьютексов и условных переменных работают одинаково. Основные понятия одинаковы.
Поток 2 заблокировал мьютекс, когда он начинает ожидать переменную условия. Операция wait() разблокирует мьютекс и атомарно ожидает переменную условия:
Атомно снимает блокировку, блокирует текущий исполняющий поток и добавляет его в список потоков, ожидающих *this.
Эта операция считается "атомарной"; другими словами, неделимый.
Затем, когда передается переменная условия, поток повторно блокирует мьютекс:
Когда разблокировано, независимо от причины, блокировка возобновляется и ожидание завершается.
Поток не "возвращается в спящий режим" до того, как другой поток "вызывает разблокировку". Если мьютекс еще не был разблокирован: когда поток просыпается после того, как на него указывает переменная условия, поток всегда будет ждать, пока ему не удастся снова заблокировать мьютекс. Это безоговорочно. когда wait()
возвращает мьютекс все еще заблокирован. Тогда и только тогда wait()
функция возвращает. Итак, последовательность событий:
В одном потоке заблокирован мьютекс, он устанавливает некоторый счетчик, переменную или любой другой тип данных, защищенных мьютексом, в состояние, которое ожидает другой поток. После этого поток сигнализирует переменную условия, а затем разблокирует мьютекс в свободное время.
Другой поток заблокировал мьютекс перед
wait()
s на условной переменной. Один изwait()
обязательным условием является то, что мьютекс должен быть заблокирован доwait()
на связанной переменной условия. Таким образом, операция wait() разблокирует мьютекс "атомарно". То есть, нет никакого случая, когда мьютекс разблокирован, и поток еще не ожидает переменную условия. когдаwait()
разблокирует мьютекс, вам гарантированно, что поток будет ждать, и он проснется. Вы можете взять его в банк.Как только переменная условия будет сигнализирована,
wait()
Эта нить не возвращается изwait()
пока он не сможет повторно заблокировать мьютекс. После получения сигнала от условной переменной это только первый шаг, мьютекс должен быть снова заблокирован потоком на последнем шагеwait()
операция. Что, конечно, происходит только после того, как сигнальный поток разблокирует мьютекс.
Когда поток получает сигнал с помощью условной переменной, он возвращается из wait()
, Но не сразу, он должен ждать, пока поток снова не заблокирует мьютекс, сколько бы времени это ни заняло. Он не будет "возвращаться ко сну", но подождет, пока мьютекс снова не заблокируется, а затем вернется. Вам гарантировано, что полученный сигнал переменной условия приведет к тому, что поток вернется из wait()
и мьютекс будет повторно заблокирован потоком. А поскольку исходная операция разблокировки-затем-ожидания была атомарной, вы гарантированно получите сигнал переменной условия.
Предположим, что в настоящее время у нас есть number_of_cakes = 0, поэтому поток 2 в настоящее время застрял в ожидании (rack_has_cakes). Поток 1 запускается и увеличивает number_of_cakes на 1. Затем он вызывает сигнал (rack_has_cakes) — это пробуждает поток 2, к сожалению, поток 2 просыпается до того, как поток 1 вызывает разблокировку (some_lock), поэтому он снова переходит в спящий режим, и сигнал был пропущен. .
Вы правы, это может случиться, потому что ваш порядок команд сигналов был неправильным. И в Producer, и в Consumer вы установили следующий порядок команд:
signal(rack_has_cakes);
unlock(some_lock);
Но порядок должен быть:
unlock(some_lock);
signal(rack_has_cakes);
Сначала вы должны разблокировать мьютекс, а затем сигнализировать другому потоку. Поскольку команда signal является условной переменной wait(), а команды signal() являются потокобезопасными, вам не следует беспокоиться о снятии блокировки раньше.
Но этот шаг очень важен, так как он дает другому потоку возможность заблокировать мьютекс.