Conforme mencionado anteriormente, cada thread possui seus próprios recursos, mas a área de código é compartilhada, ou seja, cada thread pode executar a mesma função. O problema que isso pode causar é que vários threads executam uma função ao mesmo tempo, causando confusão de dados e resultados imprevisíveis, por isso devemos evitar esta situação.
C# fornece um bloqueio de palavra-chave, que pode definir uma seção de código como uma seção mutuamente exclusiva (seção crítica). A seção mutuamente exclusiva permite que apenas um thread entre em execução por vez, enquanto outros threads devem esperar. Em C#, a palavra-chave lock é definida da seguinte forma:
lock(expressão) instrução_block
expressão representa o objeto que você deseja rastrear, geralmente uma referência de objeto.
O Statement_block é o código da seção mutuamente exclusiva. Este código só pode ser executado por um thread por vez.
A seguir está um exemplo típico de uso da palavra-chave lock. O uso e a finalidade da palavra-chave lock são explicados nos comentários.
Os exemplos são os seguintes:
Código
usando o sistema;
usando System.Threading;
namespaceThreadSimple
{
classe interna Conta
{
saldo interno;
Aleatório r = novo Aleatório();
conta interna (int inicial)
{
saldo = inicial;
}
saque interno interno (valor interno)
{
se (saldo <0)
{
//Se o saldo for menor que 0, lança uma exceção
throw new Exception("Saldo Negativo");
}
//É garantido que o código a seguir seja concluído antes que o thread atual modifique o valor de balance.
//Nenhuma outra thread executará este código para modificar o valor do saldo
//Portanto, o valor do saldo não pode ser menor que 0
bloquear (isto)
{
Console.WriteLine("Tópico Atual:"+Thread.CurrentThread.Name);
//Se não houver proteção da palavra-chave lock, pode ser após a execução do julgamento condicional if.
//Outro thread executou balance=balance-amount e modificou o valor de balance.
//Esta modificação é invisível para este thread, portanto pode fazer com que a condição if não seja mais válida.
//No entanto, este thread continua a executar balance=balance-amount, então o balance pode ser menor que 0
if (saldo >= valor)
{
Thread.Sleep(5);
saldo = saldo - valor;
valor de devolução;
}
outro
{
return 0; // transação rejeitada
}
}
}
vazio interno DoTransactions()
{
para (int i = 0; i < 100; i++)
Retirar(r.Next(-50, 100));
}
}
teste de classe interna
{
Thread interno estático[] threads = new Thread[10];
público estático vazio Principal()
{
Conta conta = nova conta (0);
para (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(acc.DoTransactions));
tópicos[i] = t;
}
para (int i = 0; i < 10; i++)
threads[i].Name=i.ToString();
para (int i = 0; i < 10; i++)
tópicos[i].Start();
Console.ReadLine();
}
}
}A classe Monitor bloqueia um objeto
Quando vários threads compartilham um objeto, problemas semelhantes ao código comum também ocorrerão. Para esse tipo de problema, a palavra-chave lock não deve ser usada aqui. , o Monitor fornece uma solução para threads compartilharem recursos.
A classe Monitor pode bloquear um objeto e um thread pode operar no objeto somente se obtiver o bloqueio. O mecanismo de bloqueio de objeto garante que apenas um thread possa acessar esse objeto por vez em situações que podem causar caos. Monitor deve estar associado a um objeto específico, mas por ser uma classe estática, não pode ser usado para definir objetos e todos os seus métodos são estáticos e não podem ser referenciados por meio de objetos. O código a seguir ilustra o uso do Monitor para bloquear um objeto:
...
Fila oQueue=new Queue();
...
Monitor.Enter(oQueue);
......//Agora o objeto oQueue só pode ser manipulado pelo thread atual
Monitor.Exit(oQueue);//Libera o bloqueio
Conforme mostrado acima, quando um thread chama o método Monitor.Enter() para bloquear um objeto, o objeto é de propriedade dele. Se outros threads quiserem acessar esse objeto, eles só poderão esperar que ele use o Monitor.Exit(). método para liberar o bloqueio. Para garantir que o thread eventualmente libere o bloqueio, você pode escrever o método Monitor.Exit() no bloco de código finalmente na estrutura try-catch-finally.
Para qualquer objeto bloqueado pelo Monitor, algumas informações relacionadas a ele são armazenadas na memória:
Uma é uma referência ao thread que atualmente mantém o bloqueio;
A segunda é uma fila de preparação, que armazena threads prontas para adquirir bloqueios;
A terceira é uma fila de espera, que contém uma referência à fila que está atualmente aguardando a mudança do status do objeto.
Quando o thread que possui o bloqueio do objeto está pronto para liberar o bloqueio, ele usa o método Monitor.Pulse() para notificar o primeiro thread na fila de espera, para que o thread seja transferido para a fila de preparação quando o bloqueio do objeto for liberado. , na fila de preparação O thread pode adquirir imediatamente o bloqueio do objeto.
A seguir está um exemplo que mostra como usar a palavra-chave lock e a classe Monitor para implementar sincronização e comunicação de thread. Também é um problema típico de produtor e consumidor.
Nesta rotina, o thread produtor e o thread consumidor se alternam. O produtor escreve um número e o consumidor imediatamente o lê e exibe (a essência do programa é apresentada nos comentários).
Os namespaces do sistema usados são os seguintes:
usando o sistema;
usando System.Threading;
Primeiro, defina uma classe Cell do objeto que está sendo operado. Nesta classe, existem dois métodos: ReadFromCell() e WriteToCell. O thread consumidor chamará ReadFromCell() para ler o conteúdo de cellContents e exibi-lo, e o processo produtor chamará o método WriteToCell() para gravar dados em cellContents.
Os exemplos são os seguintes:
Código
Célula de classe pública
{
int cellContents; //Conteúdo do objeto Cell
bool readerFlag = false; // Sinalizador de status, quando for verdadeiro, pode ser lido, quando for falso, está escrevendo
público int ReadFromCell()
{
lock(this) // O que a palavra-chave Lock garante? Leia a introdução anterior sobre lock.
{
if (!readerFlag) //Se não estiver legível agora
{
tentar
{
//Aguarde que o método Monitor.Pulse() seja chamado no método WriteToCell
Monitorar.Aguarde(este);
}
pegar (SynchronizationLockException e)
{
Console.WriteLine(e);
}
pegar (ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consumir: {0}",cellContents);
leitorFlag = falso;
//Redefinir o sinalizador readerFlag para indicar que o comportamento de consumo foi concluído
Monitor.Pulse(isto);
//Notifica o método WriteToCell() (este método é executado em outra thread e está aguardando)
}
retornar conteúdo da célula;
}
público vazio WriteToCell (int n)
{
bloquear (isto)
{
if(leitorFlag)
{
tentar
{
Monitorar.Aguarde(este);
}
pegar (SynchronizationLockException e)
{
//Quando métodos sincronizados (referentes aos métodos da classe Monitor exceto Enter) são chamados em áreas de código assíncronas
Console.WriteLine(e);
}
pegar (ThreadInterruptedException e)
{
//Aborta quando a thread está em estado de espera
Console.WriteLine(e);
}
}
conteúdo da célula = n;
Console.WriteLine("Produzir: {0}",cellContents);
leitorFlag = verdadeiro;
Monitor.Pulse(este);
//Notifica o método ReadFromCell() em espera em outro thread
}
}
}
A classe produtora CellProd e a classe consumidora CellCons são definidas abaixo. Ambas têm apenas um método ThreadRun(), de modo que o objeto proxy ThreadStart fornecido ao thread na função Main() serve como entrada para o thread.
classe pública CellProd
{
Cell cell; // Objeto de célula sendo operado
int quantidade = 1; // O número de vezes que o produtor produz, inicializado em 1;
CellProd público (caixa de célula, solicitação interna)
{
//Construtor
célula = caixa;
quantidade = solicitação;
}
public voidThreadRun()
{
for(int looper=1; looper<=quantidade; looper++)
cell.WriteToCell(looper); //O produtor grava informações no objeto de operação
}
}
classe pública CellCons
{
Célula celular;
quantidade interna = 1;
CellCons públicos (caixa de celular, solicitação interna)
{
//Construtor
célula = caixa;
quantidade = solicitação;
}
public voidThreadRun()
{
int valRetornado;
for(int looper=1; looper<=quantidade; looper++)
valReturned=cell.ReadFromCell();//Consumidor lê informações do objeto de operação
}
}
Então, na função Main() da classe MonitorSample abaixo, o que temos que fazer é criar dois threads como produtores e consumidores, e usar o método CellProd.ThreadRun() e o método CellCons.ThreadRun() para realizar operações no mesmo Objeto de célula opera.
Código
classe pública MonitorSample
{
público estático void Principal(String[] args)
{
int result = 0; //Um bit de flag Se for 0, significa que não há erro no programa.
Célula célula = nova Célula ( );
//A seguir, usamos cell para inicializar as classes CellProd e CellCons. Os tempos de produção e consumo são ambos 20 vezes.
CellProd prod = new CellProd(célula, 20);
CellCons contras = new CellCons(célula, 20);
Produtor de thread = new Thread(new ThreadStart(prod.ThreadRun));
Consumidor de thread = new Thread(new ThreadStart(cons.ThreadRun));
//Tanto o thread produtor quanto o thread consumidor foram criados, mas a execução não foi iniciada.
tentar
{
produtor.Start();
consumidor.Start();
produtor.Join();
consumidor.Join();
Console.ReadLine();
}
pegar (ThreadStateException e)
{
//Quando a thread não consegue realizar a operação solicitada devido ao seu estado
Console.WriteLine(e);
resultado = 1;
}
pegar (ThreadInterruptedException e)
{
//Aborta quando a thread está em estado de espera
Console.WriteLine(e);
resultado = 1;
}
//Embora a função Main() não retorne um valor, a instrução a seguir pode retornar o resultado da execução ao processo pai
Ambiente.ExitCode = resultado;
}
}
Na rotina acima, a sincronização é feita aguardando Monitor.Pulse(). Primeiro, o produtor produz um valor e, ao mesmo tempo, o consumidor fica em estado de espera até receber um “Pulso” do produtor para notificá-lo de que a produção foi concluída. e o produtor começa a aguardar a conclusão do consumidor. O "pulso" emitido por Monitor.Pulese() será chamado após a operação.
O resultado de sua execução é simples:
Produzir: 1
Consumir: 1
Produzir: 2
Consumir: 2
Produzir: 3
Consumir: 3
...
...
Produzir: 20
Consumir: 20
Na verdade, este exemplo simples nos ajudou a resolver grandes problemas que podem surgir em aplicativos multithread. Contanto que você entenda os métodos básicos de resolução de conflitos entre threads, é fácil aplicá-lo a programas mais complexos.
-