代码改变世界

多线程基本概念介绍(Introduction and Concepts)

2010-12-16 13:09  RyanXiang  阅读(1680)  评论(4编辑  收藏  举报

 

 

一、多线程基本概念介绍(Introduction and Concepts

 

     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”:

 

飞信截屏未命1名

 

    一旦一个线程启动了,该线程的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名

    因为局部变量都有独立的栈空间,所以打印出来的?号是十个。

 

    如果线程之间有共同的引用,则它们共享数据。例如下面代码:

 

 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资源的!

1、Join and Sleep

 

    你可以通过调用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()也有同样的功能。

2、线程是如何工作的

    在CLR中,线程是被一个叫做线程调度器的委托函数管理的。线程调度器保证所有的线程都分配用来执行的CPU时间,并保证阻塞的线程不占用CPU资源。

 

3、线程VS进程

    线程和运行在你操作系统上的应用程序的进程有很大的相似性,就像多个进程并行的运行在计算机上一样,多个线程并行的运行在一个独立的进程上。进程与进程之间是完全隔离的,而线程只是在一定程度上是隔离的。运行在一个应用程序上的线程是共享堆内存的。这很大程度上也让线程变的很好用,比如:一个线程可以在后台取数据,另一个线程可以在前台呈现。

 

4、线程的正确使用和误用

     多线程有很多用途,下面为常见的:

1、维护用户界面,让其可以及时响应。

     另起一个线程来执行比较耗时的任务,让UI线程(Main线程),可以持续响应用户操作。

2、高效的利用CPU。

   多线程是非常有用的,当一个线程等待另一台计算机或硬件的响应时。当一个线程执行某项任务而发生阻塞时,其他的线程能充分利用其他已经空闲的计算机。

3、并行程序设计。

   在执行大量计算密集型任务时,通过采取分治策略,可以把任务量均摊在不同的线程上,这样可以加快任务的执行速度。

4、推理性执行(预测执行)。

   在多核处理器中,有时你能通过预测需要处理的内容来改善性能。LINQPad使用这个技术加速创建新的查询。并行运行多个不同的算法来解决同一个任务。

允许多个请求被同步处理.这在你不知道哪个算法效率更高的情况下是非常有效的。
(译者注:关于推理性执行请参考百度百科)

5、让请求可以并行执行。

   在web服务器上,多个客户端请求同时到达,因此需要并行处理(如果你使用ASP.NET,WCF,Web Services,或者Remoting等技术,FrameWork会自动的为你创建线程)。这在客户端也是非常有用的(例如P2P的网络处理或者是来自一个用户的多个请求)。

 

    多线程也会产生额外的问题。最大的问题是多线程会在一定程度上增加程序的复杂度。拥有很多线程就其本身来言并不复杂;复杂的是线程之间的相互作用和影响(特别是通过共享数据)。这些应用无论是否是有意的相互影响,都会导致较长的开发周期和持续出现不易发现的bugs。出于这个原因,应使这种影响减到最少,尽可能避免过度设计。本书主要是讨论如何处理这些复杂问题;而对于消除相互影响说的很少。

 

    一个好的策略是把多线程的逻辑封装到一个可复用的类里,这个类可以独立地被检查和测试。Framework本身提供了很多高级别的封装类,这些我们将会在后边介绍到. Threading会在调度和转换线程时消耗资源和CPU时间(当活动线程超过CPU 核数时),而且还有线程的创建和释放成本。多线程不是总是在加速你的应用程序 - 如果你使用的过多或者不恰当。例如,在磁盘和I/O被频繁调用时,使用两个工作线程按顺序运行任务,就比用10个线程同时运行要快。