Eu li a documentação. Agora, eu meio que entendo o que encadear uma função faz no Godot. Eu o uso com mais ou menos sucesso (mas depois de adicionar mais alguns bits ao código, o projeto simplesmente trava). Eu também li a API thread-safe e essa é uma zona cinzenta para mim. Mutexes também são. E semáforos. Não sou um cara de TI, então é mais como um Explique como se eu fosse 5)
Com o que especificamente posso ou não usar threading? Posso chamar a função de outro nó? Ou devo colocar um novo thread diretamente nesse nó?
O que são mutexes? O que locking
e unlocking
um mutex faz?
O que são semáforos?
Quando usar o quê?
Como se você tivesse 5 anos? OK... Na terra do código...
Thread único vs multithread
A execução do código flui de uma instrução para a próxima. Às vezes, ele salta pulando algumas seções condicionais (quando há uma instrução
if
ou umamatch
) ou entra em loop (quando há uma instruçãofor
ou ).while
Mas não importa o que aconteça, é sempre a execução de uma única instrução por vez.Isso é o que chamamos de execução de "threaded único". Mas um dia, o "multi-threaded" chegou à terra do código...
Quando existem vários threads, cada um tem seu próprio fluxo de execução. Enquanto um thread está em uma instrução, o outro está em alguma outra instrução.
Isso pode ser bom, porque pode permitir que o código execute mais trabalho ao mesmo tempo. Mas também pode ser ruim, porque o que um thread está fazendo pode atrapalhar os outros.
Assim é preciso ter cuidado para que os fios não se enrosquem...
No tópico principal
Mesmo que exista multi-threading, ainda existe o thread principal, que é o mesmo que você tinha quando a execução era de thread único. E muitas coisas estão vinculadas ao thread principal (principalmente a UI está vinculada ao thread que criou a janela, e a primeira janela é criada pelo thread principal). E tentar acessá-los de outro tópico causará problemas... Do mundano ao desagradável.
Execução adiada
Nossa principal ferramenta para isso é a execução diferida, pois isso sempre acontecerá na thread principal, independente de qual thread a solicitou. E como um thread faz apenas uma coisa por vez, não haverá problemas.
Chamadas adiadas são uma boa maneira de outros threads entregarem resultados ao thread principal. Mas fazer tudo com execução adiada desperdiça o potencial dos threads.
Como a chamada adiada acontecerá algum tempo depois no thread principal, do ponto de vista dos threads secundários, a chamada adiada é disparada e esquecida (o thread secundário não pode aguardá-la ou obter um resultado dela, ele continua executando agora mesmo).
Você pode usar uma chamada adiada de um thread secundário para fornecê-la
Callable
. Mas se o thread principal chamar oCallable
, ele estará sendo executado no thread principal. Estou lhe dizendo que isso não é uma solução alternativa.Quero que você imagine este cenário: você tem alguma computação complexa ou está realizando alguma operação IO grande (por exemplo, lendo um arquivo grande), o que levará algum tempo...
Se você fizer isso no thread principal, ele estará ocupado fazendo isso e não atualizará o resto do seu jogo/aplicativo. Como resultado, o usuário verá que o jogo/aplicativo congela.
Para evitar isso, você pode usar um secundário
Thread
, onde você faz o trabalho. Mas no final você quer mostrar um resultado, ou quer mostrar alguma barra de progresso enquanto ele está em execução. Para fazer isso, o thread secundário pode usar uma chamada adiada.Rosqueamento preventivo
Para poder fazer com que vários threads trabalhem com os mesmos valores, precisamos entendê-los melhor. Notavelmente, o threading é preventivo ( geralmente ), o que significa que o thread pode ser suspenso e retomado a qualquer momento. Às vezes sob seu controle, às vezes fora de seu controle. Como resultado, o que quer que um thread esteja fazendo pode ser deixado temporariamente em um estado não pescado.
Algumas instruções são atômicas, o que significa que não podem ser subdivididas e, portanto, um thread as executa ou não. Eles não podem ficar pela metade. Infelizmente, pode depender da quantidade de dados e da plataforma (às vezes a leitura ou escrita não é uma operação única, mas deve ser feita em partes)… Então, a menos que seja explicitamente declarado que algo é garantido como atômico, não confie nisso .
Assim, uma das coisas que pode acontecer é que um thread esteja escrevendo uma variável, mas escreva apenas na metade, e outro thread leia assim e... ah, não, comportamento indefinido! caos! trava!
Algo que não será atômico ( normalmente ) é incrementar uma variável. Ele pode ser dividido em ler o valor da variável, calcular o valor incrementado e depois escrevê-lo.
Neste caso, um thread pode ler o valor, calcular o valor incrementado... E ser antecipado! Outro thread entra, lê o valor, calcula o valor incrementado e grava-o. Agora o primeiro thread retoma e grava o valor que calculou... Mas substitui o trabalho do segundo thread! O primeiro thread não sabia que o segundo alterou o valor! Como resultado, o valor foi incrementado apenas uma vez, não duas vezes.
Isso é conhecido como problema ABA , consulte também Tempo de verificação para tempo de uso . É também um exemplo de condição de corrida .
Fechaduras
Se você precisar de um thread secundário para esperar por algo, use um bloqueio (mutex ou semáforo).
Queremos que outros threads esperem até terminarmos de calcular e escrever valores antes de lerem os resultados. E também para impedi-los de escrever qualquer coisa que possa atrapalhar o trabalho que estamos fazendo em um tópico.
Chamamos onde queremos controlar o acesso dos threads de "Seções Críticas".
E nós temos ferramentas para isso!
Mutex
Uma ferramenta para controle de threads é o Mutex! O que significa Exclusão Mútua! Quando um thread pega o Mutex, ele o bloqueia, mas apenas um thread pode rodar com ele por vez, todos os outros threads serão suspensos até que o thread que executou com o mutex o libere.
Mas às vezes você não quer que seu thread espere até que o mutex seja liberado; nesse caso, você pode fazer com que eles tentem bloquear o mutex e, se falharem, estarão livres para fazer outra coisa. Apenas não os deixe enlouquecer na seção crítica.
Às vezes você tem um thread gerando valores para vários outros threads usarem. Neste caso, a seção crítica quando os threads leem o valor não é mutuamente exclusiva.
Semáforo
Outra ferramenta para controle de threads é o Semáforo. Isso é útil quando você tem um thread produzindo valores e outro thread os consumindo . E, claro, você não quer que o thread entre para consumir valores até que eles estejam lendo.
O que você faz é fazer com que os threads consumidores esperem no semáforo e então o thread produtor pode sinalizar ao semáforo para permitir a passagem de um thread. E o semáforo permitirá a passagem de tantos threads quanto o thread do produtor sinalizar.
*Infelizmente não temos muito mais em Godot. Eu desejaria bloqueios sofisticados de leitor-gravador, ou incrementos atômicos, ou operações interligadas, ou...
Reordenando
Por que não usamos apenas
bool
para indicar se os resultados estão prontos? Outro tópico poderia ler, e só acessar se fortrue
... Porque travessuras!Primeiro de tudo, embora Godot não faça isso com o GDScript, o compilador C++ usado para compilar Godot pode reordenar instruções desde que esteja convencido de que o resultado é o mesmo (e isso é verificado estaticamente sem considerar outros threads).
Em geral, os compiladores C++ são livres para compilar o código como quiserem, desde que o resultado seja como se o código fizesse o que o programador escreveu (esta é a liberdade que lhes permite introduzir otimizações, por exemplo, remover uma instrução para escrever uma variável que aparentemente ninguém lê).
Segundo, mesmo que o compilador não reordene as instruções, a CPU ainda poderá fazê-lo. Embora isso seja raro nas CPUs usadas em computadores desktop, não é tanto em outras arquiteturas.
E terceiro, as CPUs possuem cache. Em particular, as CPUs multi-core modernas possuem cache por núcleo. E eles lerão os dados do cache, se houver, em vez de lê-los da RAM.
Como resultado, um thread pode não ver as alterações que outro thread faz imediatamente ou na mesma ordem. Assim, mesmo que vejam o
bool
conjunto comotrue
, ainda poderão ver valores obsoletos para outras variáveis.Para evitar a reordenação, usamos algo chamado "barreiras de memória" (e mecanismos semelhantes, que seriam usados para implementar bloqueios)... No entanto, não faz muito sentido entrar nisso, já que não temos meios de fazer isso a partir do GDScript. Só podemos usar
Mutex
eSemaphore
e…Grupos de tópicos
Ainda preciso falar sobre os novos grupos de threads. Se você definir o grupo de threads de a
Node
comoPROCESS_THREAD_GROUP_MAIN_THREAD
ele será executado no thread principal. Se você definir comoPROCESS_THREAD_GROUP_SUB_THREAD
, um novo thread será executado. E se você configurá-lo,PROCESS_THREAD_GROUP_INHERIT
ele será executado no mesmo thread do pai.Isso permite que você defina um conjunto de
Node
s que irão operar em uma thread separada, e desde que funcionem apenas entre si, não haverá problemas, já que todos estão trabalhando na mesma thread. E você pode usar uma chamada adiada para fornecer resultados ao thread principal.Também temos um conjunto de funções
Node
que visam tornar segura a comunicação com subthreads (os métodos que estãothread
no nome daNode
classe). Eles possuem barreiras de memória integradas.Sinais e espera
Quando você aguarda um sinal (o que você pode fazer em threads secundários, mas não faça isso), a chamada retorna. Ou seja, o fluxo de execução sairá do método. E devolverá e armazenará o objeto a posição de onde saiu o fluxo de execução, para que possa ser retomado posteriormente, quando o sinal aguardado for emitido.
Então, quando o sinal for emitido, o thread principal pegará aquele objeto que representa a posição de execução e continuará a partir daí! Como resultado, aguardando no thread secundário, resultando na troca de threads.
Nota: este mecanismo pode mudar no futuro, pois os grupos de threads ainda são experimentais e provavelmente serão expandidos para cobrir este caso.
Na segurança do thread
Oh, segurança do fio! Isso significa que algo é seguro para ser chamado diretamente de qualquer thread. Ou dito de outra forma, ele é escrito internamente sob a suposição de que pode ser chamado de vários threads simultaneamente, e existem precauções para evitar que isso cause qualquer comportamento inesperado ou travamentos... Em palavras simples: os desenvolvedores cuidaram de o multi-threading, então você não precisa.
E, como aponta a documentação, a árvore de cena NÃO é segura para threads. O que torna qualquer operação na árvore de cena uma seção crítica por padrão.
Uma nota sobre física
Outra coisa a considerar é que Godot pode usar um thread separado para física (que você habilita nas configurações do projeto). Isso também se aplicará a algumas ligações recebidas
Node
da física.