A principal distinção, como você aponta na sua pergunta, é se o agendador deve ou não antecipar um encadeamento. A maneira como um programador pensa sobre o compartilhamento de estruturas de dados ou sobre a sincronização entre "threads" é muito diferente nos sistemas preventivo e cooperativo.
Em um sistema de cooperativa (que vai por muitos nomes, cooperação multi-tasking , nonpreemptive multi-tasking , threads de usuário , fios verdes e fibras estão cinco dos mais comuns atualmente) o programador é garantido que seu código será executado atomicamente enquanto eles não fazem nenhuma chamada ou ligação ao sistema yield()
. Isso torna particularmente fácil lidar com estruturas de dados compartilhadas entre várias fibras. A menos que você precise fazer uma chamada do sistema como parte de uma seção crítica, as seções críticas não precisam ser marcadas (com mutex lock
e unlock
chamadas, por exemplo). Então, em código como:
x = x + y
y = 2 * x
o programador não precisa se preocupar com o fato de que alguma outra fibra possa estar trabalhando com as variáveis x
e y
ao mesmo tempo. x
e y
serão atualizados juntos atomicamente da perspectiva de todas as outras fibras. Da mesma forma, todas as fibras poderiam compartilhar uma estrutura mais complicada, como uma árvore e uma chamada como tree.insert(key, value)
não precisariam ser protegidas por nenhum mutex ou seção crítica.
Por outro lado, em um sistema multithreading preventivo, como em threads verdadeiramente paralelos / multicore, toda intercalação possível de instruções entre threads é possível, a menos que haja seções críticas explícitas. Uma interrupção e preempção pode ocorrer entre duas instruções. No exemplo acima:
thread 0 thread 1
< thread 1 could read or modify x or y at this point
read x
< thread 1 could read or modify x or y at this point
read y
< thread 1 could read or modify x or y at this point
add x and y
< thread 1 could read or modify x or y at this point
write the result back into x
< thread 1 could read or modify x or y at this point
read x
< thread 1 could read or modify x or y at this point
multiply by 2
< thread 1 could read or modify x or y at this point
write the result back into y
< thread 1 could read or modify x or y at this point
Portanto, para estar correto em um sistema preventivo ou em um sistema com threads verdadeiramente paralelos, você precisa cercar cada seção crítica com algum tipo de sincronização, como um mutex lock
no início e um mutex unlock
no final.
Assim, as fibras são mais semelhantes às bibliotecas de E / S assíncronas do que aos encadeamentos preventivos ou verdadeiramente paralelos. O planejador de fibra é chamado e pode alternar fibras durante operações de E / S de longa latência. Isso pode oferecer o benefício de várias operações simultâneas de E / S sem exigir operações de sincronização em seções críticas. Assim, o uso de fibras pode, talvez, ter menos complexidade de programação do que encadeamentos preventivos ou verdadeiramente paralelos, mas a falta de sincronização em torno de seções críticas levaria a resultados desastrosos se você tentasse executar as fibras de forma simultânea ou preventiva.
A resposta é realmente que eles poderiam, mas há um desejo de não fazê-lo.
As fibras são usadas porque permitem controlar como a programação ocorre. Portanto, é muito mais simples projetar alguns algoritmos usando fibras, porque o programador disse em que fibra está sendo executada a qualquer momento. No entanto, se você deseja que duas fibras sejam executadas em dois núcleos diferentes ao mesmo tempo, é necessário agendá-las manualmente.
Os encadeamentos controlam qual código está sendo executado no sistema operacional. Em troca, o sistema operacional cuida de muitas tarefas feias para você. Alguns algoritmos ficam mais difíceis, porque o programador tem menos a dizer em qual código é executado em um determinado momento, para que casos mais inesperados possam surgir. Ferramentas como mutex e semáforos são adicionadas a um sistema operacional para dar ao programador controle suficiente para tornar os threads úteis e reduzir parte da incerteza, sem atrapalhar o programador.
Isso leva a algo que é ainda mais importante que cooperativo x preventivo: as fibras são controladas pelo programador, enquanto os threads são controlados pelo sistema operacional.
Você não precisa gerar uma fibra em outro processador. Os comandos no nível de montagem para fazer isso são atrozmente complicados e geralmente são específicos do processador. Você não precisa escrever 15 versões diferentes do seu código para lidar com esses processadores; portanto, vire para o sistema operacional. O trabalho do sistema operacional é abstrair essas diferenças. O resultado são "threads".
As fibras passam por cima dos fios. Eles não correm por conta própria. Portanto, se você deseja executar duas fibras em núcleos diferentes, basta gerar dois threads e executar uma fibra em cada um deles. Em muitas implementações de fibras, você pode fazer isso facilmente. O suporte multicore não vem das fibras, mas dos fios.
Torna-se fácil mostrar que, a menos que você queira escrever seu próprio código específico do processador, não há nada que você possa fazer atribuindo fibras a vários núcleos que você não poderia fazer criando threads e atribuindo fibras a cada um. Uma das minhas regras favoritas para o design da API é "Uma API não é concluída quando você termina de adicionar tudo a ela, mas quando não consegue mais encontrar algo para remover". Dado que o multi-core é tratado perfeitamente hospedando fibras em threads, não há motivo para complicar a API de fibra adicionando multi-core nesse nível.
fonte