多线程及线程池

  多线程的最佳示例就是采集器。本来打算以采集器为例,由于时间问题做了简易的变更,其实就是执行方法不在执行采集。

  采集器一般来说有两种,差别在于任务获取的位置。第一种轮询由采集线程来获取,第二种轮询由调度器来获取。不管哪一种,工作池的构造基本是相同的。

  先简述一下工作池的构造再来详细介绍两种采集器的差异。

     工作池:由任务队列及提供访问的方法。比如从工作池中取任务,向工作池中添加任务。注意由于是多线程访问,应该在对任务队列访问的时候加锁,否则会出现一个任务被多个线程获取导致多次执行的现象,这样不仅影响效率,而且对结果也有影响。

     第一种,由采集线程来获取任务的采集器。

     工作池代码如下:

    public class WorkPool
    {
        private static List<string> Worklist = new List<string>();
        private static object obj = new object();
        public static int WorkCount
        {
            get { return Worklist.Count; }
        }

        public static string GetFirstWork()
        {
            lock (obj)
            {
                if (Worklist.Count > 0)
                {
                    string s = Worklist[0];
                    DeleteWork();
                    return s;
                }
                return "";
            }
        }

        public void ClearWorkPool()
        {
            if (Worklist != null && Worklist.Count > 0)
            {
                lock (obj)
                {
                    Worklist.Clear();
                }
            }
        }

        private static void DeleteWork()
        {
            if (Worklist.Count > 0)
            {
                Worklist.RemoveAt(0);
            }
        }

        public static void AddWork(string work)
        {
            if (Worklist != null && !string.IsNullOrEmpty(work))
            {
                Worklist.Add(work);
            }
            if (string.IsNullOrEmpty(work))
            {
                throw new Exception("work is empty!");
            }
            if (Worklist == null)
            {
                throw new Exception("工作池未实例化");
            }
        }

        public static void AddWorkRange(IList<string> wList)
        {
            if (Worklist!=null)
            {
                lock (obj)
                {
                    Worklist.AddRange(wList);
                }
            }
        }
    }
WorkPool

   采集线程中包括后台线程,计时器两个部分。调度器中则是只有采集线程集合。


  后台线程需要向外界提供状态访问,开关等入口。

  后台线程BackgroundWorker在采集线程初始化的时候添加dowork事件,切记此时的dowork事件是执行一次的,不要将轮询的事情交给采集线程。dowork事件一定要遵循单一原则,只做一件事。

  计时器轮询bgw的状态,如果空闲,则从工作池中获取新任务。

 
    public class Reaper
    {
        Timer timer = new Timer(1000.0);

        BackgroundWorker bgw = new BackgroundWorker();

        public string ReaperName { get; private set; }

        public bool IsBusy
        {
            get { return bgw.IsBusy; }
        }

        public Reaper(int index)
        {
            ReaperName = "Reaper" + index;

            timer.Elapsed += timer_Elapsed;
            bgw.DoWork += bgw_DoWork;
        }

        void bgw_DoWork(object sender, DoWorkEventArgs e)
        {
            string work = e.Argument.ToString();
            if (!string.IsNullOrEmpty(work))
            {
                Console.WriteLine(string.Format("线程名:{0}  关键词:{1}  时间:{2} {3}", ReaperName, work, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), DateTime.Now.Millisecond));
            }

        }

        void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (!bgw.IsBusy && WorkPool.WorkCount > 0)
            {
                string work = WorkPool.GetFirstWork();
                bgw.RunWorkerAsync(work);
            }
        }

        public void Start()
        {
            timer.Start();
        }

        public void Stop()
        {
            timer.Stop();
        }
    }
Reaper

  调度器则是充当一个开关的作用。根据参数或者是配置实例化采集线程,调度器打开时循环将各个采集线程打开。各采集线程在计时器工作下轮询工作池执行dowork。

    public class ReaperMaster
    {
        private List<Reaper> list = new List<Reaper>();

        public int ReaperCount { get; private set; }

        public ReaperMaster(int count)
        {
            ReaperCount = count;
            for (int i = 0; i < count; i++)
            {
                list.Add(new Reaper(i));
            }
        }

        public void Start()
        {
            foreach (Reaper reaper in list)
            {
                if (!reaper.IsBusy)
                {
                    reaper.Start();
                }
            }
        }

        public void Stop()
        {
            foreach (Reaper reaper in list)
            {
                reaper.Stop();
            }
        }
    }
ReaperMaster
        for (int i = 0; i < 100; i++)
            {
                WorkPool.AddWork((i + 51).ToString());
            }

            ReaperMaster master=new ReaperMaster(6);
            master.Start();

            Console.WriteLine("End");
            Console.ReadLine();    
