Running Legacy, Non-Reentrant Code on a Background Thread in .NET
I'm in needed of a threa开发者_运维技巧d 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.
I assume you want this because you don't want any of those 'jobs' to run at the same time, that looks like a valid requirement. But it is slightly different from 'all on the same single thread'.
The (Fx4) Task parallel library has a 'ContinuesWith' construction that solves a similar problem but with a different interface.
So I think you have to (continue to) roll your own. Some criticism:
You check for m_Queue.Count
outside a lock, that is probably safe because you only have 1 consumer but I would fold it into the lock.
Also you could replace the AutoResetEvent with Monitor.Wait() and Monitor.Pulse(). This is 'lighter' (all managed code) and it plays nice with lock (== Monitor.Enter/.Exit).
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.
- Don't use Hungarian notation (m_*). It's not necessary.
- You need to lock when accessing
m_Terminate
. Or at the very least it needs to be volatile. Why are you usingFixedAction<object>
and then passing null as a parameter? Can't you just use ThreadStart if you don't want a parameter?- WorkItem should be immutable. Use readonly members or a property with a private setter.
- 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).
- 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.
- You should validate that the parameter
callback
toQueueUserWorkItem
is not null to avoidNullReferenceException
in the loop inThreadMain
(fail fast). - You should throw an
ObjectDisposedException
ifQueueUserWorkItem
is called afterDispose
has already been called.
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.
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.
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 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.
精彩评论