views:

910

answers:

3

I was playing with a project of mine today and found an interesting little snippet, given the following pattern, you can safely cleanup a thread, even if it's forced to close early. My project is a network server where it spawns a new thread for each client. I've found this useful for early termination from the remote side, but also from the local side (I can just call .Abort() from inside my processing code).

Are there any problems you can see with this, or any suggestions you'd make to anyone looking at a similar approach?

Test case follows:


using System;
using System.Threading;

class Program
{
 static Thread t1 = new Thread(thread1);
 static Thread t2 = new Thread(thread2);

 public static void Main(string[] args)
 {
  t1.Start();
  t2.Start();
  t1.Join();
 }

 public static void thread1() {
  try {
   // Do our work here, for this test just look busy.
   while(true) {
    Thread.Sleep(100);
   }
  } finally {
   Console.WriteLine("We're exiting thread1 cleanly.\n");
   // Do any cleanup that might be needed here.
  }
 }

 public static void thread2() {
  Thread.Sleep(500);
  t1.Abort();
 }
}

For reference, without the try/finally block, the thread just dies as one would expect.

+3  A: 

Whether or not this will "safely" cleanup a thread cannot be discerned from a general code sample unfortunately. It's highly dependent upon the actual code that is executed within the thread. There are multiple issues you must consider. Each represents a potential bug in the code.

  1. If the thread is currently in native code, it will not immediately respect the Thread.Abort call. It will do all of the work it wants to do in native code and will not throw until the code returns back to managed. Until this happens thread2 will hang.
  2. Any native resources that are not freed in a finally block will be leaked in this scenario. All native resources should be freed in a finally block but not all code does this and it's an issue to consider.
  3. Any locks that are not freed in a finally block will remain in a lock'd state and can lead to future dead locks.

There are other issues which are slipping my mind at the moment. But hopefully this will give you some guidance with your application.

JaredPar
How do you manually free locks? I've only ever seen lock(foo) {} blocks, if they are exited via an exception or similar, are they still unlocked? Ditto for using(foo) {} blocks.
Matthew Scharley
@monoxide, under the hood lock() is simply a try/finally around Monitor.Enter and Monitor.Exit. There are occasions where people validly call them outside of a try/finally. Rare but they do happen
JaredPar
For context - the biggest reason to use Monitor.Exit is when coupled with Monitor.TryEnter - i.e. using a timeout when obtaining the lock. There is no abbreviated syntax for this.
Marc Gravell
@Marc the only other valid use I've seen is when an object exists purely for the purpose of holding a lock. Typically though they implement IDisposable and Exit the lock in the Dispose method. being dipsosable, they should be in a try finally though ...
JaredPar
+12  A: 

Aborting another thread at all is just a bad idea unless the whole application is coming down. It's too easy to leave your program in an unknown state. Aborting your own thread is occasionally useful - ASP.NET throws a ThreadAbortException if you want to prematurely end the response, for example - but it's not a terribly nice design.

Safe clean-up of a thread should be mutual - there should be some shared flag requesting that the thread shuts down. The thread should check that flag periodically and quit appropriately.

Jon Skeet
The thread is representing a network connection. It is only being Abort()'d if the remote side closes the connection prematurely, or if we want to close our side.
Matthew Scharley
The main reason for doing it this way is because I seemed to be getting NullReferenceExceptions out of the blue when I had random remote disconnects, which was fixed by this approach and cleaning up the network connections properly in the finally block.
Matthew Scharley
You should certainly clean up your connections properly in a finally block, but you shouldn't be using Thread.Abort.
Jon Skeet
Also, I think I missed this in your answer: Contrary to the example given above which uses two theads for the sake of brevity, I'm only ever aborting the thread from itself. I don't know if this changes your advice for my situation or not, given what you said, but there you have it.
Matthew Scharley
@monoxide: Okay, if you're aborting the thread from itself then it's not *as* bad. I think it's still better to try to just let it terminate normally though. Why can't you just return up the stack?
Jon Skeet
I was using a StreamReader to read off the network and it was mysteriously throwing a NullReferenceError from ReadLine() if the connection disconnected.
Matthew Scharley
My solution: I have a stream wrapping the NetworkStream already (though this happens with the straight NetworkStream too), so I added a Disconnect event and added a handler that aborts the thread which throws it into the finally clause which cleans things up and makes sure everything is sane.
Matthew Scharley
It sounds very odd that it was throwing a NullReferenceException, but I don't think aborting the whole thread is an elegant solution. I would look closer at why it's throwing a NullReferenceException. I'd expect it to throw an IOException.
Jon Skeet
Well, that was fun. I figure "Well, now's as good a time as any to learn mdb." so I fire it up and every time I try to run the app, I get an mdb InternalError thrown. Joy. As for the IOException, yes, that'd be one that I'd expect too. But like so many things, what we want and what we get...
Matthew Scharley
Odd. If you can reproduce it in a short but complete program I'll take a look. I don't suppose it's as simple as StreamReader returning null, which it does at the end of the stream, is it?
Jon Skeet
Gah, you're right, I'm doing stuff like myStreamReader.ReadLine().Trim() ... Uggh, I feel stupid now. Now, how to handle the null from the StreamReader? I suppose you still advise against aborting the thread despite having the infrastructure there for it already?
Matthew Scharley
Yes, I advise against aborting the thread. The null just means there's no more data in the stream. Return appropriately from your method.
Jon Skeet
A: 

