views:

222

answers:

5

I'm in needed of a thread worker in my .NET application - .NET has several classes such as thread pools etc., but I can't find anything that runs on a single thread, which is a requirement in my case.

So I've had a go a writing one myself, but these things are notoriously tricky, I'm sure I've got something wrong. Can anyone improve it or point me in the direction of somthing similar that's already been written?

public class ThreadWorker : IDisposable
{
    public ThreadWorker()
    {
        m_Thread = new Thread(ThreadMain);
        m_Thread.IsBackground = true;
        m_Thread.Name = "Worker Thread";
        m_Thread.Start();
    }

    public void Dispose()
    {
        if (!m_Terminate)
        {
            m_Terminate = true;
            m_Event.Set();
            m_Thread.Join();
        }
    }

    public void QueueUserWorkItem(Action<object> callback, object data)
    {
        lock (m_Queue) m_Queue.Enqueue(new WorkItem(callback, data));
        m_Event.Set();
    }

    void ThreadMain()
    {
        while (!m_Terminate)
        {
            if (m_Queue.Count > 0)
            {
                WorkItem workItem;
                lock (m_Queue) workItem = m_Queue.Dequeue();
                workItem.Callback(workItem.Data);
            }
            else
            {
                m_Event.WaitOne();
            }
        }
    }

    class WorkItem
    {
        public WorkItem(Action<object> callback, object data)
        {
            Callback = callback;
            Data = data;
        }

        public Action<object> Callback;
        public object Data;
    }

    AutoResetEvent m_Event = new AutoResetEvent(false);
    Queue<WorkItem> m_Queue = new Queue<WorkItem>();
    Thread m_Thread;
    bool m_Terminate;
}

C'mon, Tear it apart!

PLEASE STOP ASKING WHETHER I NEED THIS: Yes I do - I have legacy C code that isn't thread-safe, so I need to synchronise all of my calls to it on a background thread.

EDIT: Last minute change to my code was obviously incorrect ,fixed it.

A: 

I think you're reinventing the wheel. The supplied System.Threading.ThreadPool provides this functionality. I would just use that and get back to writing actual application logic.

Bryan Ross
Not really answer material, especially given the fact that it's asking a lot of questions.
Martinho Fernandes
Edited to be more "answer-like"
Bryan Ross
Updated question to clarify - but yes there is a good reason: my application will crash if I use a ThreadPool.
Groky
ThreadPool does not provide reliable functionality for a *single* thread for processing a queue of requests.
Martinho Fernandes
Why does your app crash with a threadpool? Do the jobs touch the UI? If so, consider using the appropriate marshalling techniques (`Control.Invoke` or `SynchronizationContext`) to properly execute tasks on the correct thread.
Greg D
No, the threads are calling legacy C code that isn't thread safe. I really wish people would stop asking my if I need something like - YES I DO :)
Groky
This just seems all wrong. If the legacy C code isn't thread safe, then why are you using threads with it at all?
Bryan Ross
Sorry, Groky. Questions like this raise red flags because they usually indicate somebody hasn't asked the right question. If you have an unusual requirement, it's generally a good idea to explain why in the question to curb such suggestions. From my perspective, e.g., I see someone who doesn't appear to be familiar with the concepts of a work or message queue (pretty fundamental real-world concepts), and so I must naturally infer the more common root cause of the question.
Greg D
I'm using threads because I want the legacy C code to run on a background thread. When I say thread safe I guess I should be using the term re-entrant. Also, I would be interested to hear why I don't appear to be familiar with the concepts of a work or message queue?
Groky
@Groky: I believe your use case, I was just trying to explain why I was skeptical as to its necessity until you explained it. :) You don't appear to be familiar with the concepts of a work or message queue because you called it a thread pool with one thread.
Greg D
+1  A: 

I'll assume that you actually have a good reason for wanting a thread pool that only has one thread. In that case, I only spot one major flaw: Why are you using Action<T> when you are always passing null? Just use Action, which takes no arguments.

JSBangs
I've fixed that - a last minute change to my code that I didn't spot.
Groky
+3  A: 

