AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / coding / Perguntas / 79584702
Accepted
Enlico
Enlico
Asked: 2025-04-21 21:12:35 +0800 CST2025-04-21 21:12:35 +0800 CST 2025-04-21 21:12:35 +0800 CST

Esclarecimento sobre a importância da imutabilidade do estado dentro de um MVar no contexto de concorrência

  • 772

Em Programação Paralela e Concorrente em Haskell , de Simon Marlow, nas páginas 133 e 134, o seguinte código é mostrado:

type Name        = String
type PhoneNumber = String
type PhoneBook   = Map Name PhoneNumber

newtype PhoneBookState = PhoneBookState (MVar PhoneBook)

lookup :: PhoneBookState -> Name -> IO (Maybe PhoneNumber)
lookup (PhoneBookState m) name = do
  book <- takeMVar m
  putMVar m book
  return (Map.lookup name book)

E na página seguinte, algumas observações. A parte final do segundo parágrafo me intriga um pouco. Aqui está a primeira metade do parágrafo:

Usar estruturas de dados imutáveis ​​em um wrapper mutável traz outros benefícios. Observe que, na lookupoperação, simplesmente capturamos o valor atual do estado e, em seguida, a Map.lookupoperação complexa ocorre fora da sequência takeMVar/ putMVar. Isso é bom para a concorrência, pois significa que o bloqueio é mantido apenas por um período muito curto.

Isso é bastante claro para mim, no sentido de que entendo que isso takeMVarcoloca o bloqueio e putMVaro libera imediatamente, de modo que os acessos simultâneos subsequentes ao PhoneBookStatenão sejam mais prejudicados. Ao mesmo tempo, também vejo que outras mutações do MVarsão possíveis, então, quando o tempo lookupretorna com um Just smth, esse (name, smth)par pode ter sido removido do mapa (ou, se retornou Nothing, alguns (name, smth)podem ter sido adicionados ao mapa).

Entretanto, não entendi o que a seguinte parte do parágrafo tenta transmitir:

Isso só é possível porque o valor do estado é imutável. Se a estrutura de dados fosse mutável, teríamos que manter o bloqueio enquanto operávamos nela.⁵


⁵. A outra opção é usar um algoritmo sem bloqueios, que é extremamente complexo e difícil de acertar.

O que isso significa? O autor está se referindo ao caso que Mapexpôs alguma API a uma mutação? Isso nem é possível, a menos que estejamos em IO, certo? Ou ele está se referindo a mutações realizadas no mconteúdo de (por outras threads após putMVara liberação do bloqueio) que de alguma forma afetaram booko conteúdo de ?

haskell
  • 2 2 respostas
  • 58 Views

