Classe de thread em Delphi
Raptor[Estúdio Mental]
http://mental.mentsu.com
Quatro
Critical Section (CriticalSection) é uma tecnologia para proteção de acesso compartilhado a dados. Na verdade, é equivalente a uma variável booleana global. Mas seu funcionamento é diferente. Possui apenas duas operações: Entrar e Sair. Seus dois estados também podem ser considerados Verdadeiro e Falso, indicando respectivamente se está na seção crítica. Essas duas operações também são primitivas, portanto podem ser usadas para proteger dados compartilhados contra violações de acesso em aplicativos multithread.
O método de usar seções críticas para proteger dados compartilhados é muito simples: chame Enter para definir o sinalizador de seção crítica antes de cada acesso aos dados compartilhados, depois opere os dados e, finalmente, chame Leave para sair da seção crítica. Seu princípio de proteção é o seguinte: após um thread entrar na seção crítica, se outro thread também quiser acessar os dados neste momento, ele descobrirá que um thread já entrou na seção crítica ao chamar Enter, e então este thread será Desligue e espere que o thread atualmente na seção crítica chame Leave para sair da seção crítica. Quando outro thread concluir a operação e chamar Leave para sair, este thread será despertado, definirá o sinalizador da seção crítica e começará a operar os dados. evitando assim o conflito.
Tomando o InterlockedIncrement anterior como exemplo, usamos CriticalSection (API do Windows) para implementá-lo:
Var
InterlockedCrit: TRTLCriticalSection;
PRocedure InterlockedIncrement( var aValue : Integer );
Começar
EnterCriticalSection(InterlockedCrit);
Inc(aValor);
LeaveCriticalSection(InterlockedCrit);
Fim;
Agora observe o exemplo anterior:
1. O thread A entra na seção crítica (assumindo que os dados são 3)
2. O fio B entra na seção crítica Como A já está na seção crítica, B está suspenso.
3. Thread A adiciona um aos dados (agora 4)
4. O thread A sai da seção crítica e ativa o thread B (os dados atuais na memória são 4)
5. Thread B acorda e adiciona um aos dados (agora é 5)
6. O thread B sai da seção crítica e os dados atuais estão corretos.
É assim que as seções críticas protegem o acesso aos dados compartilhados.
Em relação ao uso de seções críticas, algo a ser observado é o tratamento de exceções durante o acesso aos dados. Porque se ocorrer uma exceção durante a operação de dados, a operação Leave não será executada. Como resultado, o thread que deveria ser despertado não será despertado, o que pode fazer com que o programa pare de responder. De modo geral, a abordagem correta é usar seções críticas da seguinte forma:
EnterCriticalSection
Tentar
//Opera os dados da seção crítica
Finalmente
Sair da Seção Crítica
Fim;
A última coisa a observar é que Event e CriticalSection são recursos do sistema operacional, que precisam ser criados antes do uso e liberados após o uso. Por exemplo, um Event: SyncEvent global e um CriticalSection: TheadLock global usado pela classe TThread são criados e lançados em InitThreadSynchronization e DoneThreadSynchronization e são chamados na unidade Inicialização e Finalização das Classes.
Como as APIs são usadas para operar Event e CriticalSection em TThread, a API é usada como exemplo acima. Na verdade, o Delphi forneceu o encapsulamento deles na unidade SyncObjs, eles são a classe TEvent e a classe TCriticalSection, respectivamente. O uso é quase igual ao método anterior de uso da API. Como o construtor do TEvent possui muitos parâmetros, para simplificar, o Delphi também fornece uma classe Event inicializada com parâmetros padrão: TSimpleEvent.
A propósito, deixe-me apresentar outra classe usada para sincronização de threads: TMultiReadExclusiveWriteSynchronizer, que é definida na unidade SysUtils. Até onde eu sei, este é o nome de classe mais longo definido no Delphi RTL. Felizmente, ele tem um alias curto: TMREWSync. Quanto ao seu uso, acho que você pode saber só de olhar o nome, então não vou falar mais nada.
Com o conhecimento preparatório prévio sobre Event e CriticalSection, podemos começar oficialmente a discutir Synchronize e WaitFor.
Sabemos que Synchronize consegue a sincronização do thread colocando parte do código no thread principal para execução, pois em um processo existe apenas um thread principal. Vejamos primeiro a implementação do Synchronize:
procedimento TThread.Synchronize(Método: TThreadMethod);
começar
FSynchronize.FThread := Próprio;
FSynchronize.FSynchronizeException := nil;
FSynchronize.FMethod := Método;
Sincronizar(@FSynchronize);
fim;
onde FSynchronize é um tipo de registro:
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = registro
FThread: TObject;
FMethod: TThreadMethod;
FSynchronizeException: TObject;
fim;
Usado para troca de dados entre threads e o thread principal, incluindo objetos de classe de thread de entrada, métodos de sincronização e exceções que ocorrem.
Uma versão sobrecarregada dele é chamada em Synchronize, e essa versão sobrecarregada é bastante especial, é um "método de classe". O chamado método de classe é um método especial de membro de classe. Sua invocação não requer a criação de uma instância de classe, mas é chamada através do nome da classe como um construtor. A razão pela qual ele é implementado usando um método de classe é que ele pode ser chamado mesmo quando o objeto thread não é criado. Porém, na prática, outra versão sobrecarregada dele (também um método de classe) e outro método de classe StaticSynchronize são usados. Aqui está o código para esta sincronização:
procedimento de classe TThread.Synchronize(ASyncRec: PSynchronizeRecord);
var
SyncProc: TSyncProc;
começar
se GetCurrentThreadID = MainThreadID então
ASyncRec.FMethod
outro
começar
SyncProc.Signal := CreateEvent(nil, True, False, nil);
tentar
EnterCriticalSection(ThreadLock);
tentar
se SyncList = nulo então
ListaSincronizada := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
SignalSyncEvent;
se atribuído (WakeMainThread) então
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection(ThreadLock);
tentar
WaitForSingleObject(SyncProc.Signal, INFINITE);
finalmente
EnterCriticalSection(ThreadLock);
fim;
finalmente
LeaveCriticalSection(ThreadLock);
fim;
finalmente
CloseHandle(SyncProc.Signal);
fim;
se Assigned(ASyncRec.FSynchronizeException) então aumente ASyncRec.FSynchronizeException;
fim;
fim;
Este código é um pouco mais longo, mas não é muito complicado.
A primeira é determinar se o thread atual é o thread principal. Nesse caso, basta executar o método de sincronização e retornar.
Se não for o thread principal, está pronto para iniciar o processo de sincronização.
Os dados de troca de thread (parâmetros) e um Event Handle são registrados por meio da variável local SyncProc. A estrutura do registro é a seguinte:
TSyncProc=registro
SyncRec: PSynchronizeRecord;
Sinal: THandle;
fim;
Em seguida, crie um Evento, entre na seção crítica (por meio da variável global ThreadLock, porque apenas um thread pode entrar no estado Sincronizar ao mesmo tempo, para que você possa usar a variável global para registrar) e, em seguida, armazene os dados gravados no Lista SyncList (se esta lista não existir, crie-a). Pode-se ver que a seção crítica do ThreadLock é proteger o acesso ao SyncList. Isso será visto novamente quando CheckSynchronize for introduzido posteriormente.
A próxima etapa é chamar SignalSyncEvent. Seu código foi introduzido antes ao introduzir o construtor TThread. Sua função é simplesmente executar uma operação Set em SyncEvent. O objetivo deste SyncEvent será detalhado posteriormente, quando o WaitFor for introduzido.
A seguir vem a parte mais importante: chamar o evento WakeMainThread para operações de sincronização. WakeMainThread é um evento global do tipo TNotifyEvent. A razão pela qual os eventos são usados para processamento aqui é porque o método Synchronize essencialmente coloca o processo que precisa ser sincronizado no thread principal para execução por meio de mensagens. Ele não pode ser usado em alguns aplicativos sem loops de mensagens (como Console ou DLL). , então use este evento para processamento.
O objeto do aplicativo responde a esse evento Os dois métodos a seguir são usados para definir e limpar a resposta ao evento WakeMainThread (da unidade Forms):
procedimento TApplication.HookSynchronizeWakeup;
começar
Classes.WakeMainThread := WakeMainThread;
fim;
procedimento TApplication.UnhookSynchronizeWakeup;
começar
Classes.WakeMainThread := nil;
fim;
Os dois métodos acima são chamados no construtor e no destruidor da classe TApplication, respectivamente.
Este é o código que responde ao evento WakeMainThread no objeto Application. A mensagem é enviada aqui. Ele usa uma mensagem vazia para fazer isso:
procedimento TApplication.WakeMainThread(Remetente: TObject);
começar
PostMessage(Handle, WM_NULL, 0, 0);
fim;
A resposta a esta mensagem também está no objeto Application, veja o código a seguir (remover partes irrelevantes):
procedimento TApplication.WndProc(var Mensagem: TMessage);
…
começar
tentar
…
com mensagem faça
mensagem de caso de
…
WM_NULL:
Verificar Sincronizar;
…
exceto
HandleException(Self);
fim;
fim;
Entre eles, CheckSynchronize também é definido na unidade Classes Por ser relativamente complexo, não iremos explicá-lo em detalhes por enquanto. Apenas saiba que é a parte que trata especificamente da função Synchronize. código.
Após executar o evento WakeMainThread, saia da seção crítica e chame WaitForSingleObject para começar a aguardar o evento criado antes de entrar na seção crítica. A função deste Evento é aguardar o término da execução deste método de sincronização. Isso será explicado posteriormente na análise do CheckSynchronize.
Observe que após WaitForSingleObject você entra novamente na seção crítica, mas sai sem fazer nada. Parece sem sentido, mas é necessário!
Porque Enter e Leave na seção crítica devem corresponder estritamente um a um. Então, pode ser alterado para isto:
se atribuído (WakeMainThread) então
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
finalmente
LeaveCriticalSection(ThreadLock);
fim;
A maior diferença entre o código acima e o código original é que WaitForSingleObject também está incluído nas restrições da seção crítica. Parece não ter impacto e simplifica muito o código, mas é realmente possível?
Na verdade, não!
Porque sabemos que após a seção crítica Enter, se outros threads quiserem entrar novamente, eles serão suspensos. O método WaitFor suspenderá o thread atual e não será despertado até aguardar SetEvent de outros threads. Se o código for alterado para o acima, se o thread SetEvent também precisar entrar na seção crítica, ocorrerá um impasse (para a teoria do impasse, consulte as informações sobre os princípios do sistema operacional).
Deadlock é um dos aspectos mais importantes da sincronização de threads!
Finalmente, o Evento criado no início é liberado. Se o método sincronizado retornar uma exceção, a exceção será lançada novamente aqui.
(continua)