I'm going to ignore the question of whether or not you should be doing this and just give you feedback about your code. Most are style issues or suggestions to use best practices, but others are bugs that need to be fixed.

  1. Don't use Hungarian notation (m_*). It's not necessary.
  2. You need to lock when accessing m_Terminate. Or at the very least it needs to be volatile.
  3. Why are you using Action<object> and then passing null as a parameter? Can't you just use ThreadStart if you don't want a parameter? Fixed
  4. WorkItem should be immutable. Use readonly members or a property with a private setter.
  5. Missing error handling. If one of your work item actions throws an exception it will stop your entire thread pool working (assuming it doesn't take your entire application down).
  6. I have to agree with Greg's comment. This isn't a thread pool. It's a work queue. The name of the class should reflect that.
  7. You should validate that the parameter callback to QueueUserWorkItem is not null to avoid NullReferenceException in the loop in ThreadMain (fail fast).
  8. You should throw an ObjectDisposedException if QueueUserWorkItem is called after Dispose has already been called.
Mark Byers
m_* isn't hungarian notation. I've seen various people use _* and this.* to reference fields. You need some sort of prefix, what you use is a matter of style.
Groky
That was supposed to read "underscore" and this.
Groky
I agree with WorkItem being immutable. I don't think properties is really a *must* in a private nested data-holder class (even though I use them). Anyway +1 for helping instead of "Don't reinvent the wheel".
Martinho Fernandes
@Groky I don't use *any* prefix. It is a matter of style, but there's absolutely no need for a prefix.
Martinho Fernandes
Why do I need to lock when acessing m_Terminate? I thought reading/writing a bool would be atomic.
Groky
@Groky: because of weird optimization stuff, you can be reading stale values, i.e. the thread may never see the change in `m_terminate`.
Martinho Fernandes
Making it volatile solves that issue in this case though.
Martinho Fernandes
It may be atomic, but on a multicore machine each core could have it's own cache of the value and unless they are forced to re-read the value from main memory they won't necessarily be able to see each others updates.
Mark Byers
Thanks, and good catch with the error handling!
Groky
@Martinho Fernandes: Point taken about it being OK to use members instead of readonly properties given that it is a private class in a small scope.
Mark Byers
+1  A: 

Its in one of the comments, but the BackgroundWorker, is essentially a good implementation of what you want. Though you could start several of them, in your case just start one.

Secondarily, you actually can use ThreadPool for this, by changing your strategy:

ThreadPool.QueueUserWorkItem(myWorkerProcess);

Then:

void myWorkerProcess(object state) {
   WorkItem workItem;

   while (!m_Terminate) {
      m_Event.WaitOne(5000);
      if (m_Queue.Count > 0) {
         lock (m_Queue) workItem = m_Queue.Dequeue();
         // ... do your single threaded operation here ...
      }
   }
}

In this way you only ever have one background thread and it just loops waiting for you to do your thing.

GrayWizardx
BackgroundWorker requires the Windows message loop so it's only useful in Windows Forms projects. It's not necessarily going to help the OP.
Mark Byers
I do have a windows message loop, but I'd still have to queue my work items etc, so I don't see what I would gain anyway.
Groky
If you have only *one* thread handling your work, you have to queue the work anyways. I am not sure what queueing you are trying to avoid. If your work is non-threadsafe then you either queue, or use seperate processes through ProcessInfo. That is assuming that the processes themselves can manage not to clobber each other.
GrayWizardx
I'm not trying to avoid queuing - just saying that I don't think BackgroundWorker would buy me anything, as I need to queue in both scenarios.
Groky
+1  A: 

Hehe, I wrote something very similar recently.

With using the AutoResetEvent for synchronization, if you queue work items faster than they are processed, you end up with items in your queue, but no event triggering the processing of the items. I suggest using a semaphore so that you can maintain a count of how many times you've been signaled, such that you process all work items.

I don't think he has this problem. When one work item completes, it loops around the while loop and then takes the next item.
Mark Byers
Ah, you are correct, Mark.