Por exemplo, considere esta função, que poderia ser usada em um WM para permitir mover uma janela de uma área de trabalho para outra em uma determinada tela,
moveWindowSTM :: Display -> Window -> Desktop -> Desktop -> STM ()
moveWindowSTM disp win a b = do
wa <- readTVar ma
wb <- readTVar mb
writeTVar ma (Set.delete win wa)
writeTVar mb (Set.insert win wb)
where
ma = disp ! a
mb = disp ! b
e obviamente seu IO
invólucro,
moveWindow :: Display -> Window -> Desktop -> Desktop -> IO ()
moveWindow disp win a b = atomically $ moveWindowSTM disp win a b
e então assumir que
- nossa transação é bem-sucedida na terceira tentativa,
- porque na primeira tentativa ele é invalidado por outra transação simultânea sendo confirmada que altera o valor interno
ma
logo após o términowa <- readTVar ma
da nossa transação, - e na segunda tentativa ele é invalidado por outra transação simultânea que altera o conteúdo de uma ou ambas as transações
ma
emb
logo depoiswriteTVar ma (Set.delete win wa)
da nossa transação.
Como o log de transações evoluiria nesse caso e em que ponto a validação falharia?
O trecho relevante de Parallel and Concurrent Programming in Haskell de Simon Marlow está abaixo (mas informações semelhantes estão disponíveis no artigo Beautiful concurrency de Simon Peyton Jones):
Uma transação STM funciona acumulando um log de
readTVar
operaçõeswriteTVar
que ocorreram até o momento durante a transação. O log é usado de três maneiras:
Ao armazenar
writeTVar
as operações no log em vez de aplicá-las imediatamente à memória principal, descartar os efeitos de uma transação é fácil; simplesmente descartamos o log. Portanto, abortar uma transação tem um custo fixo pequeno.Cada um
readTVar
deve percorrer o log para verificar se oTVar
foi escrito por umwriteTVar
. Portanto,readTVar
é uma operação O(n) no comprimento do log.Como o log contém um registro de todas as
readTVar
operações, ele pode ser usado para descobrir o conjunto completo deTVars
leituras durante a transação, o que precisamos saber para implementarretry
.Quando uma transação chega ao fim, a implementação do STM compara o log com o conteúdo da memória. Se o conteúdo atual da memória corresponder aos valores lidos por
readTVar
, os efeitos da transação são confirmados na memória; caso contrário, o log é descartado e a transação é executada novamente desde o início. Esse processo ocorre atomicamente, bloqueando todos osTVars
envolvidos na transação durante sua duração. A implementação do STM no GHC não utiliza bloqueios globais; apenas osTVars
envolvidos na transação são bloqueados durante o commit, de modo que transações que operam em conjuntos disjuntos deTVars
podem prosseguir sem interferência.
Uma transação STM é executada completamente, registrando cada evento em um log, e a consistência é verificada no final para decidir se a transação deve ser confirmada ou não.
Então, para o seu exemplo de transação
Cada tentativa é executada uma
readTVar
únicawriteTVar
vez. Essas funções gravam em um log, que registra se cada evento é uma leitura ou gravação, o TVar afetado e o valor lido ou gravado.Durante essa transação, outras threads podem confirmar transações que gravam nos mesmos TVars. A transação atual continua, independentemente disso.
Quando
atomically
termina de executar uma transação, ele bloqueia todos os TVars que aparecem em seu log para garantir que o que se segue aconteça atomicamente:atomically
verifica se os valores lidos no log correspondem aos valores atuais nos TVars, o que significa que eles não foram substituídos (ou outra transação escreveu o mesmo valor no TVar, o que é bom).Se o log corresponder aos TVars, os eventos de gravação serão aplicados aos respectivos TVars. É isso que significa "commit" a transação. Os bloqueios são liberados e
atomically
retornados.Caso contrário, há uma incompatibilidade entre o log e o conteúdo de um TVar; o log é descartado, os bloqueios são liberados e
atomically
reinicia do início.No seu exemplo, se houve três tentativas, significa que foram criados três logs completos semelhantes aos acima. A única diferença entre os dois primeiros é que, ao
atomically
analisar esses logs novamente, não foi possível fingir que eles ocorreram atomicamente, então a transação foi reiniciada.Acredito que algo assim serviria como um modelo mental bastante decente:
A coisa real é provavelmente um pouco mais complicada/otimizada/robusta. Não vou me preocupar em lidar com exceções aqui, estou assumindo que bloquear uma coleção de variáveis de uma só vez é fácil, não estou mostrando a implementação de
retry
, não pensei se alguma cerca de memória é necessária, etc.