Como o Rust diverge dos recursos de simultaneidade do C ++?

35

Questões

Estou tentando entender se o Rust melhora fundamental e suficientemente os recursos de simultaneidade do C ++, para decidir se devo dedicar algum tempo ao aprendizado do Rust.

Especificamente, como o Rust idiomático melhora ou de alguma forma diverge dos recursos de simultaneidade do C ++ idiomático?

A melhoria (ou divergência) é principalmente sintática, ou é substancialmente uma melhoria (divergência) no paradigma? Ou é outra coisa? Ou não é realmente uma melhoria (divergência)?


Fundamentação

Recentemente, tenho tentado me ensinar as instalações de simultaneidade do C ++ 14 e algo parece não estar certo. Algo parece errado. O que parece? Difícil de dizer.

Parece quase como se o compilador não estivesse realmente tentando me ajudar a escrever programas corretos no que diz respeito à simultaneidade. Parece quase como se eu estivesse usando um assembler em vez de um compilador.

É certo que é totalmente provável que eu ainda sofra de um conceito sutil e defeituoso no que diz respeito à simultaneidade. Talvez eu ainda não entenda a tensão de Bartosz Milewski entre programação stateful e corridas de dados. Talvez eu não entenda bem o quanto de metodologia simultânea sólida existe no compilador e quanto disso existe no sistema operacional.

thb
fonte

Respostas:

56

Uma história de concorrência melhor é um dos principais objetivos do projeto Rust, portanto, melhorias devem ser esperadas, desde que confiemos no projeto para atingir seus objetivos. Isenção de responsabilidade total: tenho uma opinião alta do Rust e sou investido nele. Conforme solicitado, tentarei evitar julgamentos de valor e descrever diferenças em vez de melhorias (IMHO) .

Ferrugem segura e insegura

"Rust" é composto de duas linguagens: uma que se esforça muito para isolá-lo dos perigos da programação de sistemas e outra mais poderosa sem essas aspirações.

Rust inseguro é uma linguagem desagradável e brutal que se parece muito com C ++. Ele permite que você faça coisas arbitrariamente perigosas, converse com o hardware, gerencie mal a memória manualmente, atire nos pés, etc. É muito parecido com C e C ++, pois a correção do programa está em suas mãos. e nas mãos de todos os outros programadores envolvidos. Você escolhe esse idioma com a palavra-chave unsafee, como em C e C ++, um único erro em um único local pode derrubar todo o projeto.

Ferrugem segura é o "padrão", a grande maioria do código Rust é segura e, se você nunca mencionar a palavra-chave unsafeem seu código, nunca sai do idioma seguro. O restante da postagem se preocupará principalmente com esse idioma, porque o unsafecódigo pode quebrar toda e qualquer garantia de que o Rust seguro trabalha tanto para oferecer a você. Por outro lado, o unsafecódigo não é mau e não é tratado como tal pela comunidade (é, no entanto, fortemente desencorajado quando não é necessário).

É perigoso, sim, mas também importante, porque permite construir as abstrações que o código seguro usa. Um código inseguro bom usa o sistema de tipos para impedir que outros o usem incorretamente e, portanto, a presença de código inseguro em um programa Rust não precisa perturbar o código seguro. Todas as seguintes diferenças existem porque os sistemas do tipo Rust têm ferramentas que o C ++ não possui e porque o código não seguro que implementa as abstrações de simultaneidade usa essas ferramentas efetivamente.

Sem diferença: Memória compartilhada / mutável

Embora o Rust dê mais ênfase à passagem de mensagens e controle estritamente a memória compartilhada, ele não descarta a simultaneidade da memória compartilhada e suporta explicitamente as abstrações comuns (bloqueios, operações atômicas, variáveis ​​de condição, coleções simultâneas).

Além disso, como C ++ e, ao contrário das linguagens funcionais, o Rust realmente gosta de estruturas de dados imperativas tradicionais. Não há lista vinculada persistente / imutável na biblioteca padrão. Existe, std::collections::LinkedListmas é como std::listem C ++, e desencorajado pelas mesmas razões que std::list(mau uso do cache).

No entanto, com referência ao título desta seção ("memória compartilhada / mutável"), Rust tem uma diferença em relação ao C ++: encoraja fortemente que a memória seja "mutável XOR compartilhada", ou seja, que a memória nunca seja compartilhada e mutável ao mesmo tempo Tempo. Mude a memória como desejar "na privacidade de seu próprio segmento", por assim dizer. Compare isso com C ++, onde a memória mutável compartilhada é a opção padrão e amplamente utilizada.

Embora o paradigma compartilhado com xor mutável seja muito importante para as diferenças abaixo, também é um paradigma de programação bem diferente que demora um pouco para se acostumar e coloca restrições significativas. Ocasionalmente, é preciso optar por sair desse paradigma, por exemplo, com tipos atômicos ( AtomicUsizeé a essência da memória mutável compartilhada). Observe que os bloqueios também obedecem à regra shared-xor-mutable, porque exclui leituras e gravações simultâneas (enquanto um thread grava, nenhum outro thread pode ler ou gravar).

