Para quais propósitos os sistemas multithread são usados. Oito regras simples para o desenvolvimento de aplicativos multithread

Qual tópico levanta mais dúvidas e dificuldades para os iniciantes? Quando perguntei ao meu professor e programador Java Alexander Pryakhin sobre isso, ele respondeu imediatamente: “Multithreading”. Agradeço a ele pela ideia e ajuda na preparação deste artigo!

Vamos examinar o mundo interno do aplicativo e seus processos, descobrir qual é a essência do multithreading, quando é útil e como implementá-lo - usando Java como exemplo. Se você estiver aprendendo um idioma OOP diferente, não se preocupe: os princípios básicos são os mesmos.

Sobre streams e suas origens

Para entender o multithreading, primeiro vamos entender o que é um processo. Um processo é um pedaço de memória virtual e recursos que o sistema operacional aloca para executar um programa. Se você abrir várias instâncias do mesmo aplicativo, o sistema alocará um processo para cada uma. Em navegadores modernos, um processo separado pode ser responsável por cada guia.

Você provavelmente já encontrou o "Gerenciador de Tarefas" do Windows (no Linux é "Monitor do Sistema") e sabe que os processos desnecessários em execução carregam o sistema, e os mais "pesados" deles costumam congelar, por isso precisam ser encerrados à força .

Mas os usuários adoram multitarefa: não os alimente com pão - deixe-os abrir uma dúzia de janelas e pular para a frente e para trás. Há um dilema: é preciso garantir o funcionamento simultâneo dos aplicativos e ao mesmo tempo reduzir a carga do sistema para que não fique lento. Digamos que o hardware não consiga atender às necessidades dos proprietários - você precisa resolver o problema no nível do software.

Queremos que o processador execute mais instruções e processe mais dados por unidade de tempo. Ou seja, precisamos ajustar mais código executado em cada fração de tempo. Pense em uma unidade de execução de código como um objeto - isso é um thread.

Um caso complexo é mais fácil de abordar se você dividi-lo em vários casos simples. Portanto, ao trabalhar com memória: um processo "pesado" é dividido em threads que ocupam menos recursos e são mais propensos a entregar o código à calculadora (como exatamente - veja abaixo).

Cada aplicativo possui pelo menos um processo, e cada processo possui pelo menos um thread, que é chamado de thread principal e a partir do qual, se necessário, novos são iniciados.

Diferença entre threads e processos

    Threads usam a memória alocada para o processo e os processos requerem seu próprio espaço de memória. Portanto, os threads são criados e concluídos mais rapidamente: o sistema não precisa alocar um novo espaço de endereço para eles a cada vez e, em seguida, liberá-lo.

    Cada processo trabalha com seus próprios dados - eles podem trocar algo apenas por meio do mecanismo de comunicação entre processos. Os threads acessam os dados e recursos uns dos outros diretamente: o que um deles mudou está imediatamente disponível para todos. O thread pode controlar o "companheiro" no processo, enquanto o processo controla exclusivamente suas "filhas". Portanto, alternar entre os fluxos é mais rápido e a comunicação entre eles é mais fácil.

Qual é a conclusão disso? Se você precisar processar uma grande quantidade de dados o mais rápido possível, divida-os em pedaços que podem ser processados ​​por threads separados e, em seguida, reúna o resultado. É melhor do que gerar processos que consomem muitos recursos.

Mas por que um aplicativo popular como o Firefox segue o caminho de criar vários processos? Porque é para o navegador que as guias isoladas funcionam, são confiáveis ​​e flexíveis. Se algo estiver errado com um processo, não é necessário encerrar todo o programa - é possível salvar pelo menos parte dos dados.

O que é multithreading

Então chegamos ao ponto principal. Multithreading é quando o processo do aplicativo é dividido em threads que são processados ​​em paralelo - em uma unidade de tempo - pelo processador.

A carga computacional é distribuída entre dois ou mais núcleos, para que a interface e outros componentes do programa não tornem o trabalho do outro lento.

Aplicativos multi-threaded podem ser executados em processadores single-core, mas então os threads são executados por sua vez: o primeiro funcionou, seu estado foi salvo - o segundo foi autorizado a funcionar, salvo - retornou ao primeiro ou lançou o terceiro, etc.

Pessoas ocupadas reclamam que só têm duas mãos. Os processos e programas podem ter quantas mãos forem necessárias para completar a tarefa o mais rápido possível.

Aguarde um sinal: sincronização em aplicativos multithread

Imagine que vários threads estão tentando alterar a mesma área de dados ao mesmo tempo. Quais alterações serão eventualmente aceitas e quais alterações serão canceladas? Para evitar confusão com recursos compartilhados, os threads precisam coordenar suas ações. Para fazer isso, eles trocam informações por meio de sinais. Cada thread diz aos outros o que está fazendo e quais mudanças esperar. Portanto, os dados de todos os threads sobre o estado atual dos recursos são sincronizados.

Ferramentas Básicas de Sincronização

Exclusão mútua (exclusão mútua, abreviado - mutex) - um "sinalizador" indo para o segmento que atualmente tem permissão para trabalhar com recursos compartilhados. Elimina o acesso de outros threads à área de memória ocupada. Pode haver vários mutexes em um aplicativo e eles podem ser compartilhados entre os processos. Há um problema: o mutex força o aplicativo a acessar o kernel do sistema operacional todas as vezes, o que é caro.

Semáforo - permite que você limite o número de threads que podem acessar um recurso em um determinado momento. Isso reduzirá a carga no processador ao executar o código onde há gargalos. O problema é que o número ideal de threads depende da máquina do usuário.

Evento - você define uma condição na ocorrência de qual controle é transferido para o segmento desejado. Streams trocam dados de eventos para desenvolver e continuar logicamente as ações uns dos outros. Um recebia os dados, o outro verificava sua exatidão, o terceiro salvava no disco rígido. Os eventos diferem na maneira como são cancelados. Se você precisar notificar vários threads sobre um evento, terá que definir manualmente a função de cancelamento para interromper o sinal. Se houver apenas um segmento de destino, você pode criar um evento de reinicialização automática. Ele interromperá o próprio sinal após atingir o fluxo. Os eventos podem ser enfileirados para controle de fluxo flexível.

Seção Crítica - um mecanismo mais complexo que combina um contador de loop e um semáforo. O contador permite adiar o início do semáforo pelo tempo desejado. A vantagem é que o kernel só é ativado se a seção estiver ocupada e o semáforo precisar ser ativado. No resto do tempo, o thread é executado no modo de usuário. Infelizmente, uma seção só pode ser usada em um processo.

Como implementar multithreading em Java

A classe Thread é responsável por trabalhar com threads em Java. Criar um novo thread para executar uma tarefa significa criar uma instância da classe Thread e associá-la ao código que você deseja. Isso pode ser feito de duas maneiras:

    subclasse Thread;

    implemente a interface Runnable em sua classe e, a seguir, passe as instâncias da classe para o construtor Thread.

Embora não vamos tocar no tópico de deadlocks (deadlocks), quando os threads bloqueiam o trabalho um do outro e travam, deixaremos isso para o próximo artigo.

Exemplo de multithreading Java: ping pong com mutexes

Se você acha que algo terrível está para acontecer, expire. Consideraremos trabalhar com objetos de sincronização de maneira quase lúdica: dois threads serão lançados por um mutex. Mas, na verdade, você verá um aplicativo real onde apenas um thread pode processar dados publicamente disponíveis por vez.

Primeiro, vamos criar uma classe que herda as propriedades do Thread que já conhecemos e escrever um método kickBall:

