C#支持通过多线程(multithreading),来并行的执行代码。每个线程都有一个独立的执行路径(execution path)能和其它的线程同时运行。被操作系统和CLR直接创建的,运行在独立的一个线程的C#客户端程序(Console, WPF, or Windows Forms)叫做主线程,而额外创建的其它线程就是多线程。 下面是一个简单的例子和输出。
所有的例子都引入以下的命名空间 :
1: using System;
2: using System.Threading;
3:
1: class ThreadTest
2: {
3: static void Main()
4: {
5: Thread t = new Thread(WriteY); // Kick off a new thread
6: t.Start(); // running WriteY()
7:
8: // Simultaneously, do something on the main thread.
9: for (int i = 0; i < 1000; i++) Console.Write("x");
10: }
11: static void WriteY()
12: {
13: for (int i = 0; i < 1000; i++) Console.Write("y");
14: }
15: }
16:
下面是运行结果:
主线程创建一个新的线程t,用来运行WriteY()方法。同时主线程重复的打印字符 “x”:
一旦一个线程启动了,该线程的IsAlive属性就被置为true,一直到线程执行结束。当委托传递给构造函数执行完毕后,线程就结束了,
线程一旦执行结束,就不能在被重新启动。
CLR为每个线程的局部变量分配独立的栈空间。在下个例子中,我们将定义一个带有局部变量的例子,然后分别在主线程和新创建的线程中调用:
1: static void Main()
2: {
3: new Thread (Go).Start(); // Call Go() on a new thread
4: Go(); // Call Go() on the main thread
5: }
6:
7: static void Go()
8: {
9: // Declare and use a local variable - 'cycles'
10: for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
11: }
12:
13:
运行结果如下图:
因为局部变量都有独立的栈空间,所以打印出来的?号是十个。
如果线程之间有共同的引用,则它们共享数据。例如下面代码:
1: class ThreadTest
2: {
3: bool done;
4:
5: static void Main()
6: {
7: ThreadTest tt = new ThreadTest(); // Create a common instance
8: new Thread(tt.Go).Start();
9: tt.Go();
10: }
11:
12: // Note that Go is now an instance method
13: void Go()
14: {
15: if (!done) { done = true; Console.WriteLine("Done"); }
16: }
17: }
18:
19:
因调用Go()方法的两个线程,拥有同一个引用,所以结果打印出来的是一个 ”Done”而不是两个。
静态变量以另外一种方式共享数据,下面是静态变量的例子:
1: class ThreadTest
2: {
3: static bool done; // Static fields are shared between all threads
4:
5: static void Main()
6: {
7: new Thread(Go).Start();
8: Go();
9: }
10:
11: static void Go()
12: {
13: if (!done) { done = true; Console.WriteLine("Done"); }
14: }
15: }
16:
17:
这个例子引出另外一个概念--线程安全。如果我们向下面那样修改Go方法,则"Done"被打印出来的次数是不确定的。
1: static void Go()
2: {
3: if (!done) { Console.WriteLine ("Done"); done = true; }
4: }
5:
造成这个问题原因是:当一个线程执行打印语句,还没来得及修改done为true的时候,另一个线程又执行了打印语句。C#中提供lock机制来解决这一问题:
1: class ThreadSafe
2: {
3: static bool done;
4: static readonly object locker = new object();
5:
6: static void Main()
7: {
8: new Thread(Go).Start();
9: Go();
10: }
11:
12: static void Go()
13: {
14: lock (locker)
15: {
16: if (!done) { Console.WriteLine("Done"); done = true; }
17: }
18: }
19: }
20:
当两个线程竞争同一个临界资源(locker)的时候,其中一个线程被阻塞,直到另一个线程释放该资源。这种方式保证了某一时刻只有一个线程在使用临界资源。所有 ”Done”只被打印了一次。以这种方式,保证代码在多线程不确定环境中能够正确执行,就叫做线程安全。
临界资源是造成多线程中错误的主要原因,应该让其尽量的保持简单。
阻塞的线程是不会消耗CPU资源的!
你可以通过调用Join方法来等待其他线程执行结束。例如:
1: static void Main()
2: {
3: Thread t = new Thread (Go);
4: t.Start();
5: t.Join();
6: Console.WriteLine ("Thread t has ended!");
7: }
8:
9: static void Go()
10: {
11: for (int i = 0; i < 1000; i++) Console.Write ("y");
12: }
13:
在打印了1000次字符y后,紧接着就打印出了 ”Thread t has ended!”这句话。你可以在Join方法的时候设置超时。如果正确执行则返回ture,如果超时则返回false。
Thread.Sleep方法让当前线程暂停一段时间:
1: Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour
2: Thread.Sleep (500); // sleep for 500 milliseconds
3:
无论执行Join还是Wait方法,阻塞的线程都不会占用CPU资源。
Thread.Sleep(0)
是让当前线程进行一个让位动作。 让其他线程在系统管理单元作出动作前有机会优先执行。在Framework 4.0中
Thread.Yield()也有同样的功能。
在CLR中,线程是被一个叫做线程调度器的委托函数管理的。线程调度器保证所有的线程都分配用来执行的CPU时间,并保证阻塞的线程不占用CPU资源。
线程和运行在你操作系统上的应用程序的进程有很大的相似性,就像多个进程并行的运行在计算机上一样,多个线程并行的运行在一个独立的进程上。进程与进程之间是完全隔离的,而线程只是在一定程度上是隔离的。运行在一个应用程序上的线程是共享堆内存的。这很大程度上也让线程变的很好用,比如:一个线程可以在后台取数据,另一个线程可以在前台呈现。
多线程有很多用途,下面为常见的:
另起一个线程来执行比较耗时的任务,让UI线程(Main线程),可以持续响应用户操作。
多线程是非常有用的,当一个线程等待另一台计算机或硬件的响应时。当一个线程执行某项任务而发生阻塞时,其他的线程能充分利用其他已经空闲的计算机。
在执行大量计算密集型任务时,通过采取分治策略,可以把任务量均摊在不同的线程上,这样可以加快任务的执行速度。
在多核处理器中,有时你能通过预测需要处理的内容来改善性能。LINQPad使用这个技术加速创建新的查询。并行运行多个不同的算法来解决同一个任务。
在web服务器上,多个客户端请求同时到达,因此需要并行处理(如果你使用ASP.NET,WCF,Web Services,或者Remoting等技术,FrameWork会自动的为你创建线程)。这在客户端也是非常有用的(例如P2P的网络处理或者是来自一个用户的多个请求)。
多线程也会产生额外的问题。最大的问题是多线程会在一定程度上增加程序的复杂度。拥有很多线程就其本身来言并不复杂;复杂的是线程之间的相互作用和影响(特别是通过共享数据)。这些应用无论是否是有意的相互影响,都会导致较长的开发周期和持续出现不易发现的bugs。出于这个原因,应使这种影响减到最少,尽可能避免过度设计。本书主要是讨论如何处理这些复杂问题;而对于消除相互影响说的很少。
一个好的策略是把多线程的逻辑封装到一个可复用的类里,这个类可以独立地被检查和测试。Framework本身提供了很多高级别的封装类,这些我们将会在后边介绍到. Threading会在调度和转换线程时消耗资源和CPU时间(当活动线程超过CPU 核数时),而且还有线程的创建和释放成本。多线程不是总是在加速你的应用程序 - 如果你使用的过多或者不恰当。例如,在磁盘和I/O被频繁调用时,使用两个工作线程按顺序运行任务,就比用10个线程同时运行要快。