It is generally not a good idea to abort threads. What you can do is poll for a stopRequested flag which can be set from other threads. Below is a sample WorkerThread class for your reference. For more information on how to use it, please refer to http://devpinoy.org/blogs/jakelite/archive/2008/12/20/threading-patterns-the-worker-thread-pattern.aspx

public abstract class WorkerThreadBase : IDisposable
{
    private Thread _workerThread;
    protected internal ManualResetEvent _stopping;
    protected internal ManualResetEvent _stopped;
    private bool _disposed;
    private bool _disposing;
    private string _name;

    protected WorkerThreadBase()
        : this(null, ThreadPriority.Normal)
    {
    }

    protected WorkerThreadBase(string name)
        : this(name, ThreadPriority.Normal)
    {
    }

    protected WorkerThreadBase(string name,
        ThreadPriority priority)
        : this(name, priority, false)
    {
    }

    protected WorkerThreadBase(string name,
        ThreadPriority priority,
        bool isBackground)
    {
        _disposing = false;
        _disposed = false;
        _stopping = new ManualResetEvent(false);
        _stopped = new ManualResetEvent(false);

        _name = name == null ? GetType().Name : name; ;
        _workerThread = new Thread(threadProc);
        _workerThread.Name = _name;
        _workerThread.Priority = priority;
        _workerThread.IsBackground = isBackground;
    }

    protected bool StopRequested
    {
        get { return _stopping.WaitOne(1, true); }
    }

    protected bool Disposing
    {
        get { return _disposing; }
    }

    protected bool Disposed
    {
        get { return _disposed; }
    }

    public string Name
    {
        get { return _name; }            
    }   

    public void Start()
    {
        ThrowIfDisposedOrDisposing();
        _workerThread.Start();
    }

    public void Stop()
    {
        ThrowIfDisposedOrDisposing();
        _stopping.Set();
        _stopped.WaitOne();
    }

    public void WaitForExit()
    {
        ThrowIfDisposedOrDisposing();            
        _stopped.WaitOne();
    }

    #region IDisposable Members

    public void Dispose()
    {
        dispose(true);
    }

    #endregion

    public static void WaitAll(params WorkerThreadBase[] threads)
    { 
        WaitHandle.WaitAll(
            Array.ConvertAll<WorkerThreadBase, WaitHandle>(
                threads,
                delegate(WorkerThreadBase workerThread)
                { return workerThread._stopped; }));
    }

    public static void WaitAny(params WorkerThreadBase[] threads)
    {
        WaitHandle.WaitAny(
            Array.ConvertAll<WorkerThreadBase, WaitHandle>(
                threads,
                delegate(WorkerThreadBase workerThread)
                { return workerThread._stopped; }));
    }

    protected virtual void Dispose(bool disposing)
    {
        //stop the thread;
        Stop();

        //make sure the thread joins the main thread
        _workerThread.Join(1000);

        //dispose of the waithandles
        DisposeWaitHandle(_stopping);
        DisposeWaitHandle(_stopped);
    }

    protected void ThrowIfDisposedOrDisposing()
    {
        if (_disposing)
        {
            throw new InvalidOperationException(
                Properties.Resources.ERROR_OBJECT_DISPOSING);
        }

        if (_disposed)
        {
            throw new ObjectDisposedException(
                GetType().Name,
                Properties.Resources.ERROR_OBJECT_DISPOSED);
        }
    }

    protected void DisposeWaitHandle(WaitHandle waitHandle)
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
    }

    protected abstract void Work();

    private void dispose(bool disposing)
    {
        //do nothing if disposed more than once
        if (_disposed)
        {
            return;
        }

        if (disposing)
        {
            _disposing = disposing;

            Dispose(disposing);

            _disposing = false;
            //mark as disposed
            _disposed = true;
        }
    }

    private void threadProc()
    {
        Work();
        _stopped.Set();
    }        
}
jake.stateresa