多线程及线程池
多线程的最佳示例就是采集器。本来打算以采集器为例,由于时间问题做了简易的变更,其实就是执行方法不在执行采集。
采集器一般来说有两种,差别在于任务获取的位置。第一种轮询由采集线程来获取,第二种轮询由调度器来获取。不管哪一种,工作池的构造基本是相同的。
先简述一下工作池的构造再来详细介绍两种采集器的差异。
工作池:由任务队列及提供访问的方法。比如从工作池中取任务,向工作池中添加任务。注意由于是多线程访问,应该在对任务队列访问的时候加锁,否则会出现一个任务被多个线程获取导致多次执行的现象,这样不仅影响效率,而且对结果也有影响。
第一种,由采集线程来获取任务的采集器。
工作池代码如下:
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); } } } }
采集线程中包括后台线程,计时器两个部分。调度器中则是只有采集线程集合。
后台线程需要向外界提供状态访问,开关等入口。
后台线程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(); } }
调度器则是充当一个开关的作用。根据参数或者是配置实例化采集线程,调度器打开时循环将各个采集线程打开。各采集线程在计时器工作下轮询工作池执行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(); } } }
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); } } }
采集线程中包括后台线程,任务访问入口(用于采集器指定的任务)。调度器中采集线程集合和计时器两个对象。
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(); } }
调度器中的计时器轮询采集线程集合中的采集线程的状态,如果线程空闲则获取新任务分配给此线程。调度器通过开关计时器来控制采集线程。
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(); } }
调用方法同第一种。
两种采集器还有个明显的差别,第一种采集线程运行后,由于有计时器的存在,线程可以持续运行。而第二种采集线程需要调度器不断的从外部来唤醒执行。两种方法各有优劣,第一种线程开启后可以自己执行,但是关闭的时候需要调度器循环访问挂起关闭。第二种只需要关闭调度器的计时器即可将所有的采集线程关闭挂起,但是需要调度器不断的进行唤醒线程。第一种控制力不强,这样调度器的线程则不会有太多的负担,第二种集中控制无疑给调度器线程增加了负担。由于第二种采集器只有调度器一个线程访问工作池,故工作池可以不必lock。
采集器可以由多个工作池多个调度器组合成更加复杂的采集器。比如基于搜索引擎的采集器。一个工作池和调度器负责关键词,一个工作池和调度器负责采集搜索引擎的搜索结果,再有一个工作池和调度器对搜索引擎搜索结果进行访问采集,最后将采集结果保存。此时至少有三个线程池和调度器进行拼接,建议采用多个线程池,而不是合并线程池增加采集线程的功能。诚然在一个线程中足以实现获取关键词、获取搜索引擎搜索结果、访问搜索结果获取最终原始采集数据一整套流程,但是这样会使开发调试难度成倍增加。
多线程的调试难度比较大,可以尝试单元测试。而且多线程的开发中单一原则的使用会使代码更加易懂,另外注意异常日志的记录。
----------------------------------------------------无量的分割线---------------------------------------------------------------
一年多未更新博客,由于技术进步比较缓慢,工作生活进入慢速期,当持之以恒,共勉之。