Classe pública PingPongThread extends Thread (PingPongThread (String name) (this.setName (name); // substituir o nome do thread) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame () ) (kickBall (ball);)) private void kickBall (Ball ball) (if (! ball.getSide (). equals (getName ())) (ball.kick (getName ());)))

Agora vamos cuidar da bola. Não será simples connosco, mas memorável: para que saiba quem o atingiu, de que lado e quantas vezes. Para fazer isso, usamos um mutex: ele coletará informações sobre o trabalho de cada uma das threads - isso permitirá que threads isoladas se comuniquem entre si. Após o 15º toque, tiraremos a bola do jogo, para não a lesionar gravemente.

Classe pública Ball (private int kicks = 0; private static Ball instance = new Ball (); private String side = ""; private Ball () () static Ball getBall () (return instance;) synchronized void kick (String playername) (kicks ++; side = playername; System.out.println (kicks + "" + side);) String getSide () (return side;) boolean isngame () (return (kicks< 15); } }

E agora dois tópicos de jogadores estão entrando em cena. Vamos chamá-los, sem mais delongas, de Ping e Pong:

Classe pública PingPongGame (PingPongThread player1 = new PingPongThread ("Ping"); PingPongThread player2 = new PingPongThread ("Pong"); Ball ball; PingPongGame () (ball = Ball.getBall ();) void startGame () lança InterruptedException (player1 .start (); player2.start ();))

"Estádio cheio de pessoas - hora de começar a partida." Anunciaremos oficialmente a abertura da reunião - na turma principal do aplicativo:

Classe pública PingPong (public static void main (String args) lança InterruptedException (PingPongGame game = new PingPongGame (); game.startGame ();))

Como você pode ver, não há nada de furioso aqui. Esta é apenas uma introdução ao multithreading por enquanto, mas você já sabe como funciona e pode experimentar - limite a duração do jogo não pelo número de tacadas, mas pelo tempo, por exemplo. Voltaremos ao tópico de multithreading mais tarde - veremos o pacote java.util.concurrent, a biblioteca Akka e o mecanismo volátil. Também vamos falar sobre a implementação de multithreading em Python.

A programação multithread não é fundamentalmente diferente de escrever interfaces gráficas de usuário orientadas a eventos, ou mesmo escrever aplicativos sequenciais simples. Todas as regras importantes que regem o encapsulamento, separação de interesses, acoplamento fraco, etc. se aplicam aqui. Mas muitos desenvolvedores acham difícil escrever programas multithread precisamente porque eles negligenciam essas regras. Em vez disso, eles estão tentando colocar em prática o conhecimento muito menos importante sobre threads e primitivos de sincronização, colhido nos textos sobre programação multithreading para iniciantes.

Então, quais são essas regras

Outro programador, diante de um problema, pensa: "Oh, exatamente, precisamos aplicar expressões regulares." E agora ele já tem dois problemas - Jamie Zawinski.

Outro programador, diante de um problema, pensa: "Ah, certo, vou usar streams aqui." E agora ele tem dez problemas - Bill Schindler.

Muitos programadores que se comprometem a escrever código multi-threaded caem na armadilha, como o herói da balada de Goethe " O aprendiz de feiticeiro" O programador aprenderá a criar um monte de threads que, em princípio, funcionam, mas mais cedo ou mais tarde ficam fora de controle e o programador não sabe o que fazer.

Mas, ao contrário de um feiticeiro abandonado, o infeliz programador não pode esperar a chegada de um poderoso feiticeiro que agitará sua varinha e restaurará a ordem. Em vez disso, o programador vai para os truques mais desagradáveis, tentando lidar com problemas que surgem constantemente. O resultado é sempre o mesmo: uma aplicação excessivamente complicada, limitada, frágil e não confiável é obtida. Ele tem a ameaça perpétua de deadlock e outros perigos inerentes ao código multithreading incorreto. Eu nem estou falando sobre travamentos inexplicáveis, desempenho ruim, resultados de trabalho incompletos ou incorretos.

Você pode ter se perguntado: por que isso está acontecendo? Um equívoco comum é: "A programação multithread é muito difícil." Mas este não é o caso. Se um programa multithread não for confiável, ele geralmente falhará pelos mesmos motivos que um programa single-thread de baixa qualidade. Só que o programador não segue os métodos de desenvolvimento básicos, bem conhecidos e comprovados. Programas multithread apenas parecem ser mais complexos, porque quanto mais threads paralelos dão errado, mais bagunça eles fazem - e muito mais rápido do que um único thread faria.

O equívoco sobre "a complexidade da programação multithread" se espalhou devido aos desenvolvedores que se desenvolveram profissionalmente ao escrever código de thread único, primeiro encontraram multithreading e não conseguiram lidar com isso. Mas em vez de repensar seus preconceitos e hábitos de trabalho, eles teimosamente corrigem o fato de que não querem trabalhar de forma alguma. Dando desculpas para software não confiável e prazos perdidos, essas pessoas repetem a mesma coisa: "a programação multithread é muito difícil."

Observe que acima estou falando sobre programas típicos que usam multithreading. Na verdade, existem cenários complexos de vários segmentos - bem como cenários complexos de um único segmento. Mas eles não são comuns. Como regra, na prática, nada sobrenatural é exigido do programador. Movemos os dados, transformamos, fazemos alguns cálculos de vez em quando e, por fim, salvamos as informações em um banco de dados ou exibimos na tela.

Não há nada difícil em melhorar o programa comum de thread único e transformá-lo em um multi-thread. Pelo menos não deveria ser. As dificuldades surgem por dois motivos:

  • os programadores não sabem como aplicar métodos de desenvolvimento comprovados simples e bem conhecidos;
  • a maioria das informações apresentadas nos livros sobre programação multithread é tecnicamente correta, mas completamente inaplicável para resolver problemas aplicados.

Os conceitos de programação mais importantes são universais. Eles são igualmente aplicáveis ​​a programas single-threaded e multi-threaded. Os programadores que se afogam em um turbilhão de fluxos simplesmente não aprenderam lições importantes quando dominaram o código de thread único. Posso dizer isso porque esses desenvolvedores cometem os mesmos erros fundamentais em programas multi-threaded e single-threaded.

Talvez a lição mais importante a ser aprendida em sessenta anos de história da programação seja: estado mutável global- mal... Mal de verdade. Os programas que dependem do estado globalmente mutável são relativamente difíceis de raciocinar e geralmente não confiáveis ​​porque existem muitas maneiras de alterar o estado. Existem muitos estudos que confirmam este princípio geral, existem inúmeros padrões de design, cujo objetivo principal é implementar uma forma ou outra de ocultação de dados. Para tornar seus programas mais previsíveis, tente eliminar o estado mutável tanto quanto possível.

Em um programa sequencial de encadeamento único, a probabilidade de corrupção de dados é diretamente proporcional ao número de componentes que podem modificar os dados.

Via de regra, não é possível se livrar completamente do estado global, mas o desenvolvedor possui ferramentas muito eficazes em seu arsenal que permitem controlar estritamente quais componentes do programa podem alterar o estado. Além disso, aprendemos como criar camadas de API restritivas em torno de estruturas de dados primitivas. Portanto, temos um bom controle sobre como essas estruturas de dados mudam.

Os problemas do estado globalmente mutável gradualmente se tornaram aparentes no final dos anos 80 e início dos anos 90, com a proliferação da programação orientada a eventos. Os programas não começavam mais "do início" ou seguiam um caminho único e previsível de execução "até o fim". Os programas modernos têm um estado inicial, após a saída de onde os eventos ocorrem neles - em uma ordem imprevisível, com intervalos de tempo variáveis. O código permanece com um único thread, mas já se torna assíncrono. A probabilidade de corrupção de dados aumenta exatamente porque a ordem de ocorrência dos eventos é muito importante. Situações desse tipo são bastante comuns: se o evento B ocorrer após o evento A, então tudo funciona bem. Mas se o evento A ocorre após o evento B, e o evento C tem tempo para intervir entre eles, então os dados podem ser distorcidos além do reconhecimento.

Se fluxos paralelos estiverem envolvidos, o problema é ainda mais exacerbado, uma vez que vários métodos podem operar simultaneamente no estado global. Torna-se impossível julgar exatamente como o estado global muda. Já estamos falando não apenas sobre o fato de que os eventos podem ocorrer em uma ordem imprevisível, mas também sobre o fato de que o estado de vários threads de execução pode ser atualizado. simultaneamente... Com a programação assíncrona, você pode, no mínimo, garantir que um determinado evento não aconteça antes que outro evento tenha concluído o processamento. Ou seja, é possível dizer com certeza qual será o estado global ao final do processamento de um determinado evento. Em código multithread, como regra, é impossível dizer quais eventos ocorrerão em paralelo, portanto, é impossível descrever com certeza o estado global em um determinado momento.

Um programa multithread com extenso estado globalmente mutável é um dos exemplos mais eloqüentes do princípio da incerteza de Heisenberg que conheço. É impossível verificar o estado de um programa sem alterar seu comportamento.

Quando eu começo outra filípica sobre o estado mutável global (a essência é delineada em vários parágrafos anteriores), os programadores reviram os olhos e me garantem que já sabem de tudo isso há muito tempo. Mas se você sabe disso, por que não pode saber pelo seu código? Os programas estão repletos de estados mutáveis ​​globais e os programadores se perguntam por que o código não funciona.

Não é novidade que o trabalho mais importante na programação multithread ocorre durante a fase de design. É necessário definir claramente o que o programa deve fazer, desenvolver módulos independentes para executar todas as funções, descrever em detalhes quais dados são necessários para cada módulo e determinar as formas de troca de informações entre os módulos ( Sim, não se esqueça de preparar belas camisetas para todos os envolvidos no projeto. Primeira coisa.- Aproximadamente. ed. no original) Esse processo não é fundamentalmente diferente do projeto de um programa de thread único. A chave para o sucesso, como acontece com o código de thread único, é limitar as interações entre os módulos. Se você puder se livrar do estado mutável compartilhado, os problemas de compartilhamento de dados simplesmente não surgirão.

Alguém poderia argumentar que às vezes não há tempo para um desenho tão delicado do programa, que tornará possível prescindir do estado global. Acredito que seja possível e necessário dedicar algum tempo a isso. Nada afeta os programas multithread de forma tão destrutiva quanto tentar lidar com o estado mutável global. Quanto mais detalhes você precisa gerenciar, maior a probabilidade de seu programa atingir o pico e travar.

Em aplicativos realistas, deve haver algum tipo de estado compartilhado que pode mudar. E é aqui que a maioria dos programadores começa a ter problemas. O programador vê que um estado compartilhado é necessário aqui, volta-se para o arsenal multithread e tira daí a ferramenta mais simples: um bloqueio universal (seção crítica, mutex ou o que quer que eles chamem). Eles parecem acreditar que a exclusão mútua resolverá todos os problemas de compartilhamento de dados.

O número de problemas que podem surgir com essa única fechadura é impressionante. Deve-se levar em consideração as condições de corrida, problemas de gating com bloqueios excessivamente extensos e questões de justiça de alocação são apenas alguns exemplos. Se você tiver vários bloqueios, especialmente se eles estiverem aninhados, você também precisará tomar medidas contra deadlocking, deadlocking dinâmico, filas de bloqueio e outras ameaças associadas à simultaneidade. Além disso, existem problemas inerentes de bloqueio único.
Quando escrevo ou reviso código, tenho uma regra de ferro quase infalível: se você fez um bloqueio, então parece que você cometeu um erro em algum lugar.

Esta afirmação pode ser comentada de duas maneiras:

  1. Se você precisar de bloqueio, provavelmente tem um estado mutável global que deseja proteger contra atualizações simultâneas. A presença de estado mutável global é uma falha na fase de design do aplicativo. Revisão e redesenho.
  2. Usar bloqueios corretamente não é fácil e pode ser incrivelmente difícil isolar bugs de bloqueio. É muito provável que você esteja usando a fechadura incorretamente. Se eu vir um bloqueio e o programa se comportar de maneira incomum, a primeira coisa que faço é verificar o código que depende do bloqueio. E geralmente encontro problemas nisso.

Ambas as interpretações estão corretas.

Escrever código multithread é fácil. Mas é muito, muito difícil usar primitivas de sincronização corretamente. Talvez você não esteja qualificado para usar nem mesmo uma fechadura corretamente. Afinal, os bloqueios e outras primitivas de sincronização são construções erguidas no nível de todo o sistema. Pessoas que entendem de programação paralela muito melhor do que você, usam essas primitivas para construir estruturas de dados simultâneas e construções de sincronização de alto nível. E você e eu, programadores comuns, apenas pegamos essas construções e as usamos em nosso código. Um programador de aplicativo não deve usar primitivas de sincronização de baixo nível com mais freqüência do que faz chamadas diretas para drivers de dispositivo. Ou seja, quase nunca.

Tentar usar bloqueios para resolver problemas de compartilhamento de dados é como apagar um incêndio com oxigênio líquido. Como um incêndio, esses problemas são mais fáceis de prevenir do que consertar. Se você se livrar do estado compartilhado, também não precisará abusar dos primitivos de sincronização.

Muito do que você sabe sobre multithreading é irrelevante

Nos tutoriais de multithreading para iniciantes, você aprenderá o que são threads. Em seguida, o autor começará a considerar várias maneiras pelas quais esses threads podem funcionar em paralelo - por exemplo, falar sobre como controlar o acesso a dados compartilhados usando bloqueios e semáforos e se concentrar nas coisas que podem acontecer ao trabalhar com eventos. Analisará de perto as variáveis ​​de condição, barreiras de memória, seções críticas, mutexes, campos voláteis e operações atômicas. Exemplos de como usar essas construções de baixo nível para realizar todos os tipos de operações do sistema serão discutidos. Tendo lido este material pela metade, o programador decide que já sabe o suficiente sobre todas essas primitivas e seu uso. Afinal, se eu sei como isso funciona no nível do sistema, posso aplicá-lo da mesma forma no nível do aplicativo. Sim?

Imagine dizer a um adolescente como montar sozinho um motor de combustão interna. Então, sem nenhum treinamento de direção, você o coloca atrás do volante de um carro e diz: "Dirija!" O adolescente entende como um carro funciona, mas não tem ideia de como ir do ponto A ao ponto B nele.

Compreender como os threads funcionam no nível do sistema geralmente não ajuda de forma alguma no nível do aplicativo. Não estou sugerindo que os programadores não precisem aprender todos esses detalhes de baixo nível. Só não espere ser capaz de aplicar esse conhecimento logo de cara ao projetar ou desenvolver um aplicativo de negócios.

A literatura introdutória sobre threading (e cursos acadêmicos relacionados) não deve explorar tais construções de baixo nível. Você precisa se concentrar em resolver as classes de problemas mais comuns e mostrar aos desenvolvedores como esses problemas são resolvidos usando recursos de alto nível. Em princípio, a maioria dos aplicativos de negócios são programas extremamente simples. Eles leem dados de um ou mais dispositivos de entrada, realizam algum processamento complexo nesses dados (por exemplo, no processo, eles solicitam mais dados) e, em seguida, geram os resultados.

Freqüentemente, esses programas se encaixam perfeitamente no modelo provedor-consumidor, que requer apenas três threads:

  • o fluxo de entrada lê os dados e os coloca na fila de entrada;
  • um thread de trabalho lê registros da fila de entrada, os processa e coloca os resultados na fila de saída;
  • o fluxo de saída lê as entradas da fila de saída e as salva.

Esses três threads funcionam de forma independente, a comunicação entre eles ocorre no nível da fila.

Embora tecnicamente essas filas possam ser consideradas como zonas de estado compartilhado, na prática elas são apenas canais de comunicação nos quais opera sua própria sincronização interna. As filas suportam trabalhar com muitos produtores e consumidores ao mesmo tempo, você pode adicionar e remover itens nelas em paralelo.

Como os estágios de entrada, processamento e saída são isolados uns dos outros, sua implementação pode ser facilmente alterada sem afetar o resto do programa. Contanto que o tipo de dados na fila não mude, você pode refatorar componentes individuais do programa a seu critério. Além disso, como um número arbitrário de fornecedores e consumidores participa da fila, não é difícil adicionar outros produtores / consumidores. Podemos ter dezenas de fluxos de entrada gravando informações na mesma fila, ou dezenas de threads de trabalho obtendo informações da fila de entrada e compilando dados. Na estrutura de um único computador, esse modelo é bem dimensionado.

Mais importante, bibliotecas e linguagens de programação modernas tornam muito fácil criar aplicativos produtor-consumidor. Em .NET, você encontrará Parallel Collections e a TPL Dataflow Library. Java tem o serviço Executor, bem como BlockingQueue e outras classes do namespace java.util.concurrent. C ++ tem uma biblioteca de threading Boost e a biblioteca Thread Building Blocks da Intel. O Visual Studio 2013 da Microsoft apresenta agentes assíncronos. Bibliotecas semelhantes também estão disponíveis em Python, JavaScript, Ruby, PHP e, até onde eu sei, muitas outras linguagens. Você pode criar um aplicativo produtor-consumidor usando qualquer um desses pacotes, sem nunca ter que recorrer a bloqueios, semáforos, variáveis ​​de condição ou quaisquer outras primitivas de sincronização.

Uma grande variedade de primitivas de sincronização são usadas livremente nessas bibliotecas. Isto é bom. Todas essas bibliotecas são escritas por pessoas que entendem de multithreading incomparavelmente melhor do que o programador médio. Trabalhar com essa biblioteca é praticamente o mesmo que usar uma biblioteca de linguagem de tempo de execução. Isso pode ser comparado à programação em uma linguagem de alto nível, em vez da linguagem assembly.

O modelo fornecedor-consumidor é apenas um de muitos exemplos. As bibliotecas acima contêm classes que podem ser usadas para implementar muitos dos padrões de design de threading comuns sem entrar em detalhes de baixo nível. É possível criar aplicativos multithread em grande escala sem se preocupar em como os threads são coordenados e sincronizados.

Trabalhe com bibliotecas

Portanto, criar programas multithread não é fundamentalmente diferente de escrever programas síncronos single-threaded. Os princípios importantes de encapsulamento e ocultação de dados são universais e só aumentam de importância quando vários threads simultâneos estão envolvidos. Se você negligenciar esses aspectos importantes, mesmo o conhecimento mais abrangente de segmentação de baixo nível não o salvará.

Os desenvolvedores modernos têm que resolver muitos problemas no nível da programação do aplicativo; acontece que simplesmente não há tempo para pensar sobre o que está acontecendo no nível do sistema. Quanto mais complicados os aplicativos ficam, mais detalhes complexos devem ser ocultados entre os níveis de API. Fazemos isso há mais de uma dúzia de anos. Pode-se argumentar que o ocultamento qualitativo da complexidade do sistema do programador é a principal razão pela qual o programador é capaz de escrever aplicativos modernos. Por falar nisso, não estamos escondendo a complexidade do sistema implementando o loop de mensagens da IU, criando protocolos de comunicação de baixo nível, etc.?

A situação é semelhante com multithreading. A maioria dos cenários multiencadeados que o programador médio de aplicativos de negócios pode encontrar já são bem conhecidos e bem implementados em bibliotecas. As funções de biblioteca fazem um ótimo trabalho em esconder a complexidade avassaladora do paralelismo. Você precisa aprender a usar essas bibliotecas da mesma forma que usa bibliotecas de elementos de interface do usuário, protocolos de comunicação e várias outras ferramentas que funcionam. Deixe o multithreading de baixo nível para os especialistas - os autores das bibliotecas usadas na criação de aplicativos.

NS Este artigo não é para domadores de Python experientes, para quem desvendar essa bola de cobras é brincadeira de criança, mas sim uma visão geral superficial dos recursos de multithreading para python recém-viciado.

Infelizmente, não há muito material em russo sobre o tópico de multithreading em Python, e pythoners que não tinham ouvido nada, por exemplo, sobre o GIL, começaram a vir para mim com regularidade invejável. Neste artigo, tentarei descrever os recursos mais básicos do python multithread, contarei o que é o GIL e como viver com ele (ou sem ele) e muito mais.


Python é uma linguagem de programação charmosa. Ele combina perfeitamente muitos paradigmas de programação. Muitas das tarefas que um programador pode enfrentar são resolvidas aqui de forma fácil, elegante e concisa. Mas, para todos esses problemas, uma solução de thread único geralmente é suficiente, e os programas de thread único geralmente são previsíveis e fáceis de depurar. O mesmo não pode ser dito sobre programas multithread e multiprocessamento.

Aplicativos multithread


Python tem um módulo enfiar , e tem tudo que você precisa para programação multithread: há vários tipos de bloqueios e um semáforo e um mecanismo de evento. Em uma palavra - tudo o que é necessário para a grande maioria dos programas multithread. Além disso, usar todas essas ferramentas é bastante simples. Vamos considerar um exemplo de programa que inicia 2 threads. Um tópico escreve dez "0", o outro - dez "1" e estritamente por sua vez.

importar threading

escritor def

para i em xrange (10):

imprimir x

Event_for_set.set ()

# eventos init

e1 = threading.Event ()

e2 = threading.Event ()

# threads de inicialização

0, e1, e2))

1, e2, e1))

# iniciar tópicos

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Nenhum código mágico ou vodu. O código é claro e consistente. Além disso, como você pode ver, criamos um fluxo de uma função. Isso é muito conveniente para pequenas tarefas. Este código também é bastante flexível. Suponha que temos um terceiro processo que grava "2", então o código será semelhante a este:

importar threading

escritor def (x, event_for_wait, event_for_set):

para i em xrange (10):

Event_for_wait.wait () # esperar por evento

Event_for_wait.clear () # evento limpo para futuro

imprimir x

Event_for_set.set () # definir evento para tópico vizinho

# eventos init

e1 = threading.Event ()

e2 = threading.Event ()

e3 = threading.Event ()

# threads de inicialização

t1 = threading.Thread (target = escritor, args = ( 0, e1, e2))

t2 = threading.Thread (target = escritor, args = ( 1, e2, e3))

t3 = threading.Thread (target = escritor, args = ( 2, e3, e1))

# iniciar tópicos

t1.start ()

t2.start ()

t3.start ()

e1.set () # inicia o primeiro evento

# une tópicos ao tópico principal

t1.join ()

t2.join ()

t3.join ()


Adicionamos um novo evento, um novo tópico e alteramos ligeiramente os parâmetros com os quais
streams start (é claro, você pode escrever uma solução mais geral usando, por exemplo, MapReduce, mas isso está além do escopo deste artigo).
Como você pode ver, ainda não há mágica. Tudo é simples e direto. Vamos mais longe.

Bloqueio global de intérprete


Existem duas razões mais comuns para o uso de threads: primeiro, para aumentar a eficiência do uso da arquitetura multicore dos processadores modernos e, portanto, o desempenho do programa;
em segundo lugar, se precisarmos dividir a lógica do programa em seções paralelas, total ou parcialmente assíncronas (por exemplo, para poder executar ping em vários servidores ao mesmo tempo).

