Impasse ao fechar fio
-
23-08-2019 - |
Pergunta
Eu criei uma classe que abre uma porta COM e alças sobrepostas operações de leitura e gravação. Ele contém dois tópicos independentes - um que lê e aquele que grava dados. Ambos chamar procedimentos onxxx (por exemplo OnRead ou OnWrite) notificando sobre leitura acabado ou operação de gravação.
O seguinte é um pequeno exemplo da idéia de como os tópicos funcionam:
TOnWrite = procedure (Text: string);
TWritingThread = class(TThread)
strict private
FOnWrite: TOnWrite;
FWriteQueue: array of string;
FSerialPort: TAsyncSerialPort;
protected
procedure Execute; override;
public
procedure Enqueue(Text: string);
{...}
end;
TAsyncSerialPort = class
private
FCommPort: THandle;
FWritingThread: TWritingThread;
FLock: TCriticalSection;
{...}
public
procedure Open();
procedure Write(Text: string);
procedure Close();
{...}
end;
var
AsyncSerialPort: TAsyncSerialPort;
implementation
{$R *.dfm}
procedure OnWrite(Text: string);
begin
{...}
if {...} then
AsyncSerialPort.Write('something');
{...}
end;
{ TAsyncSerialPort }
procedure TAsyncSerialPort.Close;
begin
FLock.Enter;
try
FWritingThread.Terminate;
if FWritingThread.Suspended then
FWritingThread.Resume;
FWritingThread.WaitFor;
FreeAndNil(FWritingThread);
CloseHandle(FCommPort);
FCommPort := 0;
finally
FLock.Leave;
end;
end;
procedure TAsyncSerialPort.Open;
begin
FLock.Enter;
try
{open comm port}
{create writing thread}
finally
FLock.Leave;
end;
end;
procedure TAsyncSerialPort.Write(Text: string);
begin
FLock.Enter;
try
{add Text to the FWritingThread's queue}
FWritingThread.Enqueue(Text);
finally
FLock.Leave;
end;
end;
{ TWritingThread }
procedure TWritingThread.Execute;
begin
while not Terminated do
begin
{GetMessage() - wait for a message informing about a new value in the queue}
{pop a value from the queue}
{write the value}
{call OnWrite method}
end;
end;
Quando você olha para o procedimento Close (), você vai ver que ele entra na seção crítica, termina o fio de escrita e espera que ela termine. Devido ao fato de que o fio de escrita pode enfileirar um novo valor a ser escrito quando ele chama o método OnWrite, ele vai tentar entrar na mesma seção crítica ao chamar o procedimento Write () da classe TAsyncSerialPort.
E aqui temos um impasse. O segmento que chama o método Close () entrou na seção crítica e aguarda o segmento de escrita para ser fechada, enquanto, ao mesmo tempo em que aguarda thread para a seção crítica para ser liberado.
Eu estive pensando por muito tempo e eu não conseguir encontrar uma solução para esse problema. A coisa é que eu gostaria de ter certeza de que nenhuma leitura / escrita fio está vivo quando o Close () método é para a esquerda, o que significa que eu não posso simplesmente definir o sinalizador Terminado desses tópicos e licença.
Como posso resolver o problema? Talvez eu devesse mudar minha abordagem para lidar com porta serial de forma assíncrona?
Obrigado por seu conselho com antecedência.
Mariusz.
--------- ---------- EDIT
Que tal solução tal?
procedure TAsyncSerialPort.Close;
var
lThread: TThread;
begin
FLock.Enter;
try
lThread := FWritingThread;
if Assigned(lThread) then
begin
lThread.Terminate;
if lThread.Suspended then
lThread.Resume;
FWritingThread := nil;
end;
if FCommPort <> 0 then
begin
CloseHandle(FCommPort);
FCommPort := 0;
end;
finally
FLock.Leave;
end;
if Assigned(lThread) then
begin
lThread.WaitFor;
lThread.Free;
end;
end;
Se o meu pensamento está correto, isso deve eliminar o problema de impasse. Infelizmente, no entanto, eu fechar o identificador de porta de comunicação antes que o segmento de escrita está fechado. Isto significa que quando ele chama qualquer método que leva a alça porta de comunicação como um de seus argumentos (por exemplo, escrever, ler, WaitCommEvent) uma exceção deve ser criado em um dos threads. Posso ter a certeza que se eu pegar essa exceção nesse segmento não afetará o trabalho de toda a aplicação? Esta pergunta pode parecer estúpido, mas eu acho que algumas exceções podem fazer com que o OS para fechar o aplicativo que causou isso, certo? Eu tenho que preocupar com isso neste caso?
Solução
Sim, você provavelmente deve reconsiderar a sua abordagem. Operações assíncronas estão disponíveis exatamente para eliminar a necessidade de fios. Se você usar threads, em seguida, usar chamadas síncronas (bloqueio). Se você usar operações assíncronas, então lidar com tudo em um fio -. Não necessariamente o segmento principal, mas não faz sentido IMO para fazer o envio e recebimento em diferentes segmentos
Existem maneiras de curso em torno do seu problema de sincronização, mas eu prefiro mudar o projeto.
Outras dicas
Você pode tomar o bloqueio fora do Fechar. No momento em que ele retorna a partir do waitFor, o corpo discussão notado que tenha sido encerrado, concluído o último ciclo, e terminou.
Se você não se sentir feliz fazendo isso, então você pode mover-se ajustar o bloqueio pouco antes da FreeAndNil. Este explicitamente permite que os mecanismos de fio de desligamento trabalhar antes de aplicar o bloqueio (por isso não terá que competir com qualquer coisa para o bloqueio)
EDIT:
(1) Se você também quiser fechar as comms lidar fazê-lo após o loop em Executar, ou em processo de destruição do segmento.
(2) Lamentamos, mas a sua solução editado é uma terrível bagunça. Terminar e waitFor vai fazer tudo o que precisa, perfeitamente com segurança.
O principal problema parece ser que você colocar todo o conteúdo do Close em uma seção crítica. Tenho quase certeza (mas você terá que verificar os docs) que TThread.Terminate e TThread.WaitFor são seguros para chamar de fora da seção. Ao puxar essa parte fora da seção crítica você vai resolver o impasse.