异步线程及同步控制

    在日常开发中,很多耗时的工作都是使用异步线程的完成,特别的在C/S模式中更显常见,但很多时候有涉及到资源的独占,对资源的锁定有多种做法,很多时候引起死锁及锁定失效,这些有MSDN中都有相当多的介绍,我就不再献丑了。这里主要是在一个做考勤机的模块中,对MSDN的制造都和使用都进行同步控制的改进及应用。

    参考MSDN的主题:《如何:对制造者线程和使用者线程进行同步(C# 编程指南)》

MSDN Code
using System;
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(0false))
        {
            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完成后,而与队列没有项时,才重置制造者的信号

/// <summary>
    
/// 同步事件控制类。参考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重置为阻塞,使用者因此又在等待制造者加入项,只要制造者一加入新项,又有信号给使用者,又重新开始工作,周而复始,解决了不同线程的同步控制了。在本单位的很多项目中,为了改进用户的体验,在很多地方使用些做法(更多的是放弃中间的所有操作,只要应用在用户对一个操作连续点击,到停止时其实只显示最后一个操作就行)。

    有些表述可能有误和不清晰,希望大家能得到更好的应用,对能改进的地方,多多发言,并提出意见。

原文地址:https://www.cnblogs.com/Yjianyong/p/2632310.html