No primeiro caso, nos deparamos com uma limitação do Python (ou melhor, sua principal implementação CPython) como o Global Interpreter Lock (ou GIL para abreviar). O conceito do GIL é que apenas um thread pode ser executado por um processador por vez. Isso é feito para que não haja luta entre threads para variáveis ​​separadas. O thread executável obtém acesso a todo o ambiente. Este recurso de implementação de thread em Python simplifica muito o trabalho com threads e oferece uma certa segurança de thread.

Mas há um ponto sutil: pode parecer que um aplicativo multithread rodará exatamente a mesma quantidade de tempo que um aplicativo single-threaded fazendo o mesmo, ou a soma do tempo de execução de cada thread na CPU. Mas aqui um efeito desagradável nos espera. Considere o programa:

com open ("test1.txt", "w") como fout:

para i em xrange (1000000):

imprimir >> fout, 1


Este programa apenas grava um milhão de linhas “1” em um arquivo e faz isso em aproximadamente 0,35 segundos no meu computador.

Considere outro programa:

de discussão de importação de discussão

escritor def (nome do arquivo, n):

com aberto (nome do arquivo, "w") como fout:

para i em xrange (n):

imprimir >> fout, 1

t1 = Tópico (destino = escritor, args = ("test2.txt", 500000,))

t2 = Tópico (destino = escritor, args = ("test3.txt", 500000,))

t1.start ()

t2.start ()

t1.join ()

t2.join ()


Este programa cria 2 threads. Em cada thread, ele grava em um arquivo separado meio milhão de linhas "1". Na verdade, a quantidade de trabalho é a mesma do programa anterior. Mas com o tempo, um efeito interessante é obtido aqui. O programa pode ser executado de 0,7 segundos a até 7 segundos. Por que isso está acontecendo?

Isso se deve ao fato de que quando um thread não precisa de um recurso da CPU, ele libera o GIL, e nesse momento pode tentar pegá-lo, e outro thread, e também o thread principal. Ao mesmo tempo, o sistema operacional, sabendo que existem muitos núcleos, pode agravar tudo tentando distribuir threads entre os núcleos.

UPD: no momento, em Python 3.2 existe uma implementação melhorada do GIL, em que este problema é parcialmente resolvido, em particular, devido ao fato de que cada thread, após perder o controle, aguarda um curto período de tempo antes de pode capturar o GIL novamente (há uma boa apresentação em inglês)

“Então você não pode escrever programas multithread eficientes em Python?” Você pergunta. Não, claro, há uma saída, e até várias.

Aplicativos de multiprocessamento


Para resolver de alguma forma o problema descrito no parágrafo anterior, Python tem um módulo subprocesso ... Podemos escrever um programa que queremos executar em uma thread paralela (na verdade, já é um processo). E execute-o em um ou mais threads em outro programa. Isso realmente aceleraria nosso programa, porque os threads criados no iniciador GIL não são ativados, mas apenas aguardam o término do processo em execução. No entanto, esse método tem muitos problemas. O principal problema é que fica difícil transferir dados entre processos. Você teria que serializar objetos de alguma forma, estabelecer comunicação por meio do PIPE ou outras ferramentas, mas tudo isso inevitavelmente carrega sobrecarga e o código se torna difícil de entender.

Outra abordagem pode nos ajudar aqui. Python tem um módulo de multiprocessamento ... Em termos de funcionalidade, este módulo se assemelha enfiar ... Por exemplo, os processos podem ser criados da mesma maneira a partir de funções regulares. Os métodos para trabalhar com processos são quase os mesmos que para threads do módulo de threading. Porém, para sincronização de processos e troca de dados, costuma-se usar outras ferramentas. Estamos falando de filas (Queue) e pipes (Pipe). No entanto, análogos de bloqueios, eventos e semáforos que estavam em threading também estão aqui.

Além disso, o módulo de multiprocessamento possui um mecanismo para trabalhar com memória compartilhada. Para isso, o módulo possui classes de uma variável (Value) e um array (Array), que podem ser “compartilhados” entre os processos. Para a conveniência de trabalhar com variáveis ​​compartilhadas, você pode usar as classes de gerenciador. Eles são mais flexíveis e fáceis de usar, mas mais lentos. Deve-se notar que existe uma boa oportunidade de criar tipos comuns do módulo ctypes usando o módulo multiprocessing.sharedctypes.

Também no módulo de multiprocessamento existe um mecanismo para a criação de pools de processos. Este mecanismo é muito conveniente de usar para implementar o padrão Master-Worker ou para implementar um Map paralelo (que de certa forma é um caso especial de Master-Worker).

Dos principais problemas em trabalhar com o módulo de multiprocessamento, vale a pena notar a dependência relativa da plataforma deste módulo. Como o trabalho com processos é organizado de maneira diferente em diferentes sistemas operacionais, algumas restrições são impostas ao código. Por exemplo, o Windows não tem um mecanismo de bifurcação, então o ponto de separação do processo deve ser envolvido em:

if __name__ == "__main__":


No entanto, este design já é uma boa forma.

O que mais...


Existem outras bibliotecas e abordagens para escrever aplicativos paralelos em Python. Por exemplo, você pode usar Hadoop + Python ou várias implementações Python MPI (pyMPI, mpi4py). Você pode até usar invólucros de bibliotecas C ++ ou Fortran existentes. Aqui pode-se citar frameworks / bibliotecas como Pyro, Twisted, Tornado e muitos outros. Mas tudo isso já está além do escopo deste artigo.

Se você gostou do meu estilo, no próximo artigo tentarei dizer a você como escrever intérpretes simples em PLY e para que podem ser usados.

Capítulo # 10.

Aplicativos multithread

Multitarefa em sistemas operacionais modernos é um dado adquirido [ Antes do advento do Apple OS X, não havia sistemas operacionais multitarefa modernos em computadores Macintosh. É muito difícil projetar adequadamente um sistema operacional com multitarefa completa, então o OS X teve que ser baseado no sistema Unix.] O usuário espera que quando o editor de texto e o cliente de e-mail forem iniciados ao mesmo tempo, esses programas não entrem em conflito e, ao receber e-mail, o editor não pare de funcionar. Quando vários programas são iniciados ao mesmo tempo, o sistema operacional alterna rapidamente entre os programas, fornecendo-lhes um processador por sua vez (a menos, é claro, que vários processadores estejam instalados no computador). Como resultado, ilusão rodando vários programas ao mesmo tempo, porque mesmo o melhor digitador (e a conexão de internet mais rápida) não consegue acompanhar um processador moderno.

Multithreading, em certo sentido, pode ser visto como o próximo nível de multitarefa: em vez de alternar entre diferentes programas, o sistema operacional alterna entre diferentes partes do mesmo programa. Por exemplo, um cliente de e-mail multithread permite que você receba novas mensagens de e-mail enquanto lê ou redige novas mensagens. Hoje em dia, o multithreading também é considerado um dado adquirido por muitos usuários.

O VB nunca teve suporte multithreading normal. É verdade que uma de suas variedades apareceu em VB5 - modelo de streaming colaborativo(threading apartamento). Como você verá em breve, o modelo colaborativo fornece ao programador alguns dos benefícios do multithreading, mas não tira o máximo proveito de todos os recursos. Mais cedo ou mais tarde, você terá que mudar de uma máquina de treinamento para uma real, e o VB .NET se tornou a primeira versão do VB com suporte para um modelo multithreaded gratuito.

No entanto, multithreading não é um dos recursos que são facilmente implementados em linguagens de programação e facilmente dominados por programadores. Porque?

Porque em aplicativos multithread, podem ocorrer erros muito complicados que aparecem e desaparecem de forma imprevisível (e esses erros são os mais difíceis de depurar).

Aviso: multithreading é uma das áreas mais difíceis da programação. A menor desatenção leva ao aparecimento de erros evasivos, cuja correção exige somas astronômicas. Por esta razão, este capítulo contém muitos mau exemplos - nós os escrevemos deliberadamente de forma a demonstrar erros comuns. Esta é a abordagem mais segura para aprender programação multithread: você deve ser capaz de detectar problemas potenciais quando tudo parece funcionar bem à primeira vista e saber como resolvê-los. Se você deseja usar técnicas de programação multithread, não pode ficar sem elas.

Este capítulo estabelecerá uma base sólida para trabalho independente posterior, mas não seremos capazes de descrever a programação multithread em todas as complexidades - apenas a documentação impressa nas classes do namespace Threading leva mais de 100 páginas. Se você deseja dominar a programação multithread em um nível superior, consulte livros especializados.

Porém, por mais perigosa que seja a programação multithread, ela é indispensável para a solução profissional de alguns problemas. Se seus programas não usam multithreading quando apropriado, os usuários ficarão muito frustrados e preferirão outro produto. Por exemplo, apenas na quarta versão do popular programa de e-mail Eudora surgiram capacidades multithread, sem as quais é impossível imaginar qualquer programa moderno para trabalhar com e-mail. Quando o Eudora introduziu o suporte multithreading, muitos usuários (incluindo um dos autores deste livro) haviam mudado para outros produtos.

Finalmente, no .NET, os programas de thread único simplesmente não existem. Tudo Os programas .NET são multithread porque o coletor de lixo é executado como um processo em segundo plano de baixa prioridade. Conforme mostrado abaixo, para programação gráfica séria em .NET, o encadeamento adequado pode ajudar a evitar que a interface gráfica seja bloqueada quando o programa estiver executando operações demoradas.

Apresentando multithreading

Cada programa funciona em um específico contexto, descrevendo a distribuição de código e dados na memória. Quando você salva o contexto, o estado do fluxo do programa é realmente salvo, o que permite restaurá-lo no futuro e continuar a execução do programa.

Salvar o contexto tem um custo de tempo e memória. O sistema operacional lembra o estado do encadeamento do programa e transfere o controle para outro encadeamento. Quando o programa deseja continuar executando a thread suspensa, o contexto salvo deve ser restaurado, o que leva ainda mais tempo. Portanto, multithreading só deve ser usado quando os benefícios compensam todos os custos. Alguns exemplos típicos estão listados abaixo.

  • A funcionalidade do programa é clara e naturalmente dividida em várias operações heterogêneas, como no exemplo do recebimento de e-mail e preparação de novas mensagens.
  • O programa executa cálculos longos e complexos e você não deseja que a interface gráfica seja bloqueada durante os cálculos.
  • O programa é executado em um computador multiprocessador com um sistema operacional que suporta o uso de vários processadores (desde que o número de threads ativos não exceda o número de processadores, a execução paralela é virtualmente livre da sobrecarga associada à troca de threads).

Antes de passar para a mecânica dos programas multithread, é necessário apontar uma circunstância que freqüentemente causa confusão entre os iniciantes no campo da programação multithread.

Um procedimento, não um objeto, é executado no fluxo do programa.

É difícil dizer o que significa a expressão "objeto em execução", mas um dos autores costuma dar seminários sobre programação multithreading e essa pergunta é feita com mais frequência do que outras. Talvez alguém pense que o trabalho do encadeamento do programa começa com uma chamada ao método New da classe, após o que o encadeamento processa todas as mensagens passadas para o objeto correspondente. Tais representações absolutamente está errado. Um objeto pode conter vários threads que executam métodos diferentes (e às vezes até os mesmos), enquanto as mensagens do objeto são transmitidas e recebidas por vários threads diferentes (aliás, este é um dos motivos que complicam a programação multi-threaded: para depurar um programa, você precisa descobrir qual thread está em um determinado momento realiza este ou aquele procedimento!).

Como os threads são criados a partir de métodos de objetos, o próprio objeto geralmente é criado antes do thread. Depois de criar o objeto com sucesso, o programa cria um thread, passando o endereço do método do objeto, e só depois disso dá a ordem para iniciar a execução da thread. O procedimento para o qual o thread foi criado, como todos os procedimentos, pode criar novos objetos, realizar operações em objetos existentes e chamar outros procedimentos e funções que estão em seu escopo.

Métodos comuns de classes também podem ser executados em threads de programa. Neste caso, lembre-se também de outra circunstância importante: o thread termina com uma saída do procedimento para o qual foi criado. O encerramento normal do fluxo do programa não é possível até que o procedimento seja encerrado.

Threads podem terminar não apenas naturalmente, mas também de forma anormal. Isso geralmente não é recomendado. Consulte Encerrando e interrompendo fluxos para obter mais informações.

Os principais recursos do .NET relacionados ao uso de threads programáticos estão concentrados no namespace Threading. Portanto, a maioria dos programas multithread deve começar com a seguinte linha:

Imports System.Threading

Importar um namespace torna seu programa mais fácil de digitar e habilita a tecnologia IntelliSense.

A conexão direta de fluxos com procedimentos sugere que, nesta foto, delegados(veja o capítulo 6). Especificamente, o namespace Threading inclui o delegado ThreadStart, que normalmente é usado ao iniciar threads de programa. A sintaxe para usar este delegado é assim:

Delegado público Sub ThreadStart ()

O código chamado com o delegado ThreadStart não deve ter parâmetros ou valor de retorno, portanto, os threads não podem ser criados para funções (que retornam um valor) e para procedimentos com parâmetros. Para transferir informações do fluxo, você também deve procurar meios alternativos, uma vez que os métodos executados não retornam valores e não podem usar transferência por referência. Por exemplo, se ThreadMethod estiver na classe WilluseThread, então o ThreadMethod pode comunicar informações modificando as propriedades das instâncias da classe WillUseThread.

Domínios de aplicativo

Os threads do .NET são executados nos chamados domínios de aplicativo, definidos na documentação como "a caixa de proteção em que o aplicativo é executado". Um domínio de aplicativo pode ser considerado uma versão leve dos processos Win32; um único processo Win32 pode conter vários domínios de aplicativo. A principal diferença entre domínios de aplicativo e processos é que um processo Win32 tem seu próprio espaço de endereço (na documentação, os domínios de aplicativo também são comparados a processos lógicos executados dentro de um processo físico). No NET, todo o gerenciamento de memória é feito pelo tempo de execução, portanto, vários domínios de aplicativo podem ser executados em um único processo Win32. Um dos benefícios desse esquema são os recursos de escalonamento aprimorados dos aplicativos. As ferramentas para trabalhar com domínios de aplicativo estão na classe AppDomain. Recomendamos que você estude a documentação para esta aula. Com sua ajuda, você pode obter informações sobre o ambiente em que seu programa está sendo executado. Em particular, a classe AppDomain é usada ao realizar reflexão nas classes do sistema .NET. O programa a seguir lista os assemblies carregados.

Imports System.Reflection

Módulo Módulo

Sub principal ()

Dim theDomain como AppDomain

theDomain = AppDomain.CurrentDomain

Dim Assemblies () As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

Para Cada Montagem Em Assembleias

Console.WriteLinetanAssembly.Full Name) Next

Console.ReadLine ()

End Sub

Módulo Final

Criando streams

Vamos começar com um exemplo rudimentar. Digamos que você queira executar um procedimento em uma thread separada que diminui o valor do contador em um loop infinito. O procedimento é definido como parte da aula:

Classe pública WillUseThreads

Public Sub SubtractFromCounter ()

Dim count As Integer

