views:

317

answers:

2

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?

+6  A: 

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
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
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
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
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
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
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
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
+4  A: 

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