异步线程及同步控制
在日常开发中,很多耗时的工作都是使用异步线程的完成,特别的在C/S模式中更显常见,但很多时候有涉及到资源的独占,对资源的锁定有多种做法,很多时候引起死锁及锁定失效,这些有MSDN中都有相当多的介绍,我就不再献丑了。这里主要是在一个做考勤机的模块中,对MSDN的制造都和使用都进行同步控制的改进及应用。
参考MSDN的主题:《如何:对制造者线程和使用者线程进行同步(C# 编程指南)》
using System.Threading;
using System.Collections;
using System.Collections.Generic;
public class SyncEvents
{
public SyncEvents()
{
_newItemEvent = new AutoResetEvent(false);
_exitThreadEvent = new ManualResetEvent(false);
_eventArray = new WaitHandle[2];
_eventArray[0] = _newItemEvent;
_eventArray[1] = _exitThreadEvent;
}
public EventWaitHandle ExitThreadEvent
{
get { return _exitThreadEvent; }
}
public EventWaitHandle NewItemEvent
{
get { return _newItemEvent; }
}
public WaitHandle[] EventArray
{
get { return _eventArray; }
}
private EventWaitHandle _newItemEvent;
private EventWaitHandle _exitThreadEvent;
private WaitHandle[] _eventArray;
}
public class Producer
{
public Producer(Queue<int> q, SyncEvents e)
{
_queue = q;
_syncEvents = e;
}
// Producer.ThreadRun
public void ThreadRun()
{
int count = 0;
Random r = new Random();
while (!_syncEvents.ExitThreadEvent.WaitOne(0, false))
{
lock (((ICollection)_queue).SyncRoot)
{
while (_queue.Count < 20)
{
_queue.Enqueue(r.Next(0,100));
_syncEvents.NewItemEvent.Set();
count++;
}
}
}
Console.WriteLine("Producer thread: produced {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class Consumer
{
public Consumer(Queue<int> q, SyncEvents e)
{
_queue = q;
_syncEvents = e;
}
// Consumer.ThreadRun
public void ThreadRun()
{
int count = 0;
while (WaitHandle.WaitAny(_syncEvents.EventArray) != 1)
{
lock (((ICollection)_queue).SyncRoot)
{
int item = _queue.Dequeue();
}
count++;
}
Console.WriteLine("Consumer Thread: consumed {0} items", count);
}
private Queue<int> _queue;
private SyncEvents _syncEvents;
}
public class ThreadSyncSample
{
private static void ShowQueueContents(Queue<int> q)
{
lock (((ICollection)q).SyncRoot)
{
foreach (int item in q)
{
Console.Write("{0} ", item);
}
}
Console.WriteLine();
}
static void Main()
{
Queue<int> queue = new Queue<int>();
SyncEvents syncEvents = new SyncEvents();
Console.WriteLine("Configuring worker threads...");
Producer producer = new Producer(queue, syncEvents);
Consumer consumer = new Consumer(queue, syncEvents);
Thread producerThread = new Thread(producer.ThreadRun);
Thread consumerThread = new Thread(consumer.ThreadRun);
Console.WriteLine("Launching producer and consumer threads...");
producerThread.Start();
consumerThread.Start();
for (int i=0; i<4; i++)
{
Thread.Sleep(2500);
ShowQueueContents(queue);
}
Console.WriteLine("Signaling threads to terminate...");
syncEvents.ExitThreadEvent.Set();
producerThread.Join();
consumerThread.Join();
}
}
考勤机业务描述:通过一个Windows Services来同时监听多台考勤机。由于考勤机底层SDK的问题,各种操作不能同时操作,特别是不同的考勤机的不同的操作也会引起内存的访问失败,(可能是考勤机底层SDK没有考虑到多台操作,对部分同用的内存处理不好引起的,这不是我们应该公司的问题,厂家也不可能一下子修复这些问题,只有我们想办法了),因些想到用一个线程去处理多个考勤机的动作,由于Windows Services没有消息泵,在些用一个循环线程来维持底层的状态(底层的SDK只能做由创建的线程来调用其方法),对于不同考勤机所要处理的动作,全部都放在一个队列Queue中,由循环线程来逐个执行。此做法效率虽然低了点,可以对于迸发不是很多的业务,却足够了,又能很好解决项目进度的问题。
在MSDN的示例中,消费者是可以使用新线程却工作,所以
while (WaitHandle.WaitAny(_syncEvents.EventArray) != 1)
{
lock (((ICollection)_queue).SyncRoot)
{
int item = _queue.Dequeue();
}
count++;
}
代码int item = _queue.Dequeue();是没有问题的,好像在考勤机中,只能一个线程操作,而且_queue.Dequeue后的Do something耗时比较大,(不能新开线程),该做法就完成不可行了,如果耗时过长,就会对制造都进行了阻塞,不合要求;而且在SyncEvents中使用AutoResetEvent,当WaitHandle.WaitAny(_syncEvents.EventArray) 收到信号时就会自动恢复,制造者一往Queue中加入项,又马上有新的信号,起不了同步的作用,因此,把AutoResetEvent改为ManualResetEvent,当Do Something完成后,而与队列没有项时,才重置制造者的信号
/// 同步事件控制类。参考MSDN的对制造者线程和使用者线程进行同步(C# 编程指南)
/// </summary>
/// <typeparam name="T">T 只能为 AutoResetEvent 或 ManualResetEvent</typeparam>
public class SyncEvents<T> where T : WaitHandle
{
public SyncEvents()
{
//_newItemEvent = new AutoResetEvent(false);
if (typeof(T) == typeof(AutoResetEvent))
_newItemEvent = new AutoResetEvent(false);
else if (typeof(T) == typeof(ManualResetEvent))
_newItemEvent = new ManualResetEvent(false);
_exitThreadEvent = new ManualResetEvent(false);
_eventArray = new WaitHandle[2];
_eventArray[0] = _newItemEvent;
_eventArray[1] = _exitThreadEvent;
}
/// <summary>
/// 退出线程事件。为ManualResetEvent类。
/// </summary>
public EventWaitHandle ExitThreadEvent
{
get { return _exitThreadEvent; }
}
/// <summary>
/// 新项的消息。为AutoResetEvent类
/// </summary>
public EventWaitHandle NewItemEvent
{
get { return _newItemEvent; }
}
/// <summary>
/// 事件数组。EventArray[0]为NewItemEvent,NewItemEvent[1]ExitThreadEvent
/// </summary>
public WaitHandle[] EventArray
{
get { return _eventArray; }
}
private EventWaitHandle _newItemEvent;
private EventWaitHandle _exitThreadEvent;
private WaitHandle[] _eventArray;
}
初始化相关实体。
_queue = new Queue<EventsActionEntry>();
_syncEvents = new SyncEvents<ManualResetEvent>();
m_WorkThread = new Thread(new ThreadStart(CreateEnrollListener));
m_WorkThread.Start();
在实体应用中,制造都不用考虑如果退出,只要是对队列里加进项,并发出信号就可以了
Console.WriteLine("Receive a message wait for process...");
lock (((ICollection)_queue).SyncRoot)
{
EventsActionEntry entry = new EventsActionEntry { MachineNumber = e.ReceiveData.MachineID, InvokeActionType = ActionType.LogTime}; //生成Action的类型
_queue.Enqueue(entry);
_syncEvents.NewItemEvent.Set();
}
这个MSDN的做法没有多在变动。
EventsActionEntry 实体的设计是以为使用者执行而设计,在很多项目中有一个这样的现象,当第一个操作很耗时,而队列的项不断增加,而中间的项又没有用了,只需要执行最后一个项,我们可以在lock内对entry的标识进行更改,而在使用者取出entry时,对标识进行判断,这样就可以放弃中间的所有Action的执行。
在使用者的执行方法m_WorkThread = new Thread(new ThreadStart(CreateEnrollListener)); CreateEnrollListener中,我是这样使用的
while (WaitHandle.WaitAny(_syncEvents.EventArray) != 1)
{
EventsActionEntry actionItem = null;
lock (((ICollection)_queue).SyncRoot)
{
if (_queue.Count > 0)
actionItem = _queue.Dequeue();
}
//处理事件
ProcessEventAction(actionItem);
lock (((ICollection)_queue).SyncRoot)
{
if (_queue.Count == 0)
{
_syncEvents.NewItemEvent.Reset();
}
}
}
ProcessEventAction(actionItem); 在第一个lock的外面,这样,就不会阻塞制造者往队列里加项。这时,NewItemEvent处理非阻塞状态,因此可以一直循环执行完队列的所有Action,到所有项完成后,第二个lock对NewItemEvent重置为阻塞,使用者因此又在等待制造者加入项,只要制造者一加入新项,又有信号给使用者,又重新开始工作,周而复始,解决了不同线程的同步控制了。在本单位的很多项目中,为了改进用户的体验,在很多地方使用些做法(更多的是放弃中间的所有操作,只要应用在用户对一个操作连续点击,到停止时其实只显示最后一个操作就行)。
有些表述可能有误和不清晰,希望大家能得到更好的应用,对能改进的地方,多多发言,并提出意见。