Do While True count - = 1

Console.WriteLlne ("Estou em outro tópico e counter ="

& contar)

Ciclo

End Sub

Fim da aula

Como a condição do loop Do é sempre verdadeira, você pode pensar que nada interferirá no procedimento SubtractFromCounter. No entanto, em um aplicativo multithread, nem sempre é esse o caso.

O snippet a seguir mostra o procedimento Sub Main que inicia o thread e o comando Imports:

Option Strict On Imports System.Threading Module Modulel

Sub principal ()

1 Dim myTest As New WillUseThreads ()

2 Dim bThreadStart As New ThreadStart (AddressOf _

myTest.SubtractFromCounter)

3 Dim bThread como novo tópico (bThreadStart)

4 "bThread.Start ()

Dim i As Integer

5 Faça Enquanto Verdadeiro

Console.WriteLine ("No thread principal e a contagem é" & i) i + = 1

Ciclo

End Sub

Módulo Final

Vamos dar uma olhada nos pontos mais importantes na sequência. Em primeiro lugar, o procedimento Sub Man n sempre funciona em convencional(tópico principal). Em programas .NET, sempre há pelo menos dois threads em execução: o thread principal e o thread de coleta de lixo. A linha 1 cria uma nova instância da classe de teste. Na linha 2, criamos um delegado ThreadStart e passamos o endereço do procedimento SubtractFromCounter para a instância da classe de teste criada na linha 1 (esse procedimento é chamado sem parâmetros). BoaAo importar o namespace Threading, o nome longo pode ser omitido. O novo objeto thread é criado na linha 3. Observe a passagem do delegado ThreadStart ao chamar o construtor da classe Thread. Alguns programadores preferem concatenar essas duas linhas em uma linha lógica:

Dim bThread As New Thread (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Finalmente, a linha 4 "inicia" o thread chamando o método Start da instância Thread criada para o delegado ThreadStart. Ao chamar esse método, informamos ao sistema operacional que o procedimento Subtract deve ser executado em um thread separado.

A palavra "começa" no parágrafo anterior está entre aspas, porque esta é uma das muitas estranhezas da programação multithread: chamar Start não inicia de fato o thread! Ele apenas informa ao sistema operacional para agendar a execução do thread especificado, mas está além do controle do programa iniciar diretamente. Você não poderá iniciar a execução de threads por conta própria, porque o sistema operacional sempre controla a execução de threads. Em uma seção posterior, você aprenderá como usar a prioridade para fazer o sistema operacional iniciar seu thread mais rápido.

Na fig. 10.1 mostra um exemplo do que pode acontecer após iniciar um programa e, em seguida, interrompê-lo com a tecla Ctrl + Break. Em nosso caso, um novo thread foi iniciado somente depois que o contador no thread principal aumentou para 341!

Arroz. 10.1. Tempo de execução de software multi-thread simples

Se o programa for executado por um longo período de tempo, o resultado será semelhante ao mostrado na Fig. 10,2. Nós vemos que vocêa conclusão do encadeamento em execução é suspensa e o controle é transferido para o encadeamento principal novamente. Neste caso, há uma manifestação multithreading preventivo por meio de divisão de tempo. O significado deste termo terrível é explicado abaixo.

Arroz. 10,2. Alternando entre threads em um programa multithread simples

Ao interromper threads e transferir o controle para outras threads, o sistema operacional usa o princípio de multithreading preventivo por meio de divisão de tempo. A quantização de tempo também resolve um dos problemas comuns que surgiam antes em programas multithread - um thread ocupa todo o tempo da CPU e não é inferior ao controle de outros threads (como regra, isso acontece em ciclos intensivos como o anterior). Para evitar o sequestro exclusivo de CPU, seus threads devem transferir o controle para outros threads de tempos em tempos. Se o programa ficar "inconsciente", existe outra solução um pouco menos desejável: o sistema operacional sempre antecipa uma thread em execução, independentemente de seu nível de prioridade, para que todas as threads do sistema tenham acesso ao processador.

Como os esquemas de quantização de todas as versões do Windows que executam .NET têm um intervalo de tempo mínimo alocado para cada thread, na programação .NET, os problemas com capturas exclusivas de CPU não são tão sérios. Por outro lado, se o .NET framework for adaptado para outros sistemas, isso pode mudar.

Se incluirmos a seguinte linha em nosso programa antes de chamar Start, mesmo os threads com a prioridade mais baixa obterão uma fração do tempo de CPU:

bThread.Priority = ThreadPriority.Highest

Arroz. 10.3. O tópico com a prioridade mais alta geralmente inicia mais rápido

Arroz. 10,4. O processador também é fornecido para threads de baixa prioridade

O comando atribui a prioridade máxima ao novo encadeamento e diminui a prioridade do encadeamento principal. FIG. 10.3 pode-se observar que o novo thread começa a funcionar mais rápido do que antes, mas, conforme a Fig. 10.4, o thread principal também recebe o controlepreguiça (embora por um tempo muito curto e somente após um trabalho prolongado do fluxo com subtração). Ao executar o programa em seus computadores, você obterá resultados semelhantes aos mostrados na Fig. 10.3 e 10.4, mas devido às diferenças entre nossos sistemas, não haverá correspondência exata.

O tipo enumerado ThreadPrlority inclui valores para cinco níveis de prioridade:

ThreadPriority.Highest

ThreadPriority.AboveNormal

ThreadPrlority.Normal

ThreadPriority.BelowNormal

ThreadPriority.Lowest

Método de junção

Às vezes, um thread de programa precisa ser pausado até que outro thread seja concluído. Digamos que você queira pausar o encadeamento 1 até que o encadeamento 2 conclua seu cálculo. Por esta do stream 1 o método Join é chamado para o stream 2. Em outras palavras, o comando

thread2.Join ()

suspende o encadeamento atual e espera que o encadeamento 2 seja concluído. O encadeamento 1 vai para estado bloqueado.

Se você ingressar no stream 1 ao stream 2 usando o método Join, o sistema operacional iniciará automaticamente o stream 1 após o stream 2. Observe que o processo de inicialização é não determinístico:é impossível dizer exatamente quanto tempo após o final do encadeamento 2, o encadeamento 1 começará a funcionar. Há outra versão de Join que retorna um valor booleano:

thread2.Join (inteiro)

Este método aguarda a conclusão do encadeamento 2 ou desbloqueia o encadeamento 1 após o intervalo de tempo especificado ter decorrido, fazendo com que o planejador do sistema operacional aloque o tempo de CPU para o encadeamento novamente. O método retorna True se o thread 2 terminar antes que o intervalo de tempo limite especificado expire, e False caso contrário.

Lembre-se da regra básica: se o thread 2 foi concluído ou expirou, você não tem controle sobre quando o thread 1 é ativado.

Nomes de thread, CurrentThread e ThreadState

A propriedade Thread.CurrentThread retorna uma referência ao objeto thread que está sendo executado no momento.

Embora haja uma janela de thread maravilhosa para depurar aplicativos multithread em VB .NET, que é descrita abaixo, éramos muitas vezes ajudados pelo comando

MsgBox (Thread.CurrentThread.Name)

Freqüentemente, o código estava sendo executado em uma thread completamente diferente da qual deveria ser executado.

Lembre-se de que o termo "escalonamento não determinístico de fluxos de programa" significa uma coisa muito simples: o programador praticamente não tem meios à sua disposição para influenciar o trabalho do escalonador. Por esse motivo, os programas geralmente usam a propriedade ThreadState, que retorna informações sobre o estado atual de um thread.

Janela de streams

A janela Threads do Visual Studio .NET é inestimável na depuração de programas multithread. É ativado pelo comando de submenu Debug> Windows no modo de interrupção. Digamos que você atribuiu um nome ao thread bThread com o seguinte comando:

bThread.Name = "Subtraindo tópico"

Uma visão aproximada da janela de streams após interromper o programa com a combinação de teclas Ctrl + Break (ou de outra forma) é mostrada na Fig. 10,5.

Arroz. 10,5. Janela de streams

A seta na primeira coluna marca o segmento ativo retornado pela propriedade Thread.CurrentThread. A coluna ID contém IDs de encadeamentos numéricos. A próxima coluna lista os nomes dos fluxos (se atribuídos). A coluna Location indica o procedimento a ser executado (por exemplo, o procedimento WriteLine da classe Console na Figura 10.5). As colunas restantes contêm informações sobre prioridade e threads suspensos (consulte a próxima seção).

A janela de thread (não o sistema operacional!) Permite que você controle os threads de seu programa usando menus de contexto. Por exemplo, você pode parar o encadeamento atual clicando com o botão direito do mouse na linha correspondente e escolhendo o comando Congelar (posteriormente, o encadeamento interrompido pode ser retomado). A interrupção de encadeamentos costuma ser usada durante a depuração para evitar que um encadeamento com defeito interfira no aplicativo. Além disso, a janela de streams permite que você ative outro stream (não interrompido); para fazer isso, clique com o botão direito do mouse na linha desejada e selecione o comando Mudar para thread no menu de contexto (ou simplesmente dê um clique duplo na linha de thread). Como será mostrado a seguir, isso é muito útil no diagnóstico de deadlocks em potencial.

Suspendendo um fluxo

Streams temporariamente não usados ​​podem ser transferidos para um estado passivo usando o método Slеer. Um fluxo passivo também é considerado bloqueado. Obviamente, quando um thread é colocado em um estado passivo, o restante dos threads terá mais recursos do processador. A sintaxe padrão do método Slеer é a seguinte: Thread.Sleep (interval_in_milliseconds)

Como resultado da chamada Sleep, o encadeamento ativo se torna passivo por pelo menos um número especificado de milissegundos (no entanto, a ativação imediatamente após o intervalo especificado expirar não é garantida). Observação: ao chamar o método, uma referência a um thread específico não é passada - o método Sleep é chamado apenas para o thread ativo.

Outra versão do Sleep faz com que o thread atual ceda o resto do tempo de CPU alocado:

Thread.Sleep (0)

A próxima opção coloca o thread atual em um estado passivo por um tempo ilimitado (a ativação ocorre apenas quando você chama a interrupção):

Thread.Slеer (Timeout.Infinite)

Uma vez que threads passivos (mesmo com um tempo limite ilimitado) podem ser interrompidos pelo método Interrupt, o que leva ao início de um ThreadlnterruptExcepti na exceção, a chamada do Slayer está sempre incluída em um bloco Try-Catch, como no seguinte trecho:

Experimente

Thread.Sleep (200)

"O estado passivo do thread foi interrompido

Catch e As Exception

"Outras exceções

Fim da tentativa

Cada programa .NET é executado em um thread de programa, portanto, o método Sleep também é usado para suspender programas (se o namespace Threadipg não for importado pelo programa, você deve usar o nome totalmente qualificado Threading.Thread. Sleep).

Encerrando ou interrompendo threads de programa

Um encadeamento será encerrado automaticamente quando o método especificado quando o delegado ThreadStart for criado, mas às vezes o método (e, portanto, o encadeamento) precisará ser encerrado quando certos fatores ocorrerem. Nesses casos, os fluxos geralmente verificam variável condicional, dependendo do estado de qualé tomada uma decisão sobre uma saída de emergência do riacho. Normalmente, um loop Do-While é incluído no procedimento para isso:

Sub ThreadedMethod ()

“O programa deve fornecer meios para a pesquisa

"variável condicional.

"Por exemplo, uma variável condicional pode ser estilizada como uma propriedade

Do While conditionVariable = False And MoreWorkToDo

"O código principal

Loop End Sub

Leva algum tempo para pesquisar a variável condicional. Você só deve usar a pesquisa persistente em uma condição de loop se estiver esperando que um encadeamento termine prematuramente.

Se a variável de condição deve ser verificada em um local específico, use o comando If-Then em conjunto com Exit Sub dentro de um loop infinito.

O acesso a uma variável condicional deve ser sincronizado para que a exposição de outros threads não interfira em seu uso normal. Este tópico importante é abordado na seção "Solução de problemas: Sincronização".

Infelizmente, o código de encadeamentos passivos (ou bloqueados de outra forma) não é executado, portanto, a opção de pesquisar uma variável condicional não é adequada para eles. Nesse caso, chame o método Interrupt na variável do objeto que contém uma referência ao thread desejado.

O método Interrupt só pode ser chamado em threads no estado Wait, Sleep ou Join. Se você chamar Interrupt para um thread que está em um dos estados listados, depois de um tempo, o thread começará a funcionar novamente e o ambiente de execução iniciará uma ThreadlnterruptedExcepti na exceção no thread. Isso ocorre mesmo se o thread se tornou passivo indefinidamente chamando Thread.Sleepdimeout. Infinito). Dizemos "depois de um tempo" porque o agendamento de thread não é determinístico. A exceção ThreadlnterruptedExcepti on é capturada pela seção Catch que contém o código de saída do estado de espera. No entanto, a seção Catch não precisa encerrar o encadeamento em uma chamada de interrupção - o encadeamento lida com a exceção como achar adequado.

No .NET, o método Interrupt pode ser chamado até mesmo para threads desbloqueados. Nesse caso, o thread é interrompido no bloqueio mais próximo.

Suspendendo e matando tópicos

O namespace Threading contém outros métodos que interrompem o threading normal:

  • Suspender;
  • Abortar.

É difícil dizer por que o .NET incluiu suporte para esses métodos - chamar Suspend e Abort provavelmente fará com que o programa se torne instável. Nenhum dos métodos permite a desinicialização normal do fluxo. Além disso, ao chamar Suspend ou Abort, é impossível prever em que estado o thread deixará os objetos depois de ser suspenso ou abortado.

Chamar Abort lança um ThreadAbortException. Para ajudá-lo a entender por que essa estranha exceção não deve ser tratada em programas, aqui está um trecho da documentação do .NET SDK:

“... Quando um thread é destruído chamando Abort, o tempo de execução lança um ThreadAbortException. Este é um tipo especial de exceção que não pode ser detectado pelo programa. Quando essa exceção é lançada, o tempo de execução executa todos os blocos Finalmente antes de encerrar o encadeamento. Como qualquer ação pode ocorrer em blocos Finalmente, chame Join para garantir que o fluxo seja destruído. "

Moral: Abortar e Suspender não são recomendados (e se você ainda não pode fazer sem Suspender, retome o tópico suspenso usando o método Resume). Você pode encerrar um thread com segurança apenas pesquisando uma variável de condição sincronizada ou chamando o método Interrupt discutido acima.

Tópicos de fundo (daemons)

Alguns threads em execução em segundo plano param de funcionar automaticamente quando outros componentes do programa param. Em particular, o coletor de lixo é executado em um dos threads de fundo. Os encadeamentos em segundo plano geralmente são criados para receber dados, mas isso é feito apenas se outros encadeamentos estiverem executando o código que pode processar os dados recebidos. Sintaxe: nome do stream. IsBackGround = True

Se houver apenas threads em segundo plano restantes no aplicativo, o aplicativo será encerrado automaticamente.

Exemplo mais sério: extração de dados do código HTML