2 respostas

  • Voted
  1. Paul Johnson
    2025-04-21T21:47:24+08:002025-04-21T21:47:24+08:00

    Marlow está contrastando a abordagem Haskell de dados imutáveis ​​com linguagens que permitem que todas as estruturas de dados sejam mutadas.

    Em uma linguagem como Java ou JavaScript, não seria suficiente bloquear apenas a variável que contém o Map, pois pode haver outra thread que o tenha obtido Mape o esteja modificando. Portanto, você precisaria bloquear toda a estrutura enquanto estivesse acessando-a.

    Em Haskell, já sabemos que o Mapé imutável. Qualquer coisa que queira pegar o Mape modificá-lo deve criar um novo valor. Portanto, precisamos bloquear apenas o MVarque contém o Map, e não o todo Map(embora, se você planeja escrever o mapa modificado de volta no original MVar, precisa manter um bloqueio para toda a operação).

    A parte sobre estruturas "sem bloqueio" é explicada com mais detalhes neste artigo da Wikipédia . Resumidamente, é praticamente possível criar uma árvore ordenada na qual várias threads podem alterar o conteúdo sem que nada dê errado. Mas a definição e a comprovação de tais estruturas são magia profunda , não algo a ser tentado durante o desenvolvimento normal.

    • 2
  2. Best Answer
    K. A. Buhr
    2025-04-22T04:04:41+08:002025-04-22T04:04:41+08:00

    Aqui está um exemplo completo que pode ajudar a ilustrar o problema. Tenho certeza de que você entenderá se eu não quiser escrever uma implementação completa de mapa mutável para responder a essa pergunta, mas considere o seguinte mapa mutável simplificado, capaz de armazenar apenas um par chave-valor. Ele suporta as operações "new", "lookup" e "replace". (Chamei-o de "replace" em vez de "insert" porque o mapa só pode armazenar um par chave-valor, então a entrada anterior se perde quando um novo é inserido.)

    data MMap k v = MMap (IORef k) (IORef v)
    
    mmapNew :: k -> v -> IO (MMap k v)
    mmapNew k v = MMap <$> newIORef k <*> newIORef v
    
    mmapReplace :: k -> v -> MMap k v -> IO ()
    mmapReplace k v (MMap k_ v_) = do
      writeIORef k_ k
      writeIORef v_ v
    
    mmapLookup :: (Eq k) => k -> MMap k v -> IO (Maybe v)
    mmapLookup k (MMap k'_ v_) = do
      k' <- readIORef k'_
      if k == k'
        then Just <$> readIORef v_
        else pure Nothing
    

    Agora, suponha que queremos escrever uma interface concorrente e segura para threads para uma lista telefônica usando esta estrutura de dados mutável. O código revisado lookupé mais ou menos o mesmo que o original:

    type Name = String
    type PhoneNumber = String
    type PhoneBook = MMap Name PhoneNumber
    newtype PhoneBookState = PhoneBookState (MVar PhoneBook)
    
    lookup :: PhoneBookState -> Name -> IO (Maybe PhoneNumber)
    lookup (PhoneBookState m) name = do
      book <- takeMVar m
      putMVar m book
      mmapLookup name book   -- no `return`, because this is an IO operation
    

    onde realizamos o real mmapLookupapós desbloquear o estado.

    O código API correspondente para newe replaceprovavelmente seria algo como isto:

    new :: Name -> PhoneNumber -> IO PhoneBookState
    new name number = do
      book <- mmapNew name number
      PhoneBookState <$> newMVar book
    
    replace :: PhoneBookState -> Name -> PhoneNumber -> IO ()
    replace (PhoneBookState m) name number = do
      book <- takeMVar m
      mmapReplace name number book
      putMVar m book
    

    Observe que, diferentemente de lookup, adotamos uma abordagem conservadora com replace, mantendo o bloqueio durante toda a operação de substituição.

    No entanto, esta implementação tem uma condição de corrida para o par lookup/ replace. Para entender o porquê, vamos adicionar um atraso a mmapLookup:

    mmapLookup :: (Eq k) => k -> MMap k v -> IO (Maybe v)
    mmapLookup k (MMap k'_ v_) = do
      k' <- readIORef k'_
      threadDelay 2000000
      if k == k'
        then Just <$> readIORef v_
        else pure Nothing
    

    e considere a seguinte mainfunção, que cria uma lista telefônica para o número de telefone de "Alice" e então inicia dois threads — um para procurar "Alice" e outro para substituir a entrada pelo número de "Bob":

    main = do
      pbs <- new "Alice" "555-1234"
      forkIO $ do
        aliceno <- lookup pbs "Alice"
        putStrLn $ "Alice's number is " ++ show aliceno
      threadDelay 1000000
      forkIO $ replace pbs "Bob" "555-9876"
      threadDelay 5000000
      putStrLn "Done!"
    

    Quando este programa é executado, ele recupera incorretamente o número de Bob na pesquisa de Alice:

    Alice's number is Just "555-9876"
    Done!
    

    Obviamente, o problema é que a operação de substituição ocorre entre a comparação das chaves e a recuperação do valor.

    Se você modificar lookuppara liberar o bloqueio somente após a conclusão da pesquisa:

    lookup :: PhoneBookState -> Name -> IO (Maybe PhoneNumber)
    lookup (PhoneBookState m) name = do
      book <- takeMVar m
      number <- mmapLookup name book
      putMVar m book
      return number
    

    então não haverá condição de corrida. Não importa como você intercale as operações (e não importa quão cuidadosamente você coloque os atrasos), você recuperará o número de Alice Just "555-1234"da lista telefônica original ou não conseguirá recuperar o número de Alice e retornará Nothingda lista telefônica revisada (onde ele foi substituído pelo número de Bob).

    Então, para um mutável MMap, temos que manter o bloqueio em torno da mmapLookupoperação para operar com segurança na estrutura de dados sem permitir que outros threads modifiquem a estrutura de dados sem que precisemos fazer nada enquanto estivermos executando o lookup.

    Em contraste, se usássemos uma estrutura de dados imutável análoga:

    data IMap k v = IMap k v
    
    imapNew :: k -> v -> IMap k v
    imapNew k v = IMap k v
    
    imapReplace :: k -> v -> IMap k v -> IMap k v
    imapReplace k v _ = IMap k v
    
    imapLookup :: (Eq k) => k -> IMap k v -> Maybe v
    imapLookup k (IMap k' v) | k == k' = Just v
                             | otherwise = Nothing
    
    
    type Name = String
    type PhoneNumber = String
    type PhoneBook = IMap Name PhoneNumber
    newtype PhoneBookState = PhoneBookState (MVar PhoneBook)
    

    então as implementações correspondentes de new, lookup, e replaceseriam:

    new :: Name -> PhoneNumber -> IO PhoneBookState
    new name number = PhoneBookState <$> newMVar (imapNew name number)
    
    lookup :: PhoneBookState -> Name -> IO (Maybe PhoneNumber)
    lookup (PhoneBookState m) name = do
      book <- takeMVar m
      putMVar m book
      return $ imapLookup name book
    
    replace :: PhoneBookState -> Name -> PhoneNumber -> IO ()
    replace (PhoneBookState m) name number = do
      book <- takeMVar m
      let book' = imapReplace name number book
      putMVar m book'
    

    Fundamentalmente, quando replacese retira bookdo MVar, que pode muito bem ser o mesmo bookacessado simultaneamente pela imapLookupchamada em lookup, ele não sofre mutação, bookmas cria um novo book'por meio da imapReplacechamada sem afetar o original book. Esta é a essência das estruturas de dados imutáveis, e é por isso que a imapLookupoperação pode continuar a ser executada com segurança no inalterado, bookmesmo ao replacecriar um novo book'e gravá-lo de volta no vago MVar.

    Mesmo se fizéssemos um esforço para reescrever imapLookupcomo uma IOoperação com ampla oportunidade para outro thread ser executado entre o acesso de chave e valor:

    imapLookup :: (Eq k) => k -> IMap k v -> IO (Maybe v)
    imapLookup k m = do
      k' <- evaluate $ (\(IMap k v) -> k) m    -- force retrieval of the key
      threadDelay 1000000                      -- allow another thread to run
      v' <- evaluate $ (\(IMap k v) -> v) m    -- fetch the value
      pure $ if k == k' then Just v' else Nothing
    

    ainda assim não dispararíamos uma condição de corrida, porque o book :: IMap k vobjeto acessado imapLookup name booké imutável, independentemente de como ele está sendo acessado em outras threads.

    Aqui está o exemplo completo ilustrando a condição de corrida com uma MMapestrutura mutável:

    import Prelude hiding (lookup)
    import Data.IORef
    import Control.Concurrent
    
    data MMap k v = MMap (IORef k) (IORef v)
    
    mmapNew :: k -> v -> IO (MMap k v)
    mmapNew k v = MMap <$> newIORef k <*> newIORef v
    
    mmapReplace :: k -> v -> MMap k v -> IO ()
    mmapReplace k v (MMap k_ v_) = do
      writeIORef k_ k
      writeIORef v_ v
    
    mmapLookup :: (Eq k) => k -> MMap k v -> IO (Maybe v)
    mmapLookup k (MMap k'_ v_) = do
      k' <- readIORef k'_
      threadDelay 2000000  -- allow another thread to do mischief
      if k == k'
        then Just <$> readIORef v_
        else pure Nothing
    
    type Name = String
    type PhoneNumber = String
    type PhoneBook = MMap Name PhoneNumber
    newtype PhoneBookState = PhoneBookState (MVar PhoneBook)
    
    new :: Name -> PhoneNumber -> IO PhoneBookState
    new name number = do
      book <- mmapNew name number
      PhoneBookState <$> newMVar book
    
    lookup :: PhoneBookState -> Name -> IO (Maybe PhoneNumber)
    lookup (PhoneBookState m) name = do
      book <- takeMVar m
      putMVar m book
      mmapLookup name book
    
    replace :: PhoneBookState -> Name -> PhoneNumber -> IO ()
    replace (PhoneBookState m) name number = do
      book <- takeMVar m
      mmapReplace name number book
      putMVar m book
    
    main = do
      pbs <- new "Alice" "555-1234"
      forkIO $ do
        aliceno <- lookup pbs "Alice"
        putStrLn $ "Alice's number is " ++ show aliceno
      threadDelay 1000000
      forkIO $ replace pbs "Bob" "555-9876"
      threadDelay 2000000  -- wait for children to finish
      putStrLn "Done!"
    

    e a versão imutável sem a condição de corrida:

    module ImmutableMap where
    
    import qualified Data.Map as Map
    import Prelude hiding (lookup)
    import Control.Concurrent
    import Control.Exception
    
    data IMap k v = IMap k v
    
    imapNew :: k -> v -> IMap k v
    imapNew k v = IMap k v
    
    imapReplace :: k -> v -> IMap k v -> IMap k v
    imapReplace k v _ = IMap k v
    
    imapLookup :: (Eq k) => k -> IMap k v -> IO (Maybe v)
    imapLookup k m = do
      k' <- evaluate $ (\(IMap k v) -> k) m    -- force retrieval of the key
      threadDelay 1000000                      -- allow another thread to run
      v' <- evaluate $ (\(IMap k v) -> v) m    -- fetch the value
      pure $ if k == k' then Just v' else Nothing
    
    type Name = String
    type PhoneNumber = String
    type PhoneBook = IMap Name PhoneNumber
    newtype PhoneBookState = PhoneBookState (MVar PhoneBook)
    
    new :: Name -> PhoneNumber -> IO PhoneBookState
    new name number = PhoneBookState <$> newMVar (imapNew name number)
    
    lookup :: PhoneBookState -> Name -> IO (Maybe PhoneNumber)
    lookup (PhoneBookState m) name = do
      book <- takeMVar m
      putMVar m book
      imapLookup name book
    
    replace :: PhoneBookState -> Name -> PhoneNumber -> IO ()
    replace (PhoneBookState m) name number = do
      book <- takeMVar m
      let book' = imapReplace name number book
      putMVar m book'
    
    main = do
      pbs <- new "Alice" "555-1234"
      forkIO $ do
        aliceno <- lookup pbs "Alice"
        putStrLn $ "Alice's number is " ++ show aliceno
      threadDelay 1000000
      forkIO $ replace pbs "Bob" "555-9876"
      threadDelay 2000000  -- wait for children to finish
      putStrLn "Done!"
    
    • 2

relate perguntas

  • Existe uma classe que modela "patches"?

  • O Estado é uma flecha?

  • Escrevendo uma instância de Functor para um sinônimo de tipo e sendo confundido

  • Módulo Haskell semelhante ao JavaScript (leva em conta o login e funciona para não inteiros)

  • Como o scanl em Haskell funciona na lista de ambos - comparação de dois casos

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Reformatar números, inserindo separadores em posições fixas

    • 6 respostas
  • Marko Smith

    Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não?

    • 2 respostas
  • Marko Smith

    Problema com extensão desinstalada automaticamente do VScode (tema Material)

    • 2 respostas
  • Marko Smith

    Vue 3: Erro na criação "Identificador esperado, mas encontrado 'import'" [duplicado]

    • 1 respostas
  • Marko Smith

    Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores?

    • 1 respostas
  • Marko Smith

    Como faço para corrigir um erro MODULE_NOT_FOUND para um módulo que não importei manualmente?

    • 6 respostas
  • Marko Smith

    `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso?

    • 3 respostas
  • Marko Smith

    Um programa vazio que não faz nada em C++ precisa de um heap de 204 KB, mas não em C

    • 1 respostas
  • Marko Smith

    PowerBI atualmente quebrado com BigQuery: problema de driver Simba com atualização do Windows

    • 2 respostas
  • Marko Smith

    AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos

    • 1 respostas
  • Martin Hope
    Fantastic Mr Fox Somente o tipo copiável não é aceito na implementação std::vector do MSVC 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant Encontre o próximo dia da semana usando o cronógrafo 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor O inicializador de membro do construtor pode incluir a inicialização de outro membro? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul O C++20 mudou para permitir a conversão de `type(&)[N]` de matriz de limites conhecidos para `type(&)[]` de matriz de limites desconhecidos? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann Como/por que {2,3,10} e {x,3,10} com x=2 são ordenados de forma diferente? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller O ponto e vírgula agora é opcional em condicionais bash com [[ .. ]] na versão 5.2? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench Por que um traço duplo (--) faz com que esta cláusula MariaDB seja avaliada como verdadeira? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng Por que `dict(id=1, **{'id': 2})` às vezes gera `KeyError: 'id'` em vez de um TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos 2024-03-20 03:12:31 +0800 CST

Hot tag

python javascript c++ c# java typescript sql reactjs html

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve