线程处理使 C# 程序能够执行并发处理,以便您可以同时执行多个操作。例如,您可以使用线程处理来监视用户输入,执行后台任务,以及处理并发输入流。System.Threading 命名空间提供支持多线程编程的类和接口,使您可以轻松地执行创建和启动新线程,同步多个线程,挂起线程以及中止线程等任务。
若要在 C# 代码中合并线程处理,只需创建一个将在主线程外执行的函数,并让一个新的 Thread 对象指向该函数即可。下面的代码示例在 C# 应用程序中创建一个新线程:
newThread = new System.Threading.Thread(anObject.AMethod);
下面的代码示例在 C# 应用程序中启动一个新线程:
newThread.Start();
多线程处理可解决响应性和多任务的问题,但同时引入了资源共享和同步问题,因为根据中央线程调度机制,线程将在没有警告的情况下中断和继续。有关更多信息,请参见线程同步。有关概述信息,请参见使用线程和线程处理。
此示例演示如何创建辅助线程,并用它与主线程并行执行处理。还将演示如何使一个线程等待另一个线程,并正确地终止线程。有关多线程处理的背景信息,请参见托管线程处理和使用线程处理(C# 编程指南)。
该示例创建一个名为 Worker 的类,该类包含辅助线程将执行的方法 DoWork。这实际上是辅助线程的 Main 函数。辅助线程将通过调用此方法来开始执行,并在此方法返回时自动终止。DoWork 方法如下所示:
Worker 类包含另一个方法,该方法用于通知 DoWork 它应当返回。此方法名为 RequestStop,如下所示:
RequestStop 方法只是将 true 赋给 _shouldStop 数据成员。由于此数据成员由 DoWork 方法来检查,因此这会间接导致 DoWork 返回,从而终止辅助线程。但是,需要注意:DoWork 和 RequestStop 将由不同线程执行。DoWork 由辅助线程执行,而 RequestStop 由主线程执行,因此 _shouldStop 数据成员声明为 volatile,如下所示:
volatile 关键字用于通知编译器,将有多个线程访问 _shouldStop 数据成员,因此它不应当对此成员的状态做任何优化假设。有关更多信息,请参见 volatile(C# 参考)。
通过将 volatile 与 _shouldStop 数据成员一起使用,可以从多个线程安全地访问此成员,而不需要使用正式的线程同步技术,但这仅仅是因为 _shouldStop 是 bool。这意味着只需要执行单个原子操作就能修改 _shouldStop。但是,如果此数据成员是类、结构或数组,那么,从多个线程访问它可能会导致间歇的数据损坏。假设有一个更改数组中的值的线程。Windows 定期中断线程,以便允许其他线程执行,因此线程会在分配某些数组元素之后和分配其他元素之前被中断。这意味着,数组现在有了一个程序员从不想要的状态,因此,读取此数组的另一个线程可能会失败。
在实际创建辅助线程之前,Main 函数会创建一个 Worker 对象和 Thread 的一个实例。线程对象被配置为:通过将对 Worker.DoWork 方法的引用传递给 Thread 构造函数,来将该方法用作入口点,如下所示:
此时,尽管辅助线程对象已存在并已配置,但尚未创建实际的辅助线程。只有当 Main 调用 Start 方法后,才会创建实际的辅助线程:
此时,系统将启动辅助线程的执行,但这是在与主线程异步执行的。这意味着 Main 函数将在辅助线程进行初始化的同时继续执行代码。为了保证 Main 函数不会尝试在辅助线程有机会执行之前将它终止,Main 函数将一直循环,直到辅助线程对象的 IsAlive 属性设置为 true:
下一步,通过调用 Sleep 来将主线程中断片刻。这保证了辅助线程的 DoWork 函数在 Main 函数执行其他任何命令之前,在 DoWork 方法内部执行若干次循环:
在 1 毫秒之后,Main 将通知辅助线程对象,它应当使用 Worker.RequestStop 方法(前面已介绍)自行终止:
还可以通过调用 Abort 来从一个线程终止另一个线程,但这会强行终止受影响的线程,而不管它是否已完成自己的任务,并且不提供清理资源的机会。此示例中显示的技术是首选方法。
最后,Main 函数对辅助线程对象调用 Join 方法。此方法导致当前线程阻塞或等待,直到对象所表示的线程终止。因此,直到辅助线程返回后,Join 才会返回,然后自行终止:
此时,只有执行 Main 的主线程还存在。它会显示一条最终消息,然后返回,从而使主线程也终止。
下面显示了完整的示例:





















































输出结果:















“线程池”是可以用来在后台执行多个任务的线程集合。(有关背景信息,请参见使用线程处理。)这使主线程可以自由地异步执行其他任务。
线程池通常用于服务器应用程序。每个传入请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。
一旦池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用。这种重用使应用程序可以避免为每个任务创建新线程的开销。
线程池通常具有最大线程数限制。如果所有线程都繁忙,则额外的任务将放入队列中,直到有线程可用时才能够得到处理。
您可以实现自己的线程池,但是通过 ThreadPool 类使用 .NET Framework 提供的线程池更容易一些。
下面的示例使用 .NET Framework 线程池计算 20 和 40 之间的十个数的 Fibonacci 结果。每个 Fibonacci 结果都由 Fibonacci 类表示,该类提供一种名为 ThreadPoolCallback 的方法来执行此计算。将创建表示每个 Fibonacci 值的对象,ThreadPoolCallback 方法将传递给 QueueUserWorkItem,它分配池中的一个可用线程来执行此方法。
由于为每个 Fibonacci 对象都提供了一个半随机值来进行计算,而且十个线程都将竞争处理器时间,因此无法提前知道十个结果全部计算出来所需的时间。因此在构造期间为每个 Fibonacci 对象传递 ManualResetEvent 类的一个实例。当计算完成时,每个对象都通知提供的事件对象,使主线程用 WaitAll 阻止执行,直到十个 Fibonacci 对象全部计算出了结果。然后 Main 方法将显示每个 Fibonacci 结果。



















































































输出结果:

































下面的示例演示使用 lock 关键字以及 AutoResetEvent 和 ManualResetEvent 类对主线程和两个辅助线程进行线程同步。有关更多信息,请参见 lock 语句(C# 参考)。
该示例创建两个辅助线程。一个线程生成元素并将它们存储在非线程安全的泛型队列中。有关更多信息,请参见Queue。另一个线程使用此队列中的项。另外,主线程定期显示队列的内容,因此该队列被三个线程访问。lock 关键字用于同步对队列的访问,以确保队列的状态没有被破坏。
除了用 lock 关键字来阻止同时访问外,还用两个事件对象提供进一步的同步。一个事件对象用来通知辅助线程终止,另一个事件对象由制造者线程用来在有新项添加到队列中时通知使用者线程。这两个事件对象封装在一个名为SyncEvents 的类中。这使事件可以轻松传递到表示制造者线程和使用者线程的对象。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;
}
AutoResetEvent 类用于“新项”事件,因为您希望每当使用者线程响应此事件后,此事件都能自动重置。或者,将ManualResetEvent 类用于“退出”事件,因为您希望当此事件终止时有多个线程响应。如果您改为使用AutoResetEvent,则仅在一个线程响应该事件以后,该事件就还原到非终止状态。另一个线程不会响应,因此在这种情况下,将无法终止。
SyncEvents 类创建两个事件,并将它们以两种不同的形式存储:一种是作为 EventWaitHandle(它是AutoResetEvent 和 ManualResetEvent 的基类),一种是作为基于 WaitHandle 的数组。如关于使用者线程的讨论中所述,此数组是必需的,因为它使使用者线程可以响应两个事件中的任何一个。
使用者线程和制造者线程分别由名为 Consumer 和 Producer 的类表示,这两个类都定义名为 ThreadRun 的方法。这些方法用作 Main 方法创建的辅助线程的入口点。
Producer 类定义的 ThreadRun 方法如下所示:
public void ThreadRun()
{
int count = 0;
Random r = new Random();
while (!_syncEvents.ExitThreadEvent.WaitOne(0, false))
{
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);
}
此方法一直循环,直到“退出线程”事件变为终止状态。此事件的状态由 WaitOne 方法使用 SyncEvents 类定义的ExitThreadEvent 属性测试。在这种情况下,检查该事件的状态不会阻止当前线程,因为 WaitOne 使用的第一个参数为零,这表示该方法应立即返回。如果 WaitOne 返回 true,则说明该事件当前处于终止状态。如果是这样,ThreadRun 方法将返回,其效果相当于终止执行此方法的辅助线程。
在“退出线程”事件终止前,Producer.ThreadStart 方法将尝试在队列中保持 20 个项。每个项是 0 到 100 之间的一个整数。在添加新项前,该集合必须处于锁定状态,以防止使用者线程和主线程同时访问该集合。这是使用 lock关键字来实现的。传递给 lock 的参数是通过 ICollection 接口公开的 SyncRoot 字段。此字段专门为同步线程访问而提供。对集合的独占访问权限被授予 lock 后面的代码块中包含的所有指令。对于制造者添加到队列中的每个新项,都将调用“新项”事件的 Set 方法。这将通知使用者线程离开挂起状态并开始处理新项。
Consumer 对象还定义名为 ThreadRun 的方法。与制造者的 ThreadRun 类似,此方法由 Main 方法创建的辅助线程执行。然而,使用者的 ThreadStart 必须响应两个事件。Consumer.ThreadRun 方法如下所示:
此方法使用 WaitAny 来阻止使用者线程,直到所提供的数组中的任意一个等待句柄变为终止状态。在这种情况下,数组中有两个句柄,一个用来终止辅助线程,另一个用来指示有新项添加到集合中。WaitAny 返回变为终止状态的事件的索引。“新项”事件是数组中的第一个事件,因此索引为零表示新项。在这种情况下,检查索引为 1 的项(它表示“退出线程”事件),以确定此方法是否继续使用项。如果“新项”事件处于终止状态,您将通过 lock 获得对集合的独占访问权限并使用新项。因为此示例生成并使用数千个项,所以不显示使用的每个项,而是使用 Main 定期显示队列中的内容,如下面所演示的那样。
Main 方法从创建队列(队列中的内容将被生成和使用)和 SyncEvents 的实例(已在前面演示)开始:
然后,Main 配置 Producer 和 Consumer 对象以供辅助线程使用。然而,此步骤并不创建或启动实际的辅助线程:
请注意,队列和同步事件对象作为构造函数参数同时传递给 Consumer 和 Producer 线程。这为两个对象提供了它们执行各自任务所需的共享资源。然后创建两个新的 Thread 对象,并对每个对象使用 ThreadRun 方法作为参数。每个辅助线程在启动时都将此参数用作线程的入口点。
然后,Main 通过调用 Start 方法来启动两个辅助线程,如下所示:
此时,创建了两个新的辅助线程,它们独立于当前正在执行 Main 方法的主线程开始异步执行过程。事实上,Main接下来要做的事情是通过调用 Sleep 方法将主线程挂起。该方法将当前正在执行的线程挂起指定的时间(毫秒)。在此时间间隔过后,Main 将重新激活,这时它将显示队列的内容。Main 重复此过程四次,如下所示:
最后,Main 通过调用“退出线程”事件的 Set 方法通知辅助线程终止,然后对每个辅助线程调用 Join 方法以阻止主线程,直到每个辅助线程都响应该事件并终止。
有一个线程同步的最终示例:ShowQueueContents 方法。与制造者线程和使用者线程类似,此方法使用 lock 获得对队列的独占访问权限。然而在这种情况下,独占访问尤其重要,因为 ShowQueueContents 对整个集合进行枚举。对集合进行枚举是一个很容易由于异步操作造成数据损坏的操作,因为它涉及遍历整个集合的内容。ShowQueueContents 方法如下所示:
最后请注意,ShowQueueContents 是由主线程执行的,因为它被 Main 调用。这意味着当此方法获得对项队列的独占访问权限时,它实际上既阻止了制造者线程访问队列,也阻止了使用者线程访问队列。ShowQueueContents 锁定队列并枚举其内容:
{
lock (((ICollection)q).SyncRoot)
{
foreach ( int item in q)
{
Console.Write("{0} ", item);
}
}
Console.WriteLine();
}
































































































































































