Recomendamos o uso de streams apenas quando a funcionalidade do programa estiver claramente dividida em várias operações. Um bom exemplo é o programa de extração de HTML no Capítulo 9. Nossa classe faz duas coisas: busca dados da Amazon e os processa. Este é um exemplo perfeito de uma situação em que a programação multithread é realmente apropriada. Criamos classes para vários livros diferentes e, em seguida, analisamos os dados em diferentes fluxos. Criar um novo thread para cada livro aumenta a eficiência do programa, porque enquanto um thread está recebendo dados (o que pode exigir a espera no servidor Amazon), outro thread estará ocupado processando os dados que já foram recebidos.

A versão multi-threaded deste programa funciona de forma mais eficiente do que a versão single-threaded apenas em um computador com vários processadores ou se a recepção de dados adicionais puder ser combinada efetivamente com sua análise.

Conforme mencionado acima, apenas procedimentos que não possuem parâmetros podem ser executados em threads, portanto, você terá que fazer pequenas alterações no programa. Abaixo está o procedimento básico, reescrito para excluir parâmetros:

Public Sub FindRank ()

m_Rank = ScrapeAmazon ()

Console.WriteLine ("a classificação de" & m_Name & "Is" & GetRank)

End Sub

Uma vez que não seremos capazes de usar o campo combinado para armazenar e recuperar informações (escrever programas multi-threaded com uma interface gráfica é discutido na última seção deste capítulo), o programa armazena os dados de quatro livros em um array, o definição de qual começa assim:

Dim theBook (3.1) As String theBook (0.0) = "1893115992"

theBook (0.l) = "Programação VB .NET" "Etc.

Quatro fluxos são criados no mesmo ciclo em que os objetos AmazonRanker são criados:

Para i = 0 a 3

Experimente

theRanker = Novo AmazonRanker (theBook (i.0). theBookd.1))

