Thread类是.NET中用于创建和管理线程的类。
使用多线程处理的应用程序可以更快地响应用户输入,因为在单独的线程上执行处理器密集型任务时,用户界面将保持活动状态。如果需要更好地控制应用程序线程的行为,可以自己管理线程。
1、创建多线程
通过创建System.Threding.Thread类的新实例来创建新线程。
将要在创建的新线程上执行的方法名提供给构造构造。
启动线程的时候调用Thread.Start方法。
using System;
using System.Threading;
public class ServerClass
{
// The method that will be called when the thread is started.
public void InstanceMethod()
{
Console.WriteLine(
"ServerClass.InstanceMethod is running on another thread.");
// Pause for a moment to provide a delay to make
// threads more apparent.
Thread.Sleep(3000);
Console.WriteLine(
"The instance method called by the worker thread has ended.");
}
public static void StaticMethod()
{
Console.WriteLine(
"ServerClass.StaticMethod is running on another thread.");
// Pause for a moment to provide a delay to make
// threads more apparent.
Thread.Sleep(5000);
Console.WriteLine(
"The static method called by the worker thread has ended.");
}
}
public class Simple
{
public static void Main()
{
ServerClass serverObject = new ServerClass();
// Create the thread object, passing in the
// serverObject.InstanceMethod method using a
// ThreadStart delegate.
Thread InstanceCaller = new Thread(
new ThreadStart(serverObject.InstanceMethod));
// Start the thread.
InstanceCaller.Start();
Console.WriteLine("The Main() thread calls this after "
+ "starting the new InstanceCaller thread.");
// Create the thread object, passing in the
// serverObject.StaticMethod method using a
// ThreadStart delegate.
Thread StaticCaller = new Thread(
new ThreadStart(ServerClass.StaticMethod));
// Start the thread.
StaticCaller.Start();
Console.WriteLine("The Main() thread calls this after "
+ "starting the new StaticCaller thread.");
}
}
// The example displays the output like the following:
// The Main() thread calls this after starting the new InstanceCaller thread.
// The Main() thread calls this after starting the new StaticCaller thread.
// ServerClass.StaticMethod is running on another thread.
// ServerClass.InstanceMethod is running on another thread.
// The instance method called by the worker thread has ended.
// The static method called by the worker thread has ended.
2、暂停、zhongda线程
使用Thread.Sleep(t)方法暂停指定时间t内的当前线程。
调用Thread.Interrupt来中断受阻的线程。
3、Thread的属性
IsActive:如果此线程已启动但尚未正常终止或中止,则返回 true
。
IsBackground:获取或设置布尔值,该值指示线程是否为后台线程。
Name:获取或设置线程的名称。 最常用于在调试时查找各个线程。
Priority:获取或设置由操作系统用来确定线程计划优先顺序的 ThreadPriority 值。
ThreadState:线程的当前状态。
4、计划线程
线程在初始时会被分配默认的优先级Normal,如果要获取/修改,可以使用priority属性。
5、取消线程
以协作的方式取消线程
在 .NET Framework 4 之前,.NET 不提供内置方法在线程启动后以协作方式取消线程。 不过,从 .NET Framework 4 开始,可以使用 System.Threading.CancellationToken 来取消线程,就像使用它们取消 System.Threading.Tasks.Task 对象或 PLINQ 查询一样。 虽然 System.Threading.Thread 类不提供对取消标记的内置支持,但可以使用采用 ParameterizedThreadStart 委托的 Thread 构造函数将一个标记传递给一个线程过程。 下面的示例演示如何执行此操作。
using System;
using System.Threading;
public class ServerClass
{
public static void StaticMethod(object? obj)
{
if (obj is null)
return;
CancellationToken ct = (CancellationToken)obj;
Console.WriteLine("ServerClass.StaticMethod is running on another thread.");
// Simulate work that can be canceled.
while (!ct.IsCancellationRequested)
{
Thread.SpinWait(50000);
}
Console.WriteLine("The worker thread has been canceled. Press any key to exit.");
Console.ReadKey(true);
}
}
public class Simple
{
public static void Main()
{
// The Simple class controls access to the token source.
CancellationTokenSource cts = new CancellationTokenSource();
Console.WriteLine("Press 'C' to terminate the application...\n");
// Allow the UI thread to capture the token source, so that it
// can issue the cancel command.
Thread t1 = new Thread(() =>
{
if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
cts.Cancel();
});
// ServerClass sees only the token, not the token source.
Thread t2 = new Thread(new ParameterizedThreadStart(ServerClass.StaticMethod));
// Start the UI thread.
t1.Start();
// Start the worker thread and pass it the token.
t2.Start(cts.Token);
t2.Join();
cts.Dispose();
}
}
// The example displays the following output:
// Press 'C' to terminate the application...
//
// ServerClass.StaticMethod is running on another thread.
// The worker thread has been canceled. Press any key to exit.
#1# 如何通过轮询侦听取消请求
下面的示例展示了一种方便用户代码定期轮询取消令牌,以确定是否已通过调用线程发出取消请求的方式。 此示例使用 System.Threading.Tasks.Task 类型,但相同的模式适用于 System.Threading.ThreadPool 类型或 System.Threading.Thread 类型直接创建的异步操作。
若要轮询,必须有某种循环或递归代码,可用于定期读取布尔 IsCancellationRequested 属性的值。 如果使用的是 System.Threading.Tasks.Task 类型,且正在等待任务在调用线程上完成,可以使用 ThrowIfCancellationRequested 方法来检查属性并抛出异常。 通过使用此方法,可确保抛出正确的异常来响应请求。 如果使用的是 Task,那么调用此方法优于手动抛出 OperationCanceledException。 如果无需抛出异常,可以直接检查属性,并通过方法返回结果(如果属性是
true
的话)。using System; using System.Threading; using System.Threading.Tasks; public struct Rectangle { public int columns; public int rows; } class CancelByPolling { static void Main() { var tokenSource = new CancellationTokenSource(); // Toy object for demo purposes Rectangle rect = new Rectangle() { columns = 1000, rows = 500 }; // Simple cancellation scenario #1. Calling thread does not wait // on the task to complete, and the user delegate simply returns // on cancellation request without throwing. Task.Run(() => NestedLoops(rect, tokenSource.Token), tokenSource.Token); // Simple cancellation scenario #2. Calling thread does not wait // on the task to complete, and the user delegate throws // OperationCanceledException to shut down task and transition its state. // Task.Run(() => PollByTimeSpan(tokenSource.Token), tokenSource.Token); Console.WriteLine("Press 'c' to cancel"); if (Console.ReadKey(true).KeyChar == 'c') { tokenSource.Cancel(); Console.WriteLine("Press any key to exit."); } Console.ReadKey(); tokenSource.Dispose(); } static void NestedLoops(Rectangle rect, CancellationToken token) { for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) { // Assume that we know that the inner loop is very fast. // Therefore, polling once per column in the outer loop condition // is sufficient. for (int row = 0; row < rect.rows; row++) { // Simulating work. Thread.SpinWait(5_000); Console.Write("{0},{1} ", col, row); } } if (token.IsCancellationRequested) { // Cleanup or undo here if necessary... Console.WriteLine("\r\nOperation canceled"); Console.WriteLine("Press any key to exit."); // If using Task: // token.ThrowIfCancellationRequested(); } } }
调用 ThrowIfCancellationRequested 非常快,不会在循环中产生很大的开销。
如果调用的是 ThrowIfCancellationRequested,那么如果除了抛出异常之外,还要执行其他工作来响应取消,只需显式检查 IsCancellationRequested 属性即可。 在此示例中,可以看到代码实际访问属性两次:一次是显式访问,另一次是在 ThrowIfCancellationRequested 方法中。 不过,由于读取 IsCancellationRequested 属性在每次访问时只涉及一个易失读取指令,因此从性能角度来看,双重访问的意义并不大。 最好仍调用此方法,而不是手动抛出 OperationCanceledException。
#2# 注册取消请求的回调
了解如何注册当 IsCancellationRequested 属性为 true 时调用的委托。 对创建令牌的对象调用 Cancel 时,值将从 false 更改为 true。 使用此技术可取消本地不支持统一取消框架的异步操作,也可取消阻止可能等待异步操作结束的方法。
在下面的示例中,CancelAsync 方法注册为在通过取消标记请求取消时要调用的方法。
using System; using System.Net; using System.Threading; using System.Threading.Tasks; class CancelWithCallback { static void Main() { using var cts = new CancellationTokenSource(); var token = cts.Token; _ = Task.Run(async () => { using var client = new WebClient(); client.DownloadStringCompleted += (_, args) => { if (args.Cancelled) { Console.WriteLine("The download was canceled."); } else { Console.WriteLine("The download has completed:\n"); Console.WriteLine($"{args.Result}\n\nPress any key to continue."); } }; if (!token.IsCancellationRequested) { using CancellationTokenRegistration ctr = token.Register(() => client.CancelAsync()); Console.WriteLine("Starting request\n"); await client.DownloadStringTaskAsync(new Uri("http://www.contoso.com")); } }, token); Console.WriteLine("Press 'c' to cancel.\n\n"); if (Console.ReadKey().KeyChar == 'c') { cts.Cancel(); } Console.WriteLine("\nPress any key to exit."); Console.ReadKey(); } }
如果注册回调时已经请求了取消,那么仍然要保证调用回调。 在此特定情况下,如果当前未执行异步操作,则 CancelAsync 方法将不进行任何操作,因此调用此方法始终是安全的。
#3# 如何侦听具有等待句柄的取消请求
如果方法在等待事件收到信号时受阻止,既无法检查取消令牌的值,也无法及时响应。 第一个示例展示了如何在使用 System.Threading.ManualResetEvent 等不本机支持统一取消框架的事件时解决此问题。 第二个示例展示了一种更简化的方法,即使用确实支持统一取消的 System.Threading.ManualResetEventSlim。
下面的示例使用 ManualResetEvent 展示了如何取消阻止不支持统一取消的等待句柄。
using System; using System.Threading; using System.Threading.Tasks; class CancelOldStyleEvents { // Old-style MRE that doesn't support unified cancellation. static ManualResetEvent mre = new ManualResetEvent(false); static void Main() { var cts = new CancellationTokenSource(); // Pass the same token source to the delegate and to the task instance. Task.Run(() => DoWork(cts.Token), cts.Token); Console.WriteLine("Press s to start/restart, p to pause, or c to cancel."); Console.WriteLine("Or any other key to exit."); // Old-style UI thread. bool goAgain = true; while (goAgain) { char ch = Console.ReadKey(true).KeyChar; switch (ch) { case 'c': cts.Cancel(); break; case 'p': mre.Reset(); break; case 's': mre.Set(); break; default: goAgain = false; break; } Thread.Sleep(100); } cts.Dispose(); } static void DoWork(CancellationToken token) { while (true) { // Wait on the event if it is not signaled. int eventThatSignaledIndex = WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle }, new TimeSpan(0, 0, 20)); // Were we canceled while waiting? if (eventThatSignaledIndex == 1) { Console.WriteLine("The wait operation was canceled."); throw new OperationCanceledException(token); } // Were we canceled while running? else if (token.IsCancellationRequested) { Console.WriteLine("I was canceled while running."); token.ThrowIfCancellationRequested(); } // Did we time out? else if (eventThatSignaledIndex == WaitHandle.WaitTimeout) { Console.WriteLine("I timed out."); break; } else { Console.Write("Working... "); // Simulating work. Thread.SpinWait(5000000); } } } }
下面的示例使用 ManualResetEventSlim 展示了如何取消阻止确实支持统一取消的协调基元。 相同的方法可以与其他轻型协调基元(如 Semaphore
Slim
和 CountdownEvent)结合使用。using System; using System.Threading; using System.Threading.Tasks; class CancelNewStyleEvents { // New-style MRESlim that supports unified cancellation // in its Wait methods. static ManualResetEventSlim mres = new ManualResetEventSlim(false); static void Main() { var cts = new CancellationTokenSource(); // Pass the same token source to the delegate and to the task instance. Task.Run(() => DoWork(cts.Token), cts.Token); Console.WriteLine("Press c to cancel, p to pause, or s to start/restart,"); Console.WriteLine("or any other key to exit."); // New-style UI thread. bool goAgain = true; while (goAgain) { char ch = Console.ReadKey(true).KeyChar; switch (ch) { case 'c': // Token can only be canceled once. cts.Cancel(); break; case 'p': mres.Reset(); break; case 's': mres.Set(); break; default: goAgain = false; break; } Thread.Sleep(100); } cts.Dispose(); } static void DoWork(CancellationToken token) { while (true) { if (token.IsCancellationRequested) { Console.WriteLine("Canceled while running."); token.ThrowIfCancellationRequested(); } // Wait on the event to be signaled // or the token to be canceled, // whichever comes first. The token // will throw an exception if it is canceled // while the thread is waiting on the event. try { // mres is a ManualResetEventSlim mres.Wait(token); } catch (OperationCanceledException) { // Throw immediately to be responsive. The // alternative is to do one more item of work, // and throw on next iteration, because // IsCancellationRequested will be true. Console.WriteLine("The wait operation was canceled."); throw; } Console.Write("Working..."); // Simulating work. Thread.SpinWait(500000); } } }
#4# 如何侦听多个取消请求
此示例展示了如何同时侦听两个取消令牌,以便在其中任意一个令牌发出请求时取消操作。
在下面的示例中,CreateLinkedTokenSource 方法用于将两个令牌联接为一个令牌。 这样一来,可以将此令牌传递到只需要使用一个取消令牌作为参数的方法中。 此示例展示了以下常见方案:方法必须同时观察从类外部传入的令牌和类内部生成的令牌。
using System; using System.Threading; using System.Threading.Tasks; class LinkedTokenSourceDemo { static void Main() { WorkerWithTimer worker = new WorkerWithTimer(); CancellationTokenSource cts = new CancellationTokenSource(); // Task for UI thread, so we can call Task.Wait wait on the main thread. Task.Run(() => { Console.WriteLine("Press 'c' to cancel within 3 seconds after work begins."); Console.WriteLine("Or let the task time out by doing nothing."); if (Console.ReadKey(true).KeyChar == 'c') cts.Cancel(); }); // Let the user read the UI message. Thread.Sleep(1000); // Start the worker task. Task task = Task.Run(() => worker.DoWork(cts.Token), cts.Token); try { task.Wait(cts.Token); } catch (OperationCanceledException e) { if (e.CancellationToken == cts.Token) Console.WriteLine("Canceled from UI thread throwing OCE."); } catch (AggregateException ae) { Console.WriteLine("AggregateException caught: " + ae.InnerException); foreach (var inner in ae.InnerExceptions) { Console.WriteLine(inner.Message + inner.Source); } } Console.WriteLine("Press any key to exit."); Console.ReadKey(); cts.Dispose(); } } class WorkerWithTimer { CancellationTokenSource internalTokenSource = new CancellationTokenSource(); CancellationToken internalToken; CancellationToken externalToken; Timer timer; public WorkerWithTimer() { // A toy cancellation trigger that times out after 3 seconds // if the user does not press 'c'. timer = new Timer(new TimerCallback(CancelAfterTimeout), null, 3000, 3000); } public void DoWork(CancellationToken externalToken) { // Create a new token that combines the internal and external tokens. this.internalToken = internalTokenSource.Token; this.externalToken = externalToken; using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)) { try { DoWorkInternal(linkedCts.Token); } catch (OperationCanceledException) { if (internalToken.IsCancellationRequested) { Console.WriteLine("Operation timed out."); } else if (externalToken.IsCancellationRequested) { Console.WriteLine("Cancelling per user request."); externalToken.ThrowIfCancellationRequested(); } } } } private void DoWorkInternal(CancellationToken token) { for (int i = 0; i < 1000; i++) { if (token.IsCancellationRequested) { // We need to dispose the timer if cancellation // was requested by the external token. timer.Dispose(); // Throw the exception. token.ThrowIfCancellationRequested(); } // Simulating work. Thread.SpinWait(7500000); Console.Write("working... "); } } public void CancelAfterTimeout(object? state) { Console.WriteLine("\r\nTimer fired."); internalTokenSource.Cancel(); timer.Dispose(); } }
如果链接令牌抛出 OperationCanceledException,传递到异常的令牌是链接令牌,而不是任意前置任务令牌。 若要确定取消的是哪个令牌,请直接查看前置任务令牌的状态。
虽然 AggregateException 不得引发,但此示例捕获到它的原因是,在实际情况下,任务委托引发的除 OperationCanceledException 外其他任何异常都包装在 AggregateException 中。
6、销毁线程
要终止线程,使用 System.Threading.CancellationToken。
若要强制终止线程的执行,使用Thread.Abort()
.Net Core不支持Thread.Abort方法,如果此时要强制终止,需要在单独的进程中运行该代码并使用Process.Kill
还需要注意,System.Threading.CancellationToken在.Net Framework 4之前的版本中无法使用。
线程一旦中止,将无法重启。
处理 ThreadAbortException
如果应中止线程,无论是由于在自己的代码中调用 Abort 所致,还是由于卸载正在运行线程的应用域(AppDomain.Unload 使用 Thread.Abort 终止线程)所致,线程都必须处理 ThreadAbortException,并在
finally
子句中执行任何最终处理,如下面的代码所示。try { // Code that is executing when the thread is aborted. } catch (ThreadAbortException ex) { // Clean-up code can go here. // If there is no Finally clause, ThreadAbortException is // re-thrown by the system at the end of the Catch clause. } // Do not put clean-up code here, because the exception // is rethrown at the end of the Finally clause.
清理代码必须位于
catch
子句或finally
子句中,因为系统会在finally
子句末尾或catch
子句(如果没有finally
子句的话)末尾重新抛出 ThreadAbortException。可以调用 Thread.ResetAbort 方法,以防系统重新抛出异常。 不过,只有在自己的代码导致 ThreadAbortException 抛出时,才应这样做。
7、死锁
两个线程中的每一个线程都尝试锁定另外一个线程已锁定的资源时,就会发生死锁,导致两个线程都无法执行。
托管线程处理类的许多方法都提供了超时锁定,有助于检测死锁。
例如,下面的代码舱室在lockObject对象上获取锁,如果300ms内没有获取锁,则返回false:
if (Monitor.TryEnter(lockObject, 300)) {
try {
// Place code protected by the Monitor here.
}
finally {
Monitor.Exit(lockObject);
}
}
else {
// Code to execute if the attempt times out.
}