Não diferença: as corridas de dados são um comportamento indefinido (UB)

Se você acionar uma corrida de dados no código Rust, o jogo terminará, assim como no C ++. Todas as apostas estão desativadas e o compilador pode fazer o que bem entender.

No entanto, é uma garantia garantida que o código Rust seguro não possua corridas de dados (ou qualquer UB). Isso se estende ao idioma principal e à biblioteca padrão. Se você pode escrever um programa Rust que não utiliza unsafe(inclusive em bibliotecas de terceiros, mas exclui a biblioteca padrão) que aciona o UB, isso é considerado um bug e será corrigido (isso já aconteceu várias vezes). Isso, se é claro, contrasta com o C ++, onde é trivial escrever programas com o UB.

Diferença: Disciplina de bloqueio rigorosa

Ao contrário de C ++, uma fechadura em Rust ( std::sync::Mutex, std::sync::RwLock, etc.) possui os dados que está a proteger. Em vez de pegar um bloqueio e, em seguida, manipular alguma memória compartilhada associada ao bloqueio apenas na documentação, os dados compartilhados ficam inacessíveis enquanto você não mantém o bloqueio. Um guarda RAII mantém o bloqueio e, simultaneamente, dá acesso aos dados bloqueados (isso pode ser implementado pelo C ++, mas não pelos std::bloqueios). O sistema vitalício garante que você não possa continuar acessando os dados após soltar o bloqueio (solte a proteção RAII).

Obviamente, você pode ter um bloqueio que não contém dados úteis ( Mutex<()>) e apenas compartilhar alguma memória sem associá-lo explicitamente a esse bloqueio. No entanto, ter memória compartilhada potencialmente não sincronizada requer unsafe.

Diferença: Prevenção de compartilhamento acidental

Embora você possa ter memória compartilhada, você só compartilha quando solicita explicitamente. Por exemplo, quando você usa a passagem de mensagens (por exemplo, os canais de std::sync), o sistema vitalício garante que você não mantenha nenhuma referência aos dados após enviá-los para outro encadeamento. Para compartilhar dados atrás de um bloqueio, você constrói explicitamente o bloqueio e o entrega a outro encadeamento. Para compartilhar memória não sincronizada com unsafevocê, bem, tem que usar unsafe.

Isso está vinculado ao próximo ponto:

Diferença: Rastreamento de segurança de thread

O sistema do tipo Rust rastreia alguma noção de segurança de rosca. Especificamente, a Synccaracterística denota tipos que podem ser compartilhados por vários encadeamentos sem risco de corridas de dados, enquanto Sendmarca aqueles que podem ser movidos de um encadeamento para outro. Isso é imposto pelo compilador em todo o programa e, portanto, os designers de bibliotecas se atrevem a fazer otimizações que seriam estupidamente perigosas sem essas verificações estáticas. Por exemplo, C ++, std::shared_ptrque sempre usa operações atômicas para manipular sua contagem de referência, para evitar UB, se shared_ptrfor usado por vários encadeamentos. O Rust possui Rce Arc, que diferem apenas no Rc uso de operações não-atômicas de refcount e não é seguro para threads (ou seja, não implementa Syncou Send), enquanto Arcé muito parecido comshared_ptr (e implementa as duas características).

Observe que, se um tipo não for usado unsafepara implementar manualmente a sincronização, a presença ou ausência das características serão inferidas corretamente.

Diferença: Regras muito estritas

Se o compilador não puder ter certeza absoluta de que algum código está livre de corridas de dados e outros UB, ele não será compilado . As regras e outras ferramentas acima mencionadas podem levar você muito longe, mas mais cedo ou mais tarde você desejará fazer algo que seja correto, mas por razões sutis que escapam ao aviso do compilador. Pode ser uma estrutura complicada de dados sem bloqueios, mas também pode ser algo tão banal quanto "eu escrevo para locais aleatórios em uma matriz compartilhada, mas os índices são calculados de modo que cada local seja gravado por apenas um encadeamento".

Nesse ponto, você pode morder o marcador e adicionar um pouco de sincronização desnecessária, ou reformular o código para que o compilador possa ver sua correção (geralmente factível, às vezes bastante difícil, às vezes impossível), ou você entra no unsafecódigo. Ainda assim, é uma sobrecarga mental extra, e o Rust não oferece nenhuma garantia para a correção do unsafecódigo.

Diferença: Menos ferramentas

Por causa das diferenças acima mencionadas, no Rust é muito mais raro alguém escrever código que possa ter uma corrida de dados (ou um uso depois de grátis, ou um duplo grátis, ou ...). Embora isso seja bom, tem o efeito colateral lamentável de que o ecossistema para rastrear esses erros seja ainda mais subdesenvolvido do que se esperaria, dada a juventude e o pequeno tamanho da comunidade.

