C# “Thread类Suspend()与Resume()已过时” 解决方法(利用ManualResetEvent类)
近日用C#在项目中需要多线程编程时为了挂起与恢复线程使用了Thread类的Suspend()与Resume()方法,可是VS提示这两个方法已经过时了(过时原因微软的官方文档中有介绍:https://msdn.microsoft.com/en-us/library/system.threading.thread.suspend(v=vs.110).aspx ),主要是由于Suspend方法的挂起点难以确定,容易造成线程的锁死。不得已查找了相关的资料发现很多文档都介绍的不全面,缺少了很多介绍性的说明,所以整合起来记录一下。
项目中的需要也很简单,就是新开一个线程不断去检测当前修改的一个变量是否超时。先贴程序如下:
using System.Threading;
1 public delegate void TimeOutEventHandler(); 2 3 public interface ITimeOutThread 4 { 5 event TimeOutEventHandler DataTimeOutEvent; // 超时事件 6 void StopTimeOutCheck(); // 暂停超时检测 7 void StartTimeOutCheck(); // 恢复超时检测 8 void ClearTimeOutMark(); // 清除超时标志,防止超时 9 void DisposeTimeOutCheck(); // 彻底关闭超时检测 10 11 int TimeOutInterval // 超时间隔,单位为ms 12 { 13 get; 14 set; 15 } 16 17 } 18 19 /// <summary> 20 /// 超时检测线程 21 /// 运行方式: 22 /// 1. 设置超时时间,默认超时时间为 200ms 23 /// 2. 通过ResumeTimeOutCheck()来启动超时检测, SuspendTimeOutCheck()来暂停超时检测 24 /// 3. 启动超时检测后,通过ClearTimeOutMark()来不断清除超时检测,防止其超时,通过SetTimeOutMark()来触发超时事件 25 /// 4. 超时事件为DataTimeOutEvent, 通过绑定超时事件处理函数来处理超时事件(超时事件发出后不暂停超时检测,这意味这需要手动暂停) 26 /// 5. 在线程使用完毕后一定要停止超时,停止后超时检测将直接停止(不可恢复) 27 /// </summary> 28 class TimeOutThread:ITimeOutThread 29 { 30 enum m_ThreadState 31 { 32 Stopped = 0, 33 Started, 34 Suspended, 35 Resumed, 36 }; 37 38 private int timeOutMark; // 超时标志 39 private m_ThreadState stateOfThread; // 线程运行状态 40 private Thread checkMarkThread; // 检测超时线程 41 private object criticalAraeLockItem; // 线程安全锁变量 42 private bool threadControl; // 线程停止循环运行标志 43 44 private ManualResetEvent manualResetEvent; // 线程控制事件, 当被手动Reset()则WaitOne()会使线程阻塞 45 // 当被Set()便再不会被阻塞直到Reset() 46 // 在本类中,当停止检测超时时便手动Reset()使线程阻塞,开始检测 47 // 时Set()以使线程持续执行 48 49 private int timeOutInterval; // 超时时间 50 public int TimeOutInterval 51 { 52 get 53 { 54 return timeOutInterval; 55 } 56 57 set 58 { 59 timeOutInterval = value; 60 } 61 } 62 63 public event TimeOutEventHandler DataTimeOutEvent; 64 65 /// <summary> 66 /// 构造函数, 通过设置超时时间初始化 67 /// </summary> 68 /// <param name="timeOutTime"></param> 69 public TimeOutThread(int timeoutTime) 70 { 71 this.criticalAraeLockItem = new object(); 72 this.threadControl = true; 73 this.timeOutInterval = timeoutTime; 74 this.ClearTimeOutMark(); 75 this.stateOfThread = m_ThreadState.Suspended; 76 this.checkMarkThread = new Thread(new ThreadStart(this.CheckTimeOutMark)); 77 this.manualResetEvent = new ManualResetEvent(false); // 初始情况便阻塞以便启动线程 78 this.checkMarkThread.Start(); // 此时虽然启动线程,但线程阻塞,不会运行 79 } 80 81 /// <summary> 82 /// 默认构造函数,默认超时200ms 83 /// </summary> 84 public TimeOutThread() 85 : this(200) 86 { 87 88 } 89 90 /// <summary> 91 /// 阻塞线程 92 /// </summary> 93 private void SuspendThread() 94 { 95 this.manualResetEvent.Reset(); 96 } 97 98 /// <summary> 99 /// 恢复线程 100 /// </summary> 101 private void ResumeThread() 102 { 103 this.manualResetEvent.Set(); 104 } 105 106 /// <summary> 107 /// 启动超时检测线程 108 /// </summary> 109 public void StartTimeOutCheck() 110 { 111 if (this.stateOfThread == m_ThreadState.Suspended) // 线程已启动但是被挂起 112 { 113 // 恢复线程 114 this.ResumeThread(); 115 } 116 117 // 更新状态 118 this.stateOfThread = m_ThreadState.Resumed; 119 } 120 121 /// <summary> 122 /// 停止超时检测线程 123 /// </summary> 124 public void StopTimeOutCheck() 125 { 126 if (this.stateOfThread == m_ThreadState.Resumed) 127 { 128 this.SuspendThread(); 129 this.stateOfThread = m_ThreadState.Suspended; 130 } 131 } 132 133 /// <summary> 134 /// 彻底停止超时检测 135 /// </summary> 136 public void DisposeTimeOutCheck() 137 { 138 this.threadControl = false; // 停止线程循环 139 this.ResumeThread(); 140 this.stateOfThread = m_ThreadState.Suspended; 141 142 try 143 { 144 this.checkMarkThread.Abort(); 145 } 146 catch (Exception) 147 { 148 149 } 150 } 151 /// <summary> 152 /// 检测超时标记是否已经被清除 153 /// </summary> 154 /// <returns></returns> 155 private bool IsTimeOutMarkCleared() 156 { 157 if (this.timeOutMark == 1) 158 return true; 159 else 160 return false; 161 } 162 163 /// <summary> 164 /// 清除超时标记 165 /// </summary> 166 public void ClearTimeOutMark() 167 { 168 lock(this.criticalAraeLockItem) 169 { 170 this.timeOutMark = 1; // 清除超时标记 171 } 172 } 173 174 175 /// <summary> 176 /// 设置超时标记 177 /// </summary> 178 private void SetTimeOutMark() 179 { 180 lock(this.criticalAraeLockItem) 181 { 182 this.timeOutMark = 0; // 设置超时标记 183 } 184 } 185 186 /// <summary> 187 /// routine work, 在threadControl不为false时不断检测timeOutMark,若有超时,则发出超时事件 188 /// </summary> 189 private void CheckTimeOutMark() 190 { 191 while (this.threadControl == true) 192 { 193 manualResetEvent.WaitOne(); // 用以阻塞线程, 当Set()被调用后恢复,Reset()被调用后阻塞 194 195 Thread.Sleep(this.timeOutInterval); // 线程睡眠超时事件长度 196 197 if (this.IsTimeOutMarkCleared()) // 线程超时标志已被更新,不发出超时事件 198 { 199 //设置超时标志, 若下次检测超时标记依旧处于被设置状态,则超时 200 this.SetTimeOutMark(); 201 } 202 else 203 { 204 // 超时标志未被清除并且未被要求停止检测超时,发出超时事件 205 if ((DataTimeOutEvent != null) && (this.stateOfThread == m_ThreadState.Resumed)) 206 { 207 DataTimeOutEvent.Invoke(); 208 } 209 } 210 } 211 212 } 213 214 /// <summary> 215 /// 析构函数 216 /// </summary> 217 ~TimeOutThread() 218 { 219 this.DisposeTimeOutCheck(); // 彻底停止线程 220 } 221 }
开始运行线程后 ,不断检测timeOutMark变量在线程休眠的期间是否被重置过,即是否调用了接口中的ClearTimeOutMark()方法。当在规定的timeoutInterval期间调用了该方法后发出超时事件等待处理,在处理的过程中为了节约系统资源便需要挂起线程以及恢复线程。
在程序中使用了ManualResetEvent类,此类用于进行线程安全的线程通讯(System.Threading)。在应用中一般只需要使用ManualResetEvent类的四个方法:构造函数,Set(), Reset(), 以及WaitOne().
此处引用(http://www.cnblogs.com/modify/archive/2013/01/10/2855014.html)的博客中关于此类的介绍:
在看介绍理解前可以先假设ManualResetEvent类中有一个bool类型的变量A
1、初始化:public ManualResetEvent(bool initialState);
ManualResetEvent的构造方法有个bool型参数,当为true时,则表示有信号(该变量A初始状态为true, 处于Set状态),为false时,则表示无信号(即该变量A为false, 处于ReSet状态)。这个怎么理解呢?我们接着看ManualResetEvent3个基本方法中的WaitOne方法。
2、WaitOne方法:WaitOne方法有几种4种重载,我在这里只对它的功能进行分析。
WaitOne方法,顾名思义,它会具有一种等待的功能,也就是线程阻塞。这里的阻塞功能是有条件的,当无信号(ManualResetEvent对象的A变量为false, 状态为Reset)时,它是阻塞的,有信号时(A变量为true,状态为Set),它将无任何阻塞,被执行时就直接跳过了(这个从逻辑上应该挺好理解:当有信号需要处理时,需要立即处理,没有任何信号时,就当然要等一等了)。所以,回顾到1,当初始化ManualResetEvent时,initialState为false,WaitOne将会有阻塞效果,否则,没有阻塞效果。
3、Set方法:将ManualResetEvent对象的信号状态设为有信号状态,这个时候WaitOne如果正在阻塞中的话,将会立即终止阻塞,向下继续执行。而且这个状态一直不变的话,每次执行到WaitOne都将无任何阻塞。
4、Reset方法:将ManualResetEvent对象的信号状态设为无信号状态,当下次执行到WaitOne时,又将重新开始阻塞。
这里对应这些操作到需要Suspend与Resume的线程上来,首先可以先将ManualResetEvent对象的WaitOne()方法放在该线程需要循环执行的函数内部(一般应该放在开头,这样可以使线程的挂起恢复的位置都在线程任务的初始点)。需要Suspend功能时将调用ManualResetEvent对象的Reset()方法, 这时线程任务运行至WaitOne()语句就会阻塞,起到了挂起线程的作用。当需要Resume时,调用Set()方法,WaitOne()便不再阻塞向下执行直到下次调用Reset()方法。这种做法实际上就避免了Suspend在程序中不知道挂起位置在哪里的弊端,使得挂起位置始终处于WaitOne()方法调用的位置。
需要再提一下的是manualResetEvent在初始化时的bool参数,当该参数为true时,manualResetEvent对象在开始时便处于Set状态,WaitOne()不会阻塞,当该参数为false时, manualResetEvent对象在开始时处于Reset()状态,第一次调用WaitOne()方法时便会使线程阻塞,直到第一次调用Reset()方法才可以使线程任务继续执行。
(除引用内容外,介绍理解内容均为原创,转载请注明出处)