Отсутствие справедливости в STM, почему заблокированные потоки не могут быть разбужены в порядке FIFO?

#haskell #concurrency #stm

#haskell #параллелизм #stm

Вопрос:

Я пересматриваю главу STM книги Марлоу. Там указано, что:

Когда несколько потоков блокируются на an MVar , они гарантированно будут разбужены в порядке FIFO

Однако то же самое невозможно сделать в случае STM:

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

Чего я не понимаю, так это почему из этого следует, что

Поскольку среда выполнения должна запускать все заблокированные транзакции, нет гарантии, что потоки будут разблокированы в порядке FIFO …

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

Комментарии:

1. Мы, конечно, можем разбудить потоки в порядке FIFO, но это не гарантирует, что они будут завершены в том же порядке. Более ранний поток может снова заблокироваться, а более поздний завершится.

Ответ №1:

Смысл STM в том, чтобы размышлять: мы пытаемся выполнить все транзакции, надеясь, что они не конфликтуют друг с другом (или выполняют a retry ). Когда мы обнаруживаем конфликт, мы разрешаем фиксацию некоторых транзакций, а конфликтующие транзакции откатываются.

Мы могли бы запустить только одну транзакцию, дождаться ее завершения или блокировки, а затем запустить другую и так далее, Но это означало бы использование одной «глобальной блокировки», что сделало бы вычисления полностью последовательными.

Более технически, когда потоки ожидают a MVar , эти потоки будут выполняться при очень простом условии: MVar становиться непустыми. При пробуждении поток примет значение, сделав его пустым. Таким образом, не более одного потока может выполнить take, и нет смысла пробуждать более одного.

По определению, когда потоки ожидают из-за STM, условие намного сложнее. Предположим, что они ожидают, потому что ранее выполняли a retry , поэтому они ожидают изменения некоторых ранее прочитанных TVar . Когда это происходит, мы не можем точно знать, какой поток снова заблокируется, если мы не повторим его транзакцию. В отличие MVar от этого, теперь возможно, что их пробуждение приведет к тому, что все они завершатся без конфликтов, поэтому мы пытаемся сделать именно это. При этом мы надеемся, что многие (если не все) завершат работу, и снова подготовимся к откату для тех, кто этого не сделает.

Рассмотрим этот конкретный пример STM:

 Thread 1:
read X
if X is zero, retry
compute f(X)
write Y = f(X)

Thread 2:
read X
if X is zero, retry
compute g(X)
write Z = g(X)
 

Предположим, что в начале у нас есть «X = Y = Z = 0». Мы запускаем оба потока, но они повторяют попытку. Затем пишет третий поток X=1 . Мы можем разбудить оба, и нет смысла делать это в порядке FIFO, поскольку оба завершатся. Мы можем и должны вычислять f(X) и g(X) параллельно.

Теперь, если поток 2 записал Y вместо Z в конце, был бы конфликт. В этом случае последовательное выполнение транзакций было бы более эффективным (поскольку в противном случае мы вычисляем f(X) или g(X) дважды). Однако в общем случае трудно предсказать, возникнет конфликт или нет. STM не пытается, предоставляя программисту оптимизировать свой код.

Ответ №2:

Когда куча потоков ожидает получения из an MVar и кто-то заполняет его, может выполняться только один из ожидающих потоков. В этом весь смысл. Тот, который просыпается, (предположительно) разбудит еще один поток после того, как он выполнит свою задачу и заполнит MVar в свою очередь. Поэтому имеет смысл иметь четко определенный порядок, в котором официанты получат свою очередь.

TVar s — это не MVar s. У них нет дихотомии пустой / зарезервированный против полного / доступного. Это просто значения, к которым можно получить доступ и / или изменить транзакциями. Потоки не «блокируют ожидание TVar «; они просто считывают текущее значение для TVar , а затем решают, что они не могут добиться прогресса с этим значением (и так retry далее ).

Система времени выполнения знает, к каким TVar s обратился поток, поэтому она знает, что не следует снова пробуждать этот поток, если хотя бы один из них не TVar изменился; при чистом вычислении решение потока retry о том, должен он быть или нет, должно быть чистой функцией всех TVar прочитанных им s, поэтому мы знаем, что это не может сделатьпрогресс, пока один из них не изменится.

Но когда у вас есть куча ожидающих потоков retry , которые все читают одинаково TVar , и это меняется, система выполнения не хочет просто разбудить один. Нет оснований полагать, что только один из них сможет добиться прогресса с новым значением. Предполагается, что транзакции STM должны работать параллельно, оптимистично. Мы хотим разбудить их всех и позволить им попытаться запустить. Если они конфликтуют, мы выясним это позже, с помощью обычных средств обнаружения конфликта между транзакциями STM; мы не можем сказать это из того факта, что все они хотели прочитать одно и то же TVar .

Потенциально мы могли бы разбудить их все одновременно и (если их слишком много для доступных вычислительных единиц) попытаться убедиться, что планировщик запускает их в порядке FIFO. Может быть, это даже сделано; Я понятия не имею. Но я бы предположил, что они обрабатываются точно так же, как и любые другие потоки, которые стали доступны для планирования, и никаких специальных действий не предпринимается. Все они «должны» выполняться параллельно, поэтому не должно иметь значения, какой из них выходит из ворот первым.