aThreadStart = Novo ThreadStar (AddressOf theRanker.FindRan ()

aThread = Novo Tópico (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start () Catch e As Exception

Console.WriteLine (e.Message)

Fim da tentativa

Próximo

Abaixo está o texto completo do programa:

Opção Strict On Imports System.IO Imports System.Net

Imports System.Threading

Módulo Módulo

Sub principal ()

Dim theBook (3.1) As String

theBook (0,0) = "1893115992"

theBook (0.l) = "Programação VB .NET"

theBook (l.0) = "1893115291"

theBook (l.l) = "Programação de banco de dados VB .NET"

theBook (2,0) = "1893115623"

theBook (2.1) = "Introdução do programador ao C #."

theBook (3.0) = "1893115593"

theBook (3.1) = "Gland the .Net Platform"

Dim i As Integer

Dim theRanker As = AmazonRanker

Dim aThreadStart As Threading.ThreadStart

Dim aThread As Threading.Thread

Para i = 0 a 3

Experimente

theRanker = Novo AmazonRankerttheBook (i.0). theBook (i.1))

aThreadStart = New ThreadStart (AddressOf theRanker. FindRank)

aThread = Novo Tópico (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start ()

Catch e As Exception

Console.WriteLlnete.Message)

Fim Tente Próximo

Console.ReadLine ()

End Sub

Módulo Final

Classe pública AmazonRanker

M_URL privado como string

M_Rank privado como inteiro

M_Name privado como string

Public Sub New (ByVal ISBN As String. ByVal theName As String)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" e ISBN

m_Name = theName End Sub

Public Sub FindRank () m_Rank = ScrapeAmazon ()

Console.Writeline ("a classificação de" & m_Name & "é"

& GetRank) End Sub

Propriedade pública somente leitura GetRank () As String Get

If m_Rank<>0 então

Retornar CStr (m_Rank) Else

"Problemas

Fim se

End Get

Propriedade Final

Propriedade pública somente leitura GetName () As String Get

Retornar m_Name

End Get

Propriedade Final

Função privada ScrapeAmazon () As Integer Try

Dim theURL como novo Uri (m_URL)

Dim theRequest As WebRequest

theRequest = WebRequest.Create (theURL)

Dim theResponse As WebResponse

theResponse = theRequest.GetResponse

Dim aReader como novo StreamReader (theResponse.GetResponseStream ())

Dim theData As String

theData = aReader.ReadToEnd

Análise de Retorno (theData)

Catch E As Exception

Console.WriteLine (E.Message)

Console.WriteLine (E.StackTrace)

Console. Leia a linha ()

End Try End Function

Análise de função privada (ByVal theData As String) As Integer

Dim Location As.Integer Location = theData.IndexOf (" Amazon.com

Classificação de vendas:") _

+ "Classificação de vendas da Amazon.com:".Comprimento

Dim temp como string

Faça Até theData.Substring (Location.l) = "<" temp = temp

& theData.Substring (Location.l) Location + = 1 Loop

Return Clnt (temp)

Função Final

Fim da aula

Operações multithread são comumente usadas em namespaces .NET e de E / S, portanto, a biblioteca .NET Framework fornece métodos assíncronos especiais para eles. Para obter mais informações sobre o uso de métodos assíncronos ao escrever programas multithread, consulte os métodos BeginGetResponse e EndGetResponse da classe HTTPWebRequest.

Risco principal (dados gerais)

Até agora, o único caso de uso seguro para threads foi considerado - nossos streams não alteraram os dados gerais. Se você permitir a mudança nos dados gerais, os erros potenciais começam a se multiplicar exponencialmente e se torna muito mais difícil eliminá-los para o programa. Por outro lado, se você proibir a modificação de dados compartilhados por threads diferentes, a programação .NET multithreading dificilmente será diferente dos recursos limitados do VB6.

Gostaríamos de chamar sua atenção para um pequeno programa que demonstra os problemas que surgem sem entrar em detalhes desnecessários. Este programa simula uma casa com um termostato em cada cômodo. Se a temperatura for 5 graus Fahrenheit ou mais (cerca de 2,77 graus Celsius) inferior à temperatura alvo, ordenamos que o sistema de aquecimento aumente a temperatura em 5 graus; caso contrário, a temperatura sobe apenas 1 grau. Se a temperatura atual for maior ou igual à definida, nenhuma alteração será feita. O controle da temperatura em cada sala é realizado com um fluxo separado com um atraso de 200 milissegundos. O trabalho principal é feito com o seguinte snippet:

If mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Thread.Sleep (200)

Capturar empate como ThreadlnterruptedException

"A espera passiva foi interrompida

Catch e As Exception

"Outras exceções de tentativa de fim

mHouse.HouseTemp + - 5 "etc.

Abaixo está o código-fonte completo do programa. O resultado é mostrado na figura. 10.6: A temperatura na casa atingiu 105 graus Fahrenheit (40,5 graus Celsius)!

1 opção estrita em

2 Sistema de Importações.Threading

Módulo de 3 Módulos

4 Sub Main ()

5 Dim myHouse como nova casa (10)

6 Console. Leia a linha ()

7 End Sub

8 Módulo Final

9 Public Class House

10 Const Public MAX_TEMP As Integer = 75

11 mCurTemp privado como número inteiro = 55

12 mRooms privados () como quarto

13 Sub Public New (ByVal numOfRooms As Integer)

14 ReDim mRooms (numOfRooms = 1)

15 Dim i As Integer

16 Dim aThreadStart As Threading.ThreadStart

17 Dim aThread As Thread

18 Para i = 0 Para numOfRooms -1

19 Tente

20 mRooms (i) = NewRoom (Me, mCurTemp, CStr (i) e "throom")

21 aThreadStart - Novo ThreadStart (AddressOf _

mRooms (i) .CheckTempInRoom)

22 aThread = Novo Tópico (aThreadStart)

23 aThread.Start ()

24 Catch E como exceção

25 Console.WriteLine (E.StackTrace)

26 Fim da tentativa

27 Next

28 End Sub

29 Public Property HouseTemp () As Integer

trinta . Pegue

31 Retorno mCurTemp

32 End Get

33 Set (ByVal Value As Integer)

34 mCurTemp = Valor 35 End Set

36 End Property

37 Fim da aula

38 Sala de Aula Pública

39 Privado mCurTemp como inteiro

40 mName privado como string

41 Private mHouse As House

42 Public Sub New (ByVal theHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mHouse = theHouse

44 mCurTemp = temp

45 mName = roomName

46 End Sub

47 Public Sub CheckTempInRoom ()

48 ChangeTemperature ()

49 End Sub

50 Sub Mudança de Temperatura Privada ()

51 Tente

52 If mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 Thread.Sleep (200)

54 mHouse.HouseTemp + - 5

55 Console.WriteLine ("Estou em" & Me.mName & _

56 ".A temperatura atual é" & mHouse.HouseTemp)

57 Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 Thread.Sleep (200)

59 mHouse.HouseTemp + = 1

60 Console.WriteLine ("Am in" & Me.mName & _

61 ". A temperatura atual é" & mHouse.HouseTemp)

62 Else

63 Console.WriteLine ("Estou em" & Me.mName & _

64 ". A temperatura atual é" & mHouse.HouseTemp)

65 "Não faça nada, a temperatura está normal

66 End If

67 Catch tae As ThreadlnterruptedException

68 "A espera passiva foi interrompida

69 Catch e As Exception

70 "Outras exceções

71 Fim da tentativa

72 End Sub

73 Fim da aula

Arroz. 10,6. Problemas de multithreading

O procedimento Sub Main (linhas 4-7) cria uma "casa" com dez "quartos". A classe House define uma temperatura máxima de 75 graus Fahrenheit (cerca de 24 graus Celsius). As linhas 13-28 definem um construtor de casa bastante complexo. As linhas 18-27 são fundamentais para a compreensão do programa. A linha 20 cria outro objeto de quarto e uma referência ao objeto de casa é passada ao construtor para que o objeto de quarto possa se referir a ela, se necessário. As linhas 21-23 iniciam dez fluxos para ajustar a temperatura em cada sala. A classe Room é definida nas linhas 38-73. Referência de coxpa domésticoé armazenado na variável mHouse no construtor da classe Room (linha 43). O código para verificar e ajustar a temperatura (linhas 50-66) parece simples e natural, mas como você verá em breve, essa impressão engana! Observe que esse código é empacotado em um bloco Try-Catch porque o programa usa o método Sleep.

Quase ninguém concordaria em viver em temperaturas de 105 graus Fahrenheit (40,5 a 24 graus Celsius). O que aconteceu? O problema está relacionado à seguinte linha:

If mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

E acontece o seguinte: primeiro, a temperatura é verificada pelo fluxo 1. Ele vê que a temperatura está muito baixa e a aumenta em 5 graus. Infelizmente, antes que a temperatura aumente, o fluxo 1 é interrompido e o controle é transferido para o fluxo 2. O fluxo 2 verifica a mesma variável que não foi alterado ainda fluxo 1. Assim, o fluxo 2 também está se preparando para elevar a temperatura em 5 graus, mas não tem tempo para fazer isso e também entra em estado de espera. O processo continua até que o fluxo 1 seja ativado e prossiga para o próximo comando - aumentando a temperatura em 5 graus. O aumento se repete quando todos os 10 riachos são ativados, e os moradores da casa vão ter um péssimo momento.

Solução para o problema: sincronização

No programa anterior, surge uma situação em que a saída do programa depende da ordem de execução das threads. Para se livrar dele, você precisa se certificar de que comandos como

If mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

são totalmente processados ​​pelo encadeamento ativo antes de ser interrompido. Esta propriedade é chamada vergonha atômica - um bloco de código deve ser executado por cada thread sem interrupção, como uma unidade atômica. Um grupo de comandos, combinado em um bloco atômico, não pode ser interrompido pelo agendador de thread até que seja concluído. Qualquer linguagem de programação multithread tem suas próprias maneiras de garantir a atomicidade. No VB .NET, a maneira mais fácil de usar o comando SyncLock é passar uma variável de objeto quando chamada. Faça pequenas alterações no procedimento ChangeTemperature do exemplo anterior e o programa funcionará bem:

Private Sub ChangeTemperature () SyncLock (mHouse)

Experimente

If mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Thread.Sleep (200)

mHouse.HouseTemp + = 5

Console.WriteLine ("Am in" & Me.mName & _

".A temperatura atual é" & mHouse.HouseTemp)

Elself

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Thread.Sleep (200) mHouse.HouseTemp + = 1

Console.WriteLine ("Am in" & Me.mName & _ ".A temperatura atual é" & mHouse.HomeTemp) Else

Console.WriteLineC "Am in" & Me.mName & _ ".A temperatura atual é" & mHouse.HouseTemp)

"Não faça nada, a temperatura está normal

End If Catch tie As ThreadlnterruptedException

"A espera passiva foi interrompida por Catch e As Exception

"Outras exceções

Fim da tentativa

Terminar SyncLock

End Sub

O código do bloco SyncLock é executado atomicamente. O acesso a ele de todos os outros threads será fechado até que o primeiro thread libere o bloqueio com o comando End SyncLock. Se um thread em um bloco sincronizado entrar em um estado de espera passivo, o bloqueio permanecerá até que o thread seja interrompido ou retomado.

O uso correto do comando SyncLock mantém o thread do programa seguro. Infelizmente, o uso excessivo de SyncLock tem um impacto negativo no desempenho. A sincronização de código em um programa multithread reduz a velocidade de seu trabalho várias vezes. Sincronize apenas o código mais necessário e libere o bloqueio o mais rápido possível.

As classes de coleção base não são seguras em aplicativos multithread, mas o .NET Framework inclui versões thread-safe da maioria das classes de coleção. Nessas classes, o código de métodos potencialmente perigosos é colocado em blocos SyncLock. Versões thread-safe de classes de coleção devem ser usadas em programas multithread sempre que a integridade dos dados for comprometida.

Resta mencionar que as variáveis ​​condicionais são facilmente implementadas usando o comando SyncLock. Para fazer isso, basta sincronizar a gravação para a propriedade booleana comum, disponível para leitura e gravação, como é feito no seguinte fragmento:

Public Class ConditionVariable

Cacifo compartilhado privado As Object = New Object ()

Privado compartilhado mOK como booleano compartilhado

Propriedade TheConditionVariable () As Boolean

Pegue

Retorno mOK

End Get

Set (ByVal Value As Boolean) SyncLock (locker)

mOK = valor

Terminar SyncLock

End Set

Propriedade Final

Fim da aula

Comando SyncLock e Classe Monitor

O uso do comando SyncLock envolve algumas sutilezas que não foram mostradas nos exemplos simples acima. Portanto, a escolha do objeto de sincronização desempenha um papel muito importante. Tente executar o programa anterior com o comando SyncLock (Me) em vez de SyncLock (mHouse). A temperatura sobe acima do limite novamente!

Lembre-se de que o comando SyncLock sincroniza usando objeto, passado como um parâmetro, não pelo snippet de código. O parâmetro SyncLock atua como uma porta para acessar o fragmento sincronizado de outros threads. O comando SyncLock (Me) na verdade abre várias "portas" diferentes, que é exatamente o que você estava tentando evitar com a sincronização. Moralidade:

Para proteger os dados compartilhados em um aplicativo multithread, o comando SyncLock deve sincronizar um objeto por vez.

Como a sincronização está associada a um objeto específico, em algumas situações, é possível bloquear inadvertidamente outros fragmentos. Digamos que você tenha dois métodos sincronizados, primeiro e segundo, e ambos os métodos estejam sincronizados no objeto bigLock. Quando o encadeamento 1 entra no método primeiro e captura bigLock, nenhum encadeamento será capaz de entrar no segundo método porque o acesso a ele já está restrito ao encadeamento 1!

A funcionalidade do comando SyncLock pode ser considerada um subconjunto da funcionalidade da classe Monitor. A classe Monitor é altamente personalizável e pode ser usada para resolver tarefas de sincronização não triviais. O comando SyncLock é um análogo aproximado dos métodos Enter e Exi t da classe Monitor:

Experimente

Monitor.Enter (theObject) Finalmente

Monitor.Exit (theObject)

Fim da tentativa

Para algumas operações padrão (aumentar / diminuir uma variável, trocar o conteúdo de duas variáveis), o .NET Framework fornece a classe Interlocked, cujos métodos executam essas operações no nível atômico. Usando a classe Interlocked, essas operações são muito mais rápidas do que usar o comando SyncLock.

Intertravamento

Durante a sincronização, o bloqueio é definido em objetos, não threads, portanto, ao usar diferente objetos para bloquear diferente trechos de código em programas às vezes ocorrem erros não triviais. Infelizmente, em muitos casos, a sincronização em um único objeto simplesmente não é permitida, pois levará ao bloqueio de threads com muita frequência.

Considere a situação entrelaçado(impasse) em sua forma mais simples. Imagine dois programadores à mesa de jantar. Infelizmente, eles só têm uma faca e um garfo para dois. Supondo que você precise de uma faca e de um garfo para comer, duas situações são possíveis:

  • Um programador consegue pegar um garfo e uma faca e começa a comer. Quando ele está cheio, ele coloca o jantar de lado, e então outro programador pode levá-los.
  • Um programador pega a faca e o outro pega o garfo. Nenhum pode começar a comer a menos que o outro desista de seu eletrodoméstico.

Em um programa multithread, esta situação é chamada bloqueio mútuo. Os dois métodos são sincronizados em objetos diferentes. O thread A captura o objeto 1 e entra na parte do programa protegida por este objeto. Infelizmente, para funcionar, ele precisa de acesso ao código protegido por outro Sync Lock com um objeto de sincronização diferente. Mas antes que tenha tempo de entrar em um fragmento que está sincronizado por outro objeto, o fluxo B entra nele e captura esse objeto. Agora o thread A não pode entrar no segundo fragmento, o thread B não pode entrar no primeiro fragmento e ambos os threads estão condenados a esperar indefinidamente. Nenhum encadeamento pode continuar em execução porque o objeto necessário nunca será liberado.

O diagnóstico de deadlocks é complicado pelo fato de que eles podem ocorrer em casos relativamente raros. Tudo depende da ordem em que o planejador aloca o tempo de CPU para eles. É possível que, na maioria dos casos, os objetos de sincronização sejam capturados em uma ordem sem conflito.

A seguir está uma implementação da situação de conflito que acabamos de descrever. Após uma breve discussão sobre os pontos mais fundamentais, mostraremos como identificar uma situação de deadlock na janela do thread:

1 opção estrita em

2 Sistema de Importações.Threading

Módulo de 3 Módulos

4 Sub Main ()

5 Dim Tom como novo programador ("Tom")

6 Dim Bob como novo programador ("Bob")

7 Dim aThreadStart As New ThreadStart (AddressOf Tom.Eat)

8 Dim aThread como novo tópico (aThreadStart)

9 aThread.Name = "Tom"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Dim bThread como novo tópico (bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start ()

14 bThread.Start ()

15 End Sub

Módulo 16 Final

17 Public Class Fork

18 Private Shared mForkAvaiTable As Boolean = True

19 Private Shared mOwner As String = "Ninguém"

20 Propriedade privada somente leitura OwnsUtensil () As String

21 Get

22 Retornar mOwner

23 End Get

24 Propriedade Final

25 Public Sub GrabForktByVal a As Programmer)

26 Console.Writel_ine (Thread.CurrentThread.Name & _

"tentando agarrar o garfo.")

27 Console.WriteLine (Me.OwnsUtensil & "tem a bifurcação."). ...

28 Monitor.Enter (Me) "SyncLock (aFork)"

29 Se mForkAvailable Then

30 a.HasFork = Verdadeiro

31 mOwner = a.MyName

32 mForkAvailable = Falso

33 Console.WriteLine (a.MyName & "acabou de pegar o fork.waiting")

34 Tente

Thread.Sleep (100) Catch e As Exception Console.WriteLine (e.StackTrace)

Fim da tentativa

35 End If

36 Monitor.Exit (Me)

Terminar SyncLock

37 End Sub

38 Fim da aula

39 Canivete de Classe Pública

40 Private Shared mKnifeAvailable As Boolean = True

41 Private Shared mOwner As String = "Ninguém"

42 Propriedade privada somente leitura OwnsUtensi1 () como string

43 Get

44 Retornar mOwner

45 End Get

46 End Property

47 Public Sub GrabKnifetByVal a As Programmer)

48 Console.WriteLine (Thread.CurrentThread.Name & _

"tentando pegar a faca.")

49 Console.WriteLine (Me.OwnsUtensil & "tem a faca.")

50 Monitor.Enter (Me) "SyncLock (aKnife)"

51 Se mKnifeAvailable Então

52 mKnifeAvailable = False

53 a.HasKnife = Verdadeiro

54 mOwner = a.MyName

55 Console.WriteLine (a.MyName & "acabou de pegar a faca.waiting")

56 Tente

Thread.Sleep (100)

Catch e As Exception

Console.WriteLine (e.StackTrace)

Fim da tentativa

57 End If

58 Monitor.Exit (Me)

59 End Sub

60 Fim da aula

61 Programador de Classe Pública

62 mName privado como string

63 Private Shared mFork As Fork

64 Private Shared mKnife As Knife

65 Private mHasKnife As Boolean

66 mHasFork privado como booleano

67 Sub Novo Compartilhado ()

68 mFork = Novo garfo ()

69 mKnife = New Knife ()

70 End Sub

71 Public Sub New (ByVal theName As String)

72 mName = theName

73 End Sub

74 Propriedade pública somente leitura MyName () como string

75 Get

76 Return mName

77 End Get

78 End Property

79 Propriedade Pública HasKnife () As Boolean

80 Get

81 Retorno mHasKnife

82 End Get

83 Definido (valor de ByVal como booleano)

84 mHasKnife = Valor

85 End Set

86 End Property

87 Propriedade Pública HasFork () As Boolean

88 Get

89 Retornar mHasFork

90 End Get

91 Set (ByVal Value As Boolean)

92 mHasFork = Valor

93 End Set

94 End Property

95 Public Sub Eat ()

96 Do Until Me.HasKnife And Me.HasFork

97 Console.Writeline (Thread.CurrentThread.Name & "está no thread.")

98 Se Rnd ()< 0.5 Then

99 mFork.GrabFork (Me)

100 Else

101 mKnife.GrabKnife (Me)

102 End If

103 Loop

104 MsgBox (Me.MyName & "pode ​​comer!")

105 mKnife = New Knife ()

106 mFork = New Fork ()

107 End Sub

108 Fim da aula

O procedimento principal Main (linhas 4-16) cria duas instâncias da classe Programmer e então inicia duas threads para executar o método Eat crítico da classe Programmer (linhas 95-108), descrito abaixo. O procedimento Main define os nomes dos threads e os configura; provavelmente tudo o que acontece é compreensível e sem comentários.

O código para a classe Fork parece mais interessante (linhas 17-38) (uma classe Knife semelhante é definida nas linhas 39-60). As linhas 18 e 19 especificam os valores dos campos comuns, pelos quais você pode descobrir se o plugue está disponível no momento e, se não, quem o está usando. A propriedade ReadOnly OwnUtensi1 (linhas 20-24) se destina à transferência mais simples de informações. Central para a classe Fork é o método GrabFork “pegar o garfo”, definido nas linhas 25-27.

  1. As linhas 26 e 27 simplesmente imprimem informações de depuração no console. No código principal do método (linhas 28-36), o acesso ao fork é sincronizado por objetocinto Me. Uma vez que nosso programa usa apenas um fork, o Me sync garante que dois threads não possam pegá-lo ao mesmo tempo. O comando Slee "p (no bloco que começa na linha 34) simula o atraso entre pegar um garfo / faca e começar a comer. Observe que o comando Sleep não desbloqueia objetos e apenas acelera bloqueios!
    Porém, o mais interessante é o código da classe Programmer (linhas 61 a 108). As linhas 67-70 definem um construtor genérico para garantir que haja apenas um garfo e faca no programa. O código de propriedade (linhas 74-94) é simples e não requer nenhum comentário. A coisa mais importante acontece no método Eat, que é executado por dois threads separados. O processo continua em um loop até que algum fluxo capture o garfo junto com a faca. Nas linhas 98-102, o objeto agarra aleatoriamente o garfo / faca usando a chamada Rnd, que é o que causa o deadlock. Acontece o seguinte:
    A thread que executa o método Eat do objeto Thoth é chamada e entra no loop. Ele pega a faca e entra em um estado de espera.
  2. A thread que executa o método Eat de Bob é chamada e entra no loop. Ele não pode agarrar a faca, mas agarra o garfo e entra em um estado de espera.
  3. A thread que executa o método Eat do objeto Thoth é chamada e entra no loop. Ele tenta agarrar o garfo, mas Bob já agarrou o garfo; o thread entra em um estado de espera.
  4. O encadeamento que executa o método Eat de Bob é invocado e entra no loop. Ele tenta agarrar a faca, mas a faca já foi capturada pelo objeto Thoth; o thread entra em um estado de espera.

Tudo isso continua indefinidamente - nos deparamos com uma situação típica de impasse (tente executar o programa e você verá que ninguém consegue comer assim).
Você também pode ver se ocorreu um deadlock na janela de threads. Execute o programa e interrompa-o com as teclas Ctrl + Break. Inclua a variável Me na janela de visualização e abra a janela de fluxos. O resultado se parece com o mostrado na Fig. 10,7. Pode-se ver na figura que o fio de Bob agarrou uma faca, mas não tem um garfo. Clique com o botão direito na janela Threads na linha Tot e selecione o comando Switch to Thread no menu de contexto. A janela de visualização mostra que o riacho de Thoth tem um garfo, mas nenhuma faca. Claro, isso não é uma prova cem por cento, mas tal comportamento pelo menos faz você suspeitar que algo estava errado.
Se a opção de sincronização por um objeto (como no programa com aumento da temperatura da casa) não for possível, para evitar bloqueios mútuos, pode-se numerar os objetos de sincronização e capturá-los sempre em ordem constante. Vamos continuar com a analogia do programador de jantar: se o thread sempre pegar a faca primeiro e depois o garfo, não haverá problemas com deadlock. O primeiro jato que pegar a faca poderá comer normalmente. Traduzido para a linguagem de fluxos de programa, isso significa que a captura do objeto 2 só é possível se o objeto 1 for capturado primeiro.

Arroz. 10,7. Análise de deadlocks na janela de thread

Portanto, se removermos a chamada para Rnd na linha 98 e substituí-la pelo snippet

mFork.GrabFork (Me)

mKnife.GrabKnife (Me)

impasse desaparece!

Colabore com os dados à medida que são criados

Em aplicativos multithread, geralmente há uma situação em que os threads não apenas funcionam com dados compartilhados, mas também esperam que eles apareçam (ou seja, o thread 1 deve criar dados antes que o thread 2 possa usá-los). Como os dados são compartilhados, o acesso a eles precisa ser sincronizado. Também é necessário fornecer meios para notificar os threads em espera sobre o aparecimento de dados prontos.

Esta situação é geralmente chamada de o problema do fornecedor / consumidor. O encadeamento está tentando acessar dados que ainda não existem, portanto, ele deve transferir o controle para outro encadeamento que cria os dados necessários. O problema é resolvido com o seguinte código:

  • O thread 1 (consumidor) é ativado, entra em um método sincronizado, procura dados, não os encontra e entra em um estado de espera. Preliminarmentefisicamente, ele deve remover o bloqueio para não atrapalhar o trabalho do fio fornecedor.
  • Thread 2 (provedor) entra em um método sincronizado liberado pelo thread 1, cria dados para o fluxo 1 e de alguma forma notifica o fluxo 1 sobre a presença de dados. Em seguida, ele libera o bloqueio para que o thread 1 possa processar os novos dados.

Não tente resolver este problema invocando constantemente o encadeamento 1 e verificando a condição da variável de condição, cujo valor é> definido pelo encadeamento 2. Esta decisão afetará seriamente o desempenho do seu programa, uma vez que na maioria dos casos o encadeamento 1 irá ser invocado sem motivo; e o thread 2 irá esperar com tanta freqüência que ficará sem tempo para criar dados.

Relacionamentos provedor / consumidor são muito comuns, portanto, primitivas especiais são criadas para tais situações em bibliotecas de classes de programação multithread. No NET, essas primitivas são chamadas Wait e Pulse-PulseAl 1 e fazem parte da classe Monitor. A Figura 10.8 ilustra a situação que estamos prestes a programar. O programa organiza três filas de encadeamento: uma fila de espera, uma fila de bloqueio e uma fila de execução. O planejador de thread não aloca tempo de CPU para threads que estão na fila de espera. Para que um thread tenha tempo alocado, ele deve ser movido para a fila de execução. Como resultado, o trabalho do aplicativo é organizado com muito mais eficiência do que com a pesquisa usual de uma variável condicional.

No pseudocódigo, o idioma do consumidor de dados é formulado da seguinte forma:

"Entrada em um bloco sincronizado do seguinte tipo

Enquanto não há dados

Vá para a fila de espera

Ciclo

Se houver dados, processe-os.

Sair do bloco sincronizado

Imediatamente após a execução do comando Wait, o encadeamento é suspenso, o bloqueio é liberado e o encadeamento entra na fila de espera. Quando o bloqueio é liberado, o encadeamento na fila de execução pode ser executado. Com o tempo, um ou mais encadeamentos bloqueados criarão os dados necessários para a operação do encadeamento que está na fila de espera. Como a validação de dados é realizada em um loop, a transição para o uso dos dados (após o loop) ocorre apenas quando há dados prontos para processamento.

Em pseudocódigo, o idioma do provedor de dados é semelhante a este:

"Entrando em um bloco de visão sincronizada

Embora os dados NÃO sejam necessários

Vá para a fila de espera

Else Produce Data

Quando os dados estiverem prontos, ligue para Pulse-PulseAll.

para mover um ou mais threads da fila de bloqueio para a fila de execução. Saia do bloco sincronizado (e volte para a fila de execução)

Suponha que nosso programa simule uma família com um dos pais que ganha dinheiro e um filho que gasta esse dinheiro. Quando o dinheiro acabarAcontece que a criança tem que esperar a chegada de uma nova quantia. A implementação de software deste modelo se parece com isto:

1 opção estrita em

2 Sistema de Importações.Threading

Módulo de 3 Módulos

4 Sub Main ()

5 Dim theFamily As New Family ()

6 theFamily.StartltsLife ()

7 End Sub

8 Fim do fjódulo

9

10 Família de Classe Pública

11 mMoney privado como inteiro

12 Privado mWeek As Integer = 1

13 Public Sub StartltsLife ()

14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)

16 Dim aThread como novo thread (aThreadStart)

17 Dim bThread como novo tópico (bThreadStart)