调用

  第二种,由调度器来获取任务的采集器。

  工作池略有差异(没有锁),代码如下:

 public class WorkPool
    {
        private static List<string> Worklist = new List<string>();

        public static int WorkCount
        {
            get { return Worklist.Count; }
        }

        public static string GetFirstWork()
        {

            if (Worklist.Count > 0)
            {
                string s = Worklist[0];
                DeleteWork();
                return s;
            }
            return "";

        }

        public void ClearWorkPool()
        {
            if (Worklist != null && Worklist.Count > 0)
            {
                Worklist.Clear();
            }
        }

        private static void DeleteWork()
        {
            if (Worklist.Count > 0)
            {
                Worklist.RemoveAt(0);
            }
        }

        public static void AddWork(string work)
        {
            if (Worklist != null && !string.IsNullOrEmpty(work))
            {
                Worklist.Add(work);
            }
            if (string.IsNullOrEmpty(work))
            {
                throw new Exception("work is empty!");
            }
            if (Worklist == null)
            {
                throw new Exception("工作池未实例化");
            }
        }

        public static void AddWorkRange(IList<string> wList)
        {
            if (Worklist != null)
            {
                Worklist.AddRange(wList);

            }
        }
    }
WorkPool

  采集线程中包括后台线程,任务访问入口(用于采集器指定的任务)。调度器中采集线程集合和计时器两个对象。

  bgw与第一个的差别不大。

  任务的访问入口使用属性就可以了。

    public class Reaper
    {

        BackgroundWorker bgw = new BackgroundWorker();

        public string ReaperName { get; private set; }

        public bool IsBusy
        {
            get { return bgw.IsBusy; }
        }

        public Reaper(int index)
        {
            ReaperName = "Reaper" + index;


            bgw.DoWork += bgw_DoWork;
        }

        void bgw_DoWork(object sender, DoWorkEventArgs e)
        {
            string work = e.Argument.ToString();
            if (!string.IsNullOrEmpty(work))
            {
                Console.WriteLine(string.Format("线程名:{0}  关键词:{1}  时间:{2} {3}", ReaperName, work, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), DateTime.Now.Millisecond));
            }
        }


        public void Start(string work)
        {
            bgw.RunWorkerAsync(work);
        }

        public void Stop()
        {
            bgw.CancelAsync();
        }
    }
Reaper

  调度器中的计时器轮询采集线程集合中的采集线程的状态,如果线程空闲则获取新任务分配给此线程。调度器通过开关计时器来控制采集线程。

    public class ReaperMaster
    {
        private List<Reaper> list = new List<Reaper>();

        private Timer timer = new Timer(1000.0);

        public int ReaperCount { get; private set; }

        public ReaperMaster(int count)
        {
            ReaperCount = count;
            for (int i = 0; i < count; i++)
            {
                list.Add(new Reaper(i));
            }
            timer.Elapsed += timer_Elapsed;
        }

        void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            foreach (Reaper reaper in list)
            {
                if (!reaper.IsBusy && WorkPool.WorkCount > 0)
                {
                    string work = WorkPool.GetFirstWork();
                    reaper.Start(work);
                }
            }
        }

        public void Start()
        {
            timer.Start();
        }

        public void Stop()
        {
            timer.Stop();
        }
    }
ReaperMaster

  调用方法同第一种。

  两种采集器还有个明显的差别,第一种采集线程运行后,由于有计时器的存在,线程可以持续运行。而第二种采集线程需要调度器不断的从外部来唤醒执行。两种方法各有优劣,第一种线程开启后可以自己执行,但是关闭的时候需要调度器循环访问挂起关闭。第二种只需要关闭调度器的计时器即可将所有的采集线程关闭挂起,但是需要调度器不断的进行唤醒线程。第一种控制力不强,这样调度器的线程则不会有太多的负担,第二种集中控制无疑给调度器线程增加了负担。由于第二种采集器只有调度器一个线程访问工作池,故工作池可以不必lock。
  采集器可以由多个工作池多个调度器组合成更加复杂的采集器。比如基于搜索引擎的采集器。一个工作池和调度器负责关键词,一个工作池和调度器负责采集搜索引擎的搜索结果,再有一个工作池和调度器对搜索引擎搜索结果进行访问采集,最后将采集结果保存。此时至少有三个线程池和调度器进行拼接,建议采用多个线程池,而不是合并线程池增加采集线程的功能。诚然在一个线程中足以实现获取关键词、获取搜索引擎搜索结果、访问搜索结果获取最终原始采集数据一整套流程,但是这样会使开发调试难度成倍增加。
多线程的调试难度比较大,可以尝试单元测试。而且多线程的开发中单一原则的使用会使代码更加易懂,另外注意异常日志的记录。

----------------------------------------------------无量的分割线---------------------------------------------------------------

一年多未更新博客,由于技术进步比较缓慢,工作生活进入慢速期,当持之以恒,共勉之。

原文地址:https://www.cnblogs.com/cnDqf/p/6404861.html