Эта конструкция, вероятно, будет хорошо, если выполняются следующие условия:
- читает будет гораздо более распространены, чем пишет
- ряд чтений будут перемежаться между пишет
- (возможно) пишет будет влияют только на небольшую часть глобальной структуры данных
Конечно, учитывая эти условия, практически любая система параллелизма будет в порядке. Поскольку вы беспокоитесь о livelock, я подозреваю, что вы имеете дело с более сложным шаблоном доступа. В этом случае, читайте дальше.
Ваш дизайн, как представляется, руководствуясь следующей цепочкой рассуждений:
atomicModifyIORef
очень дешево, потому что он просто создает санки
потому что atomicModifyIORef
дешево, это не будет вызывать нить
Недорогой доступ к данным + без конкуренции = параллелизм FTW!
Вот недостающий шаг в этом рассуждении: ваши IORef
модификации только создавать санки, и вы не имеете никакого контроля над тем, где санками оцениваются. Если вы не можете контролировать, где данные оцениваются, у вас нет реального параллелизма.
Поскольку вы еще не представили предполагаемые шаблоны доступа к данным, это предположение, однако я ожидаю, что произойдет то, что ваши повторные модификации данных будут создавать цепочку thunks. Затем в какой-то момент вы будете читать данные и заставлять оценивать, заставляя все эти thunks оцениваться последовательно в одном потоке. На этом этапе вы можете также написать однопоточный код.
Путь к этому заключается в том, чтобы ваши данные были оценены (по крайней мере, насколько это было бы желательно), прежде чем они будут записаны в IORef. Это и есть возвращаемый параметр atomicModifyIORef
.
Рассмотрим эти функции, предназначенные для изменения aVar :: IORef [Int]
doubleList1 :: [Int] -> ([Int],())
doubleList1 xs = (map (*2) xs,())
doubleList2 :: [Int] -> ([Int], [Int])
doubleList2 xs = let ys = map (*2) xs in (ys,ys)
doubleList3 :: [Int] -> ([Int], Int)
doubleList3 xs = let ys = map (*2) xs in (ys, sum ys)
Вот что происходит, когда вы используете эти функции в качестве аргументов:
!() <- atomicModifyIORef aVar doubleList1
- только преобразователь создан, никакие данные не оценены. Неприятный сюрприз для любой темы, прочитанной от aVar
.
!oList <- atomicModifyIORef aVar doubleList2
- новый список оценивается только до сих пор, чтобы определить начальный конструктор, то есть (:)
или []
. Все еще не было сделано никакой реальной работы.
!oSum <- atomicModifyIORef aVar doubleList3
- оценивая сумму списка, это гарантирует, что вычисление будет полностью оценено.
В первых двух случаях выполняется очень мало работы, поэтому atomicModifyIORef
быстро выйдет. Но эта работа не была выполнена в этой теме, и теперь вы не знаете, когда это произойдет.
В третьем случае вы знаете, что работа была выполнена в заданной резьбе. Сначала создается thunk и обновляется IORef, затем поток начинает оценивать сумму и, наконец, возвращает результат. Но предположим, что какой-то другой поток считывает данные во время вычисления суммы. Он может начать оценивать сам кузнец, и теперь у вас есть два потока, делающих дублирующую работу.
Вкратце, этот дизайн ничего не решил. Скорее всего, он будет работать в ситуациях, когда проблемы с параллелизмом не были трудными, но для экстремальных случаев, которые вы рассматривали, вы все равно собираетесь записывать циклы с несколькими потоками, делающими дублируемую работу. И в отличие от STM, у вас есть нет управления над тем, как и когда повторить попытку. По крайней мере, STM вы можете прервать в середине транзакции, с оценкой thunk это полностью из ваших рук.
Почему бы вам просто не использовать STM? Это намного проще и работает очень хорошо. –
Почему STM проще, чем IORef? Не нужно беспокоиться о livelock с IORef, что делает IORef проще? – Clinton
Я уверен, что STM не ведет к живым, но я могу ошибаться. –