Embora ferramentas como valgrind e o desinfetante de roscas do LLVM possam, em princípio, ser aplicadas ao código Rust, se isso realmente funciona ainda varia de ferramenta para ferramenta (e mesmo aquelas que funcionam podem ser difíceis de configurar, especialmente porque você pode não encontrar nenhum -data recursos sobre como fazê-lo). Realmente não ajuda que o Rust atualmente não tenha uma especificação real e, em particular, um modelo de memória formal.

Em resumo, escrever o unsafecódigo Rust corretamente é mais difícil do que escrever o código C ++ corretamente, apesar de ambos os idiomas serem aproximadamente comparáveis ​​em termos de recursos e riscos. É claro que isso deve ser ponderado contra o fato de que um programa Rust típico conterá apenas uma fração de unsafecódigo relativamente pequena , enquanto um programa C ++ é, bem, totalmente C ++.


fonte
6
Onde na minha tela está o interruptor upvote +25? Não consigo encontrar! Esta resposta informativa é muito apreciada. Isso me deixa sem perguntas óbvias sobre os pontos que aborda. Portanto, para outros pontos: se eu entendo a documentação da Rust, a Rust [a] integrou instalações de teste e [b] um sistema de construção chamado Cargo. Estes estão razoavelmente prontos para produção na sua opinião? Além disso, em relação ao Cargo, é bem-humorado deixar-me adicionar shell, scripts Python e Perl, compilação do LaTeX etc. ao processo de compilação?
THB
2
@thb O material de teste é muito simples (por exemplo, sem zombaria), mas funcional. A carga funciona muito bem, embora seu foco em Ferrugem e reprodutibilidade signifique que talvez não seja a melhor opção para cobrir todas as etapas, do código-fonte aos artefatos finais. Você pode escrever scripts de construção, mas isso pode não ser apropriado para todas as coisas mencionadas. (As pessoas, no entanto, usam regularmente scripts de construção para compilar bibliotecas C ou encontrar versões existentes de bibliotecas C, então não é como carga pára de funcionar quando você usar mais de pura Rust.)
2
A propósito, pelo que vale a pena, sua resposta parece bastante conclusiva. Como gosto de C ++, já que o C ++ possui instalações decentes para quase tudo o que eu precisava fazer, uma vez que o C ++ é estável e amplamente utilizado, até agora fiquei bastante satisfeito em usar o C ++ para todos os propósitos não leves (nunca desenvolvi um interesse em Java , por exemplo). Mas agora temos simultaneidade e C ++ 14 parece para mim estar lutando com ele. Eu não tentei voluntariamente uma nova linguagem de programação em uma década, mas (a menos que Haskell deva parecer uma opção melhor), acho que terei que experimentar o Rust.
THB
Note that if a type doesn't use unsafe to manually implement synchronization, the presence or absence of the traits are inferred correctly.na verdade, ainda funciona mesmo com unsafeelementos. Apenas ponteiros brutos não são Syncnem o Shareque significa que, por padrão, a estrutura que os contém não terá nenhum.
Hauleth
@ ŁukaszNiemier Pode dar certo, mas há um bilhão de maneiras pelas quais um tipo de uso inseguro pode acabar Sendou Syncmesmo que realmente não deveria.
-2

Ferrugem também é muito parecida com Erlang e Go. Ele se comunica usando canais que possuem buffers e espera condicional. Assim como o Go, ele relaxa as restrições do Erlang, permitindo que você compartilhe memória, suporte à contagem e bloqueios de referências atômicas e ao passar canais de um segmento para outro.

No entanto, Rust vai um passo além. Enquanto Go confia em você para fazer a coisa certa, Rust designa um mentor que se senta com você e reclama se você tentar fazer a coisa errada. O mentor de Rust é o compilador. Ele faz análises sofisticadas para determinar a propriedade dos valores que são passados ​​pelos encadeamentos e fornecer erros de compilação, se houver problemas em potencial.

A seguir, uma citação dos documentos da RUST.

As regras de propriedade desempenham um papel vital no envio de mensagens, porque nos ajudam a escrever código simultâneo e seguro. Prevenir erros na programação simultânea é a vantagem que obtemos ao compensar ter que pensar em propriedade em todos os nossos programas Rust. - Mensagem que passa com propriedade de valores.

Se Erlang é draconiano e Go é um estado livre, então Rust é um estado de babá.

Você pode encontrar mais informações nas ideologias de simultaneidade das linguagens de programação: Java, C #, C, C +, Go e Rust

srinath_perera
fonte
2
Bem-vindo ao Stack Exchange! Observe que sempre que você cria um link para seu próprio blog, você precisa declarar isso explicitamente; consulte o centro de ajuda .
Glorfindel