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 unsafe
e, 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 unsafe
em seu código, nunca sai do idioma seguro. O restante da postagem se preocupará principalmente com esse idioma, porque o unsafe
código pode quebrar toda e qualquer garantia de que o Rust seguro trabalha tanto para oferecer a você. Por outro lado, o unsafe
có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::LinkedList
mas é como std::list
em 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 unsafe
você, 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 Sync
característica denota tipos que podem ser compartilhados por vários encadeamentos sem risco de corridas de dados, enquanto Send
marca 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_ptr
que sempre usa operações atômicas para manipular sua contagem de referência, para evitar UB, se shared_ptr
for usado por vários encadeamentos. O Rust possui Rc
e 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 Sync
ou Send
), enquanto Arc
é muito parecido comshared_ptr
(e implementa as duas características).
Observe que, se um tipo não for usado unsafe
para 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 unsafe
código. Ainda assim, é uma sobrecarga mental extra, e o Rust não oferece nenhuma garantia para a correção do unsafe
có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 unsafe
có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 unsafe
código relativamente pequena , enquanto um programa C ++ é, bem, totalmente C ++.