1

I created a class that opens a COM port and handles overlapped read and write operations. It contains two independent threads - one that reads and one that writes data. Both of them call OnXXX procedures (eg OnRead or OnWrite) notifying about finished read or write operation.

The following is a short example of the idea how the threads work:

  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;

When you look at the Close() procedure, you will see that it enters the critical section, terminates the writing thread and then waits for it to finish. Because of the fact that the writing thread can enqueue a new value to be written when it calls the OnWrite method, it will try to enter the same critical section when calling the Write() procedure of the TAsyncSerialPort class.

And here we've got a deadlock. The thread that called the Close() method entered the critical section and then waits for the writing thread to be closed, while at the same time that thread waits for the critical section to be freed.

I've been thinking for quite a long time and I didn't manage to find a solution to that problem. The thing is that I would like to be sure that no reading/writing thread is alive when the Close() method is left, which means that I cannot just set the Terminated flag of those threads and leave.

How can I solve the problem? Maybe I should change my approach to handling serial port asynchronously?

Thanks for your advice in advance.

Mariusz.

--------- EDIT ----------
How about such a solution?

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;

If my thinking is correct, this should eliminate the deadlock problem. Unfortunately, however, I close the comm port handle before the writing thread is closed. This means that when it calls any method that takes the comm port handle as one of its arguments (eg Write, Read, WaitCommEvent) an exception should be raised in that thread. Can I be sure that if I catch that exception in that thread it will not affect the work of the whole application? This question may sound stupid, but I think some exceptions may cause the OS to close the application that caused it, right? Do I have to worry about that in this case?

Mariusz Schimke
  • 3,185
  • 8
  • 45
  • 63

3 Answers3

6

Yes, you should probably reconsider your approach. Asynchronous operations are available exactly to eliminate the need for threads. If you use threads, then use synchronous (blocking) calls. If you use asynchronous operations, then handle everything in one thread - not necessarily the main thread, but it doesn't make sense IMO to do the sending and receiving in different threads.

There are of course ways around your synchronization problem, but I'd rather change the design.

mghie
  • 32,028
  • 6
  • 87
  • 129
  • I think you are absolutely right. There is a reason, however, why I chose this approach. If I chose the synchronous approach, the WaitCommEvent() method would block my thread until an event specified by the mask would occur. This means that I would not be able to close my thread if no event occured, right? Or maybe it would be enough to close the serial port handle and the function would return then (with exception)? – Mariusz Schimke Jul 05 '09 at 10:14
  • On the other hand, I create two independent threads for this allows me to wait for a device's answer. Let's say I communicate with a modem. I write eg the "AT" command and I expect the modem to respond with "OK" or "ERROR". I reset an event (TEvent), call the WaitForSingleObject() method which returns when the event is set by the reading thread's OnRead method. If there was only one reading/writing thread, I would have to block the main application's thread to do the same :(. Am I right? – Mariusz Schimke Jul 05 '09 at 10:20
  • If there was only one serial port to communicate on I would probably use asynchronous I/O in the main thread. If there were more than one I would probably use asynchronous I/O in one worker thread. Also, whether to use synchronous or asynchronous I/O in a background thread depends on whether you know beforehand when data is to be received. If you just receive answers to requests, why use WaitCommEvent() at all? Just read directly, after setting up sensible timeout values. If the other side could send at any time WaitCommEvent() makes more sense of course. – mghie Jul 05 '09 at 10:25
  • Also, you can always use WaitForMultipleObjects() with one additional event for shutting down the thread. See my answer to http://stackoverflow.com/questions/863135/why-does-readdirectorychangesw-omit-events for an example. – mghie Jul 05 '09 at 10:27
  • OK, so one problem is solved :). I was also thinking about the MsgWaitForMultipleObjects() which returns also when a message is sent to that thread. But how about WaitCommEvent()? How to make it return when no event occurs (non-overlapped approach)? Please tell me one thing - is it safe to close comm port handle when the function has not yet returned? To answer to your previous comment I would like to say that not always do I want to wait for device's answer to a command. Some devices can send at any time and I want my class to be universal, so I consider WaitCommEvent() neccessary. – Mariusz Schimke Jul 05 '09 at 10:50
  • I have not been able to make WaitCommEvent() return prematurely. Worse, calling SetCommMask() on a handle on which WaitCommEvent() waits in another thread has always deadlocked for me. – mghie Jul 05 '09 at 13:22
  • Thank you very much for the time you devoted to me. I decided to handle the serial port asynchronously, albeit using only one thread that will perform waiting for events and reading data from serial port. The writing thread is unnecessary for the write operation can be performed by the thread that calls the Write() method of my class, which writes data asynchronously, but returns only when it finishes (so in fact the write operation becomes synchronous). That's what satisfies me. I think the altered structure and all those pieces of advice will help me solve the original deadlock problem. Thx! – Mariusz Schimke Jul 05 '09 at 20:45
4

You can take the lock out of the Close. By the time it returns from the WaitFor, the thread body has noticed it has been terminated, completed the last loop, and ended.

If you don't feel happy doing this, then you could move setting the lock just before the FreeAndNil. This explicitly lets the thread shutdown mechanisms work before you apply the lock (so it won't have to compete with anything for the lock)

EDIT:

(1) If you also want to close the comms handle do it after the loop in the Execute, or in the thread's destructor.

(2) Sorry, but your edited solution is a terrible mess. Terminate and Waitfor will do everything you need, perfectly safely.

Bill99
  • 395
  • 2
  • 8
2

The main problem seems to be that you place the entire content of Close in a critical section. I'm almost sure (but you'll have to check the docs) that TThread.Terminate and TThread.WaitFor are safe to call from outside the section. By pulling that part outside the critical section you will solve the deadlock.

H H
  • 263,252
  • 30
  • 330
  • 514
  • Yes, but the thing is that I have to assign nil to my FWritingThread variable for if the Open() method is used after then, I have to know whether the writing thread has to be recreated. It's a good idea, however. Please look at my edited question. Thanks. – Mariusz Schimke Jul 05 '09 at 09:57
  • Your suggestion in the edit is worse if anything. Sorry. You have to consider carefully what needs locking, and that is only the COM port. Not the thread(s). Just take out the lock from the first version, it'll work. – H H Jul 05 '09 at 10:31
  • Yes, I understand. This is, however, only a fragment of the whole code and the FWritingThread variable can be assigned by some other thread at the same time and this is the reason why I put most the Close() method code in a critical section. Still, you are right - the code is awkward :). But let's leave this subject - I'm considering a change of the whole serial port handling approach. Nevertheless, thanks for your support. – Mariusz Schimke Jul 05 '09 at 11:02