C#多线程实践——为什么要多线程

   C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行。一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的。  

  一个简单示例如下:

using System; using System.Threading; class ThreadDemo { static void Main() { Thread t = new Thread (WriteY); t.Start(); while (true) Console.Write ("x"); } static void WriteY()  { while (true) Console.Write ("y"); } }

  主线程创建了一个新线程“t”,运行了一个重复打印字母"y"的方法,同时主线程重复打印字母“x”。CLR分配每个线程到它自己的内存堆栈上,来保证局部变量的分离运行。在接下来的方法中我们定义了一个局部变量,然后在主线程和新创建的线程上同时地调用这个方法。

static void Main() { new Thread (Go).Start(); Go(); } static void Go() { for (int cycles = 0; cycles < 5; cycles++)     Console.Write ('?'); }

  变量cycles的副本分别在各自的内存堆栈中创建,因此会有10个问号输出。而当线程们引用了一些公用的目标实例的时候,他们会共享数据。示例如下:

class ThreadDemo { bool done; static void Main() { ThreadDemo tt = new ThreadDemo(); new Thread (tt.Go).Start(); tt.Go(); } void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } }

  在上面的示例中,两个线程都调用了Go方法,它们共享了done字段,这个结果输出的是一个"Done",而不是两个。

  静态字段则提供了另一种在线程间共享数据的方式,下面是一个以done为静态字段的例子:

class ThreadDemo { static bool done; // 静态字段在所有线程中共享 static void Main() { new Thread (Go).Start(); Go(); } static void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } }

  上述两个示例足以说明另一个关键概念, 即线程安全, 输出实际上是不确定的:可能输出一次done,也可能打印两次。如果我们在Go方法里语句顺序, "Done"被打印两次的机会会大幅地上升,如:

static void Go() { if (!done) { Console.WriteLine ("Done"); done = true; } }

  问题出在当一个线程正在判断if块的时候,正好另一个线程在执行WriteLine语句,而且在它将done设置为true之前,导致输出两次done。补救措施是对读写公共字段提供一个排他锁;C#提供了lock语句来达到这个目的:

class ThreadSafe { static bool done; static object locker = new object(); static void Main() { new Thread (Go).Start(); Go(); } static void Go() { lock (locker) { if (!done) { Console.WriteLine ("Done"); done = true; } } } }

  当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以"Done"只被打印了1次。这样的代码编写方式在不确定的多线程环境中被叫做线程安全

     临时暂停或阻止是多线程同步活动的本质特征。等待一个排它锁被释放是一个线程被阻止的原因,另一个原因是线程想要暂停或Sleep一段时间:

Thread.Sleep (TimeSpan.FromSeconds (30));   

  一个线程也可以使用它的Join方法来等待另一个线程结束:

Thread t = new Thread (Go);           

t.Start(); t.Join(); 

  一个线程,一旦被阻止,它就不再消耗CPU的资源了。

线程是如何工作的

     线程在.NET中由一个线程协调程序管理着——一个CLR委托给操作系统的函数。线程协调程序确保将所有活动的线程被分配适当的执行时间;并且那些等待或阻止的线程——比如说在排它锁中、或在用户输入——都是不消耗CPU时间的。

     在单核处理器电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多,即通常在几微秒区间。在多核电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。这几乎可以肯定仍然会出现一些时间切片, 由于操作系统的需要服务自己的线程,以及一些其他的应用程序。

     线程由于外部因素(比如时间片)被中断被称为被抢占,在大多数情况下,一个线程方面在被抢占的那一时那一刻就失去了对它的控制权。

线程 vs. 进程

     属于一个单一应用程序的所有的线程逻辑上被包含在一个进程中,进程指一个应用程序所运行的操作系统单元。

     线程于进程有某些相似的地方,进程在电脑中运行方式与一个C#程序线程运行的方式大致相同。二者的关键区别在于进程彼此是完全隔绝的。线程与运行在相同程序其它线程共享堆内存,这就是线程为何如此有用:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。

何时使用多线程

     多线程程序一般被用来在后台执行耗时的任务。主线程保持运行,并且工作线程做它的后台工作。对于Windows Forms程序来说,如果主线程试图执行冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应。由于这个原因,应该在工作线程中运行一个耗时任务时添加一个工作线程,即使在主线程上有一个有好的提示“处理中...”,以防止工作无法继续。这就避免了程序出现由操作系统提示的“没有响应”,使得用户强制结束程序的进程而导致错误。模式对话框还允许实现“取消”功能,允许继续接收事件,而实际的任务已被工作线程完成。BackgroundWorker恰好可以辅助完成这一功能。

     在没有用户界面的程序里,比如说Windows Service, 多线程在当一个任务有潜在的耗时,因为它在等待另台电脑的响应(比如一个应用服务器,数据库服务器,或者一个客户端)的实现特别有意义。用工作线程完成任务意味着主线程可以立即做其它的事情。

     另一个多线程的用途是在方法中完成一个复杂的计算工作。这个方法会在多核的电脑上运行的更快,如果工作量被多个线程分开的话(使用Environment.ProcessorCount属性来侦测处理芯片的数量)。

     在C#程序可以通过以下2种方式使用多线程:明确地创建和运行多线程,或者使用.NET framework的暗中使用了多线程的特性——比如BackgroundWorker类, 线程池threading timer,远程服务器,或Web Services或ASP.NET程序。在应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。

何时不要使用多线程

     多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程本身并不复杂,复杂是的线程的交互作用,无论这种交互是否有意,都会带来较长的开发周期,间歇性和非重复性的bugs。因此,要么多线程的交互设计简单一些,要么就根本不使用多线程,除非你有强烈的重写和调试欲望。

  当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。在某些情况下,太多的I/O操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务块的多。稍后我们将实现生产者/耗费者 队列,它提供了上述功能。

 

你可能感兴趣的:(多线程)