18 aThread.Name = "Produzir"

19 aThread.Start ()

20 bThread.Name = "Consumir"

21 bThread. Começar ()

22 End Sub

23 Propriedade Pública TheWeek () As Integer

24 Get

25 Retorno na semana

26 End Get

27 Set (ByVal Value As Integer)

28 semanas - Valor

29 End Set

30 End Property

31 Propriedade Pública OurMoney () As Integer

32 Get

33 Return mMoney

34 End Get

35 Set (ByVal Value As Integer)

36 mMoney = Valor

37 End Set

38 End Property

39 Subproduto público ()

40 Thread.Sleep (500)

41 Do

42 Monitor.Enter (Me)

43 Do While Me.OurMoney> 0

44 Monitor.Wait (Me)

45 Loop

46 Me.OurMoney = 1000

47 Monitor.PulseAll (Me)

48 Monitor.Exit (Me)

49 Loop

50 End Sub

51 Subconsumo público ()

52 MsgBox ("Estou no segmento de consumo")

53 Faça

54 Monitor.Enter (Me)

55 Do While Me.OurMoney = 0

56 Monitor.Wait (Me)

57 Loop

58 Console.WriteLine ("Prezado pai, acabei de gastar todos os seus" & _

dinheiro na semana "& TheWeek)

59 TheWeek + = 1

60 Se TheWeek = 21 * 52 Then System.Environment.Exit (0)

61 Me.OurMoney = 0

62 Monitor.PulseAll (Me)

63 Monitor.Exit (Me)

64 Loop

65 End Sub

66 Fim da aula

O método StartltsLife (linhas 13-22) se prepara para iniciar os fluxos de produção e consumo. A coisa mais importante acontece nos fluxos Produce (linhas 39-50) e Consume (linhas 51-65). O procedimento Subproduce verifica a disponibilidade de dinheiro e, se houver dinheiro, ele vai para a fila de espera. Caso contrário, o pai gera dinheiro (linha 46) e notifica os objetos na fila de espera sobre uma mudança na situação. Observe que a chamada para Pulse-Pulse All tem efeito somente quando o bloqueio é liberado com o comando Monitor.Exit. Por outro lado, o procedimento Sub Consume verifica a disponibilidade de dinheiro e, se não houver dinheiro, notifica o pai / mãe grávida sobre isso. A linha 60 simplesmente termina o programa após 21 anos condicionais; chamando o sistema. Environment.Exit (0) é o análogo .NET do comando End (o comando End também é suportado, mas ao contrário de System. Environment. Exit, ele não retorna um código de saída para o sistema operacional).

Os threads que são colocados na fila de espera devem ser liberados por outras partes do seu programa. É por esse motivo que preferimos usar o PulseAll em vez do Pulse. Como não se sabe com antecedência qual thread será ativado quando o Pulso 1 for chamado, com um número relativamente pequeno de threads na fila, você também pode chamar PulseAll.

Multithreading em programas gráficos

Nossa discussão sobre multithreading em aplicativos GUI começa com um exemplo que explica para que serve o multithreading em aplicativos GUI. Crie um formulário com dois botões Iniciar (btnStart) e Cancelar (btnCancel), conforme mostrado na Fig. 10,9. Clicar no botão Iniciar gera uma classe que contém uma sequência aleatória de 10 milhões de caracteres e um método para contar as ocorrências da letra "E" nessa longa sequência. Observe o uso da classe StringBuilder para uma criação mais eficiente de strings longas.

Passo 1

O thread 1 percebe que não há dados para ele. Ele chama Wait, libera o bloqueio e vai para a fila de espera.



Passo 2

Quando o bloqueio é liberado, o thread 2 ou thread 3 deixa a fila de blocos e entra em um bloco sincronizado, adquirindo o bloqueio

Etapa 3

Digamos que o thread 3 entre em um bloco sincronizado, crie dados e chame Pulse-Pulse All.

Imediatamente após sair do bloco e liberar o bloqueio, o thread 1 é movido para a fila de execução. Se o thread 3 chamar Pluse, apenas um entrará na fila de execuçãothread, quando Pluse All é chamado, todos os threads vão para a fila de execução.



Arroz. 10,8. Problema do provedor / consumidor

Arroz. 10,9. Multithreading em um aplicativo GUI simples

Imports System.Text

Public Class RandomCharacters

M_Data privado como StringBuilder

Mjength privado, m_count como inteiro

Public Sub New (ByVal n As Integer)

m_Length = n -1

m_Data = Novo StringBuilder (m_length) MakeString ()

End Sub

Sub MakeString privado ()

Dim i As Integer

Dim myRnd como novo aleatório ()

Para i = 0 até m_length

"Gere um número aleatório entre 65 e 90,

"converta para maiúsculas

"e anexe ao objeto StringBuilder

m_Data.Append (Chr (myRnd.Next (65.90)))

Próximo

End Sub

Sub StartCount pública ()

GetEes ()

End Sub

Sub GetEes privados ()

Dim i As Integer

Para i = 0 até m_length

Se m_Data.Chars (i) = CChar ("E") Então

m_count + = 1

End If Next

m_CountDone = True

End Sub

Público somente leitura

Propriedade GetCount () As Integer Get

Se não (m_CountDone) Então

Retornar m_count

Fim se

End Get End Property

Público somente leitura

Propriedade IsDone () As Boolean Get

Retornar

m_CountDone

End Get

Propriedade Final

Fim da aula

Existe um código muito simples associado aos dois botões do formulário. O procedimento btn-Start_Click instancia a classe RandomCharacters acima, que encapsula uma string com 10 milhões de caracteres:

Private Sub btnStart_Click (ByVal sender As System.Object.

ByVal e As System.EventArgs) Lida com btnSTart.Click

Dim RC como novos caracteres aleatórios (10000000)

RC.StartCount ()

MsgBox ("O número de es é" & RC.GetCount)

End Sub

O botão Cancelar exibe uma caixa de mensagem:

Private Sub btnCancel_Click (ByVal sender As System.Object._

ByVal e As System.EventArgs) Lida com btnCancel.Click

MsgBox ("Contagem interrompida!")

End Sub

Quando o programa é executado e o botão Iniciar é pressionado, verifica-se que o botão Cancelar não está respondendo à entrada do usuário porque o loop contínuo impede que o botão manipule o evento que recebe. Isso é inaceitável em programas modernos!

Há duas soluções possíveis. A primeira opção, bem conhecida das versões anteriores do VB, dispensa o multithreading: a chamada DoEvents é incluída no loop. No NET, esse comando é parecido com este:

Application.DoEvents ()

Em nosso exemplo, isso definitivamente não é desejável - quem quer desacelerar um programa com dez milhões de chamadas DoEvents! Se, em vez disso, você alocar o loop para um thread separado, o sistema operacional alternará entre os threads e o botão Cancelar permanecerá funcional. A implementação com um thread separado é mostrada abaixo. Para mostrar claramente que o botão Cancelar funciona, ao clicar nele, simplesmente encerramos o programa.

Próxima etapa: Mostrar botão de contagem

Digamos que você decidiu mostrar sua imaginação criativa e dar à forma o visual mostrado na fig. 10,9. Observação: o botão Mostrar contagem ainda não está disponível.

Arroz. 10,10. Formulário de botão bloqueado

Espera-se que um tópico separado faça a contagem e desbloqueie o botão indisponível. É claro que isso pode ser feito; além disso, essa tarefa surge com bastante frequência. Infelizmente, você não será capaz de agir da maneira mais óbvia - vincule o thread secundário ao thread da GUI, mantendo um link para o botão ShowCount no construtor, ou mesmo usando um delegado padrão. Em outras palavras, nunca não use a opção abaixo (básico errôneo linhas em negrito).

Public Class RandomCharacters

M_0ata privado como StringBuilder

Privado m_CountDone As Boolean

Mjength privado. m_count As Integer

M_Button privado como Windows.Forms.Button

Public Sub New (ByVa1 n As Integer, _

ByVal b As Windows.Forms.Button)

comprimento_m = n - 1

m_Data = Novo StringBuilder (mJength)

m_Button = b MakeString ()

End Sub

Sub MakeString privado ()

Dim I As Integer

Dim myRnd como novo aleatório ()

Para I = 0 até m_length

m_Data.Append (Chr (myRnd.Next (65.90)))

Próximo

End Sub

Sub StartCount pública ()

GetEes ()

End Sub

Sub GetEes privados ()

Dim I As Integer

Para I = 0 para mjength

Se m_Data.Chars (I) = CChar ("E") Então

m_count + = 1

End If Next

m_CountDone = True

m_Button.Enabled = True

End Sub

Público somente leitura

Propriedade GetCount () As Integer

Pegue

Se não (m_CountDone) Então

Lance uma nova exceção ("Contagem ainda não concluída") Else

Retornar m_count

Fim se

End Get

Propriedade Final

Propriedade pública somente leitura IsDone () como booleano

Pegue

Retornar m_CountDone

End Get

Propriedade Final

Fim da aula

É provável que esse código funcione em alguns casos. No entanto:

  • A interação do thread secundário com o thread de criação da GUI não pode ser organizada óbvio meios.
  • Nunca não modifique elementos em programas gráficos de outros fluxos de programa. Todas as alterações devem ocorrer apenas no thread que criou a GUI.

Se você quebrar essas regras, nós Nós garantimos que erros sutis ocorrerão em seus programas gráficos multithread.

Também não conseguirá organizar a interação de objetos usando eventos. O trabalhador de 06 eventos é executado no mesmo encadeamento que o RaiseEvent foi chamado, portanto, os eventos não irão ajudá-lo.

Ainda assim, o bom senso dita que os aplicativos gráficos devem ter um meio de modificar os elementos de outro encadeamento. No NET Framework, há uma maneira segura de thread para chamar métodos de aplicativos GUI de outro thread. Um tipo especial de delegado Method Invoker do namespace System.Windows é usado para essa finalidade. Formulários. O snippet a seguir mostra uma nova versão do método GetEes (linhas alteradas em negrito):

Sub GetEes privados ()

Dim I As Integer

Para I = 0 até m_length

Se m_Data.Chars (I) = CChar ("E") Então

m_count + = 1

End If Next

m_CountDone = Verdadeira tentativa

Dim mylnvoker As New Methodlnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Catch e As ThreadlnterruptedException

"Fracasso

Fim da tentativa

End Sub

Public Sub UpDateButton ()

m_Button.Enabled = True

End Sub

Chamadas entre threads para o botão não são feitas diretamente, mas por meio do Invocador de Método. O .NET Framework garante que essa opção seja segura para threads.

Por que existem tantos problemas com a programação multithread?

Agora que você tem algum conhecimento sobre multithreading e os problemas potenciais associados a ele, decidimos que seria apropriado responder à pergunta no título desta subseção no final deste capítulo.

Um dos motivos é que o multithreading é um processo não linear e estamos acostumados com um modelo de programação linear. No início, é difícil se acostumar com a própria ideia de que a execução do programa pode ser interrompida aleatoriamente e o controle será transferido para outro código.

No entanto, há outra razão mais fundamental: hoje em dia, os programadores muito raramente programam em assembler, ou pelo menos olham para a saída desmontada do compilador. Caso contrário, seria muito mais fácil para eles se acostumarem com a ideia de que dezenas de instruções de montagem podem corresponder a um comando de uma linguagem de alto nível (como VB .NET). O thread pode ser interrompido após qualquer uma dessas instruções e, portanto, no meio de um comando de alto nível.

Mas isso não é tudo: os compiladores modernos otimizam o desempenho do programa e o hardware do computador pode interferir no gerenciamento da memória. Como resultado, o compilador ou hardware pode alterar a ordem dos comandos especificados no código-fonte do programa sem o seu conhecimento [ Muitos compiladores otimizam a cópia cíclica de matrizes como para i = 0 a n: b (i) = a (i): ncxt. O compilador (ou mesmo um gerenciador de memória especializado) pode simplesmente criar um array e preenchê-lo com uma única operação de cópia em vez de copiar elementos individuais muitas vezes!].

Esperançosamente, essas explicações irão ajudá-lo a entender melhor por que a programação multithread causa tantos problemas - ou pelo menos menos surpresa com o comportamento estranho de seus programas multithread!

Um exemplo de construção de um aplicativo multi-thread simples.

Nasceu da causa de muitas perguntas sobre a construção de aplicativos multithread no Delphi.

O objetivo deste exemplo é demonstrar como construir corretamente um aplicativo multi-threaded, com a remoção do trabalho de longo prazo em um thread separado. E como, em tal aplicação, garantir a interação do thread principal com o trabalhador para a transferência de dados do formulário (componentes visuais) para o stream e vice-versa.

O exemplo não pretende ser completo, apenas demonstra as formas mais simples de interação entre threads. Permitir ao usuário "cegar rapidamente" (quem sabe o quanto eu odeio) um aplicativo multithread que funciona corretamente.
Tudo nele é comentado em detalhes (na minha opinião), mas se você tiver alguma dúvida, pergunte.
Mas mais uma vez te aviso: Streams não são fáceis... Se você não tem ideia de como tudo funciona, existe um grande perigo de que muitas vezes tudo funcione bem para você e, às vezes, o programa se comporte de forma mais do que estranha. O comportamento de um programa multithread escrito incorretamente é altamente dependente de um grande número de fatores que às vezes não podem ser reproduzidos durante a depuração.

Então, um exemplo. Por conveniência, coloquei o código e anexei o arquivo com o módulo e o código do formulário

unit ExThreadForm;

usa
Windows, Mensagens, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

// constantes usadas ao transferir dados de um fluxo para um formulário usando
// enviar mensagens de janela
const
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

modelo
// descrição da classe do thread, um descendente de tThread
tMyThread = class (tThread)
privado
SyncDataN: Integer;
SyncDataS: String;
procedimento SyncMetod1;
protegido
procedimento Execute; sobrepor;
público
Param1: String;
Param2: Integer;
Param3: Boolean;
Parado: Booleano;
LastRandom: Integer;
IterationNo: Integer;
ResultList: tStringList;

Criação do construtor (aParam1: String);
destruidor Destroy; sobrepor;
fim;

// descrição da classe do formulário usando o fluxo
TForm1 = classe (TForm)
Label1: TLabel;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
procedimento btnStartClick (Sender: TObject);
procedimento btnStopClick (Sender: TObject);
privado
(Declarações privadas)
MyThread: tMyThread;
procedimento EventMyThreadOnTerminate (Sender: tObject);
procedimento EventOnSendMessageMetod (var Msg: TMessage); mensagem WM_USER_SendMessageMetod;
procedimento EventOnPostMessageMetod (var Msg: TMessage); mensagem WM_USER_PostMessageMetod;

Público
(Declarações públicas)
fim;

var
Form1: TForm1;

{
Parado - demonstra a transferência de dados de um formulário para um fluxo.
Sincronização adicional não é necessária, pois é simples
tipo de palavra única e é escrito por apenas um segmento.
}

procedimento TForm1.btnStartClick (Sender: TObject);
começar
Aleatória (); // garantindo aleatoriedade na sequência por Random () - não tem nada a ver com o fluxo

// Cria uma instância do objeto stream, passando um parâmetro de entrada
{
ATENÇÃO!
O construtor de fluxo é escrito de forma que o fluxo seja criado
suspenso, pois permite:
1. Controle o momento de seu lançamento. Isso quase sempre é mais conveniente porque
permite que você configure um stream antes mesmo de começar, passe a entrada
parâmetros, etc.
2. Porque o link para o objeto criado será salvo no campo do formulário, a seguir
após a autodestruição do thread (veja abaixo) que quando o thread está em execução
pode ocorrer a qualquer momento, este link se tornará inválido.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// No entanto, como o thread foi criado suspenso, em caso de erros
// durante sua inicialização (antes de começar), devemos destruí-lo nós mesmos
// para o que usamos try / except block
Experimente

// Atribuindo um manipulador de terminação de thread em que receberemos
// os resultados do trabalho do stream e "sobrescrever" o link para ele
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Uma vez que os resultados serão coletados no OnTerminate, ou seja, antes da autodestruição
// o riacho, então vamos tirar as preocupações de destruí-lo
MyThread.FreeOnTerminate: = True;

// Um ​​exemplo de passagem de parâmetros de entrada pelos campos do objeto de fluxo, no ponto
// instancia quando ainda não estiver em execução.
// Pessoalmente, prefiro fazer isso por meio dos parâmetros do sobrescrito
// construtor (tMyThread.Create)
MyThread.Param2: = StrToInt (Form1.Edit2.Text);

MyThread.Stopped: = False; // um tipo de parâmetro também, mas mudando em
// tempo de execução do thread
exceto
// uma vez que o thread ainda não foi iniciado e não será capaz de se autodestruir, iremos destruí-lo "manualmente"
FreeAndNil (MyThread);
// e, em seguida, deixe a exceção ser tratada como de costume
levantar;
fim;

// Uma vez que o objeto thread foi criado e configurado com sucesso, é hora de iniciá-lo
MyThread.Resume;

ShowMessage ("Stream iniciado");
fim;

procedimento TForm1.btnStopClick (Sender: TObject);
começar
// Se a instância do thread ainda existir, peça para ela parar
// E, exatamente "perguntar". Em princípio, também podemos "forçar", mas vai
// opção extremamente emergencial, exigindo um entendimento claro de tudo isso
// cozinha de streaming. Portanto, não é considerado aqui.
se atribuído (MyThread), então
MyThread.Stopped: = True
outro
ShowMessage ("O tópico não está em execução!");
fim;

procedimento TForm1.EventOnSendMessageMetod (var Msg: TMessage);
começar
// método para processar uma mensagem síncrona
// em WParam o endereço do objeto tMyThread, em LParam o valor atual de LastRandom do thread
com tMyThread (Msg.WParam) comece
Form1.Label3.Caption: = Format ("% d% d% d",);
fim;
fim;

procedimento TForm1.EventOnPostMessageMetod (var Msg: TMessage);
começar
// método para lidar com uma mensagem assíncrona
// em WParam o valor atual de IterationNo, em LParam o valor atual do fluxo LastRandom
Form1.Label4.Caption: = Format ("% d% d",);
fim;

procedimento TForm1.EventMyThreadOnTerminate (Sender: tObject);
começar
// IMPORTANTE!
// O método para lidar com o evento OnTerminate é sempre chamado no contexto do principal
// thread - isso é garantido pela implementação tThread. Portanto, nele você pode livremente
// use quaisquer propriedades e métodos de quaisquer objetos

// Por precaução, certifique-se de que a instância do objeto ainda exista
se não atribuído (MyThread), saia; // se não estiver lá, então não há nada a fazer

// obtém os resultados do trabalho da thread da instância do objeto thread
Form1.Memo1.Lines.Add (Format ("O fluxo terminou com o resultado% d",));
Form1.Memo1.Lines.AddStrings ((Remetente como tMyThread) .ResultList);

// Destrói a referência à instância do objeto stream.
// Uma vez que nosso thread é autodestrutivo (FreeOnTerminate: = True)
// então, após a conclusão do manipulador OnTerminate, a instância do objeto de fluxo será
// destruído (gratuito) e todas as referências a ele se tornarão inválidas.
// Para não encontrar acidentalmente esse link, iremos bloquear MyThread
// Vou observar mais uma vez - não vamos destruir o objeto, mas apenas sobrescrever o link. Um objeto
// destruir a si mesmo!
MyThread: = Nil;
fim;

construtor tMyThread.Create (aParam1: String);
começar
// Crie uma instância do stream SUSPENDED (veja o comentário ao instanciar)
Criar herdado (Verdadeiro);

// Crie objetos internos (se necessário)
ResultList: = tStringList.Create;

// Obtenha os dados iniciais.

// Copia os dados de entrada passados ​​pelo parâmetro
Param1: = aParam1;

// Um ​​exemplo de recebimento de dados de entrada de componentes VCL no construtor de um objeto de fluxo
// Isso é aceitável neste caso, uma vez que o construtor é chamado no contexto
// tópico principal. Portanto, os componentes da VCL podem ser acessados ​​aqui.
// Mas, eu não gosto disso, porque acho que é ruim quando o segmento sabe de algo
// sobre alguma forma lá. Mas, o que você não pode fazer para demonstração.
Param3: = Form1.CheckBox1.Checked;
fim;

destructor tMyThread.Destroy;
começar
// destruição de objetos internos
FreeAndNil (ResultList);
// destrói a base tThread
herdado;
fim;

procedure tMyThread.Execute;
var
t: Cardeal;
s: String;
começar
IterationNo: = 0; // contador de resultados (número do ciclo)

// No meu exemplo, o corpo do thread é um loop que termina
// ou por um "pedido" externo de finalização passado através do parâmetro variável Parado,
// quer apenas fazendo 5 loops
// É mais agradável para mim escrever isso por meio de um loop "eterno".

Enquanto True comece

Inc (IterationNo); // número do próximo ciclo

LastRandom: = Aleatório (1000); // número da chave - para demonstrar a transferência de parâmetros do fluxo para o formulário

T: = Aleatório (5) +1; // hora em que adormeceremos se não terminarmos

// Trabalho idiota (dependendo do parâmetro de entrada)
se não Param3, então
Inc (Param2)
outro
Dez (Param2);

// Forme um resultado intermediário
s: = Formato ("% s% 5d% s% d% d",
);

// Adicione um resultado intermediário à lista de resultados
ResultList.Add (s);

//// Exemplos de passagem de um resultado intermediário para um formulário

//// Passando por um método sincronizado - a maneira clássica
//// Desvantagens:
//// - o método que está sendo sincronizado é geralmente um método da classe stream (para acessar
//// aos campos do objeto stream), mas, para acessar os campos do formulário, ele deve
//// "saber" sobre ele e seus campos (objetos), o que geralmente não é muito bom com
//// ponto de vista da organização do programa.
//// - o thread atual será suspenso até que a execução seja concluída
//// método sincronizado.

//// Vantagens:
//// - padrão e versátil
//// - em um método sincronizado, você pode usar
//// todos os campos do objeto stream.
// primeiro, se necessário, você precisa salvar os dados transferidos em
// campos especiais do objeto objeto.
SyncDataN: = IterationNo;
SyncDataS: = "Sincronizar" + s;
// e, em seguida, fornece uma chamada de método sincronizada
Sincronizar (SyncMetod1);

//// Enviando via envio de mensagem síncrona (SendMessage)
//// neste caso, os dados podem ser passados ​​tanto por meio de parâmetros de mensagem (LastRandom),
//// e pelos campos do objeto, passando o endereço da instância no parâmetro mensagem
//// do objeto stream - Integer (Self).
//// Desvantagens:
//// - o segmento deve conhecer o identificador da janela do formulário
//// - como com Sincronizar, o tópico atual será suspenso até
//// terminando o processamento da mensagem pelo thread principal
//// - requer uma quantidade significativa de tempo de CPU para cada chamada
//// (para alternar tópicos), portanto, chamadas muito frequentes são indesejáveis
//// Vantagens:
//// - assim como em Sincronizar, ao processar uma mensagem, você pode usar
//// todos os campos do objeto stream (se, é claro, seu endereço foi passado)


//// iniciar o tópico.
SendMessage (Form1.Handle, WM_USER_SendMessageMetod, Integer (Self), LastRandom);

//// Transferir via envio de mensagem assíncrona (PostMessage)
//// Visto que, neste caso, no momento em que a mensagem é recebida pelo thread principal,
//// o fluxo de envio pode já ter terminado, passando o endereço da instância
//// objeto de stream é inválido!
//// Desvantagens:
//// - o segmento deve conhecer o identificador da janela do formulário;
//// - devido à assincronia, a transferência de dados só é possível por meio de parâmetros
//// mensagens, o que complica significativamente a transferência de dados que têm um tamanho
//// mais de duas palavras de máquina. É conveniente usar para passar inteiros, etc.
//// Vantagens:
//// - ao contrário dos métodos anteriores, o tópico atual NÃO
//// pausado, mas retoma a execução imediatamente
//// - ao contrário de uma chamada sincronizada, um gerenciador de mensagens
//// é um método de formulário que deve ter conhecimento do objeto stream,
//// ou não sabe nada sobre o fluxo se os dados são transmitidos apenas
//// via parâmetros de mensagem. Ou seja, o thread pode não saber nada sobre o formulário.
//// geralmente - apenas seu Handle, que pode ser passado como um parâmetro antes
//// iniciar o tópico.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Verifique para possível conclusão

// Verifique a conclusão por parâmetro
se Parado, então Break;

// Verifique a conclusão na ocasião
se IterationNo> = 10 então Break;

Sono (t * 1000); // Adormecer por t segundos
fim;
fim;

procedure tMyThread.SyncMetod1;
começar
// este método é chamado por meio do método Synchronize.
// Ou seja, apesar de ser um método do thread tMyThread,
// é executado no contexto do thread principal do aplicativo.
// Portanto, ele pode fazer qualquer coisa, bem, ou quase tudo :)
// Mas lembre-se, não vale a pena "brincar" aqui por muito tempo

// Os parâmetros passados, podemos extrair dos campos especiais, onde os temos
// salvo antes de chamar.
Form1.Label1.Caption: = SyncDataS;

// seja de outros campos do objeto stream, por exemplo, refletindo seu estado atual
Form1.Label2.Caption: = Format ("% d% d",);
fim;

Em geral, o exemplo foi precedido pelo seguinte raciocínio sobre o tema ...

Em primeiro lugar:
A regra MAIS IMPORTANTE de programação multithread no Delphi é:
No contexto de um thread não principal, é impossível acessar as propriedades e métodos de formulários e, de fato, todos os componentes que "crescem" a partir de tWinControl.

Isso significa (um tanto simplificado) que nem no método Execute herdado de TThread, nem em outros métodos / procedimentos / funções chamados de Execute, é proibido acesse diretamente quaisquer propriedades e métodos de componentes visuais.

Como fazer direito.
Não existem receitas uniformes. Mais precisamente, são tantas e diferentes opções que, dependendo do caso específico, terá de escolher. Portanto, eles se referem ao artigo. Depois de lê-lo e compreendê-lo, o programador será capaz de entender e a melhor forma de fazê-lo em um caso particular.

Resumindo em seus dedos:

Na maioria das vezes, um aplicativo multithread se torna quando é necessário fazer algum tipo de trabalho de longo prazo ou quando é possível fazer simultaneamente várias coisas que não sobrecarregam o processador.

No primeiro caso, a implementação do trabalho dentro do thread principal leva a uma "desaceleração" da interface do usuário - enquanto o trabalho está sendo feito, o loop de mensagem não é executado. Como resultado, o programa não responde às ações do usuário e o formulário não é desenhado, por exemplo, depois que o usuário o move.

No segundo caso, quando o trabalho envolve uma troca ativa com o mundo exterior, então durante o "tempo de inatividade" forçado. Enquanto espera receber / enviar dados, você pode fazer outra coisa em paralelo, por exemplo, novamente, enviar / receber dados.

Existem outros casos, mas com menos frequência. No entanto, isso não importa. Agora não é sobre isso.

Agora, como está tudo escrito. Naturalmente, um certo caso mais frequente, um tanto generalizado, é considerado. Então.

O trabalho realizado em thread separado, no caso geral, tem quatro entidades (não sei como chamá-lo mais precisamente):
1. Dados iniciais
2. Na verdade, o próprio trabalho (pode depender dos dados iniciais)
3. Dados intermediários (por exemplo, informações sobre o estado atual da execução do trabalho)
4. Dados de saída (resultado)

Na maioria das vezes, os componentes visuais são usados ​​para ler e exibir a maioria dos dados. Mas, como mencionado acima, você não pode acessar diretamente os componentes visuais do stream. Como ser?
Os desenvolvedores do Delphi sugerem o uso do método Synchronize da classe TThread. Aqui não vou descrever como usá-lo - existe o artigo mencionado anteriormente para isso. Permitam-me apenas dizer que sua aplicação, mesmo a correta, nem sempre é justificada. Existem dois problemas:

Primeiro, o corpo de um método chamado via Synchronize é sempre executado no contexto da thread principal e, portanto, enquanto está sendo executado, o loop de mensagem da janela não é executado novamente. Portanto, deve ser executado rapidamente, caso contrário, teremos os mesmos problemas de uma implementação de thread único. Idealmente, um método chamado via Synchronize deve geralmente ser usado apenas para acessar propriedades e métodos de objetos visuais.

Em segundo lugar, executar um método por meio de Sincronizar é um prazer "caro" devido à necessidade de duas alternâncias entre threads.

Além disso, os dois problemas estão interligados, e causam uma contradição: por um lado, para resolver o primeiro, é necessário "moer" os métodos chamados por Synchronize, e por outro, muitas vezes eles precisam ser chamados, perdendo preciosos recursos do processador.

Portanto, como sempre, é necessário abordar razoavelmente, e para diferentes casos, utilizar diferentes formas de interação do fluxo com o mundo exterior:

Dados iniciais
Todos os dados que são transferidos para o fluxo, e não mudam durante sua operação, devem ser transferidos antes mesmo de começar, ou seja, ao criar um fluxo. Para usá-los no corpo de um thread, você precisa fazer uma cópia local deles (geralmente nos campos do descendente TThread).
Se houver dados iniciais que podem ser alterados durante a execução do thread, esses dados devem ser acessados ​​por meio de métodos sincronizados (métodos chamados por meio de Synchronize) ou por meio dos campos do objeto de thread (descendente de TThread). Este último requer algum cuidado.

Dados intermediários e de saída
Aqui, novamente, existem várias maneiras (na ordem de minha preferência):
- Método de envio assíncrono de mensagens para a janela principal do aplicativo.
Geralmente é usado para enviar mensagens sobre o andamento do processo para a janela principal do aplicativo, com a transferência de uma pequena quantidade de dados (por exemplo, percentual de conclusão)
- Método de envio síncrono de mensagens para a janela principal do aplicativo.
Normalmente usado para os mesmos fins do envio assíncrono, mas permite que você transfira uma grande quantidade de dados, sem criar uma cópia separada.
- Métodos sincronizados, se possível, combinando a transferência de tantos dados quanto possível em um método.
Também pode ser usado para recuperar dados de um formulário.
- Através dos campos do objeto stream, proporcionando acesso mutuamente exclusivo.
Mais detalhes podem ser encontrados no artigo.

Eh. Não funcionou por um curto período de tempo