C#Thread类多线程系列(一)入门

来源:互联网 发布:淘宝信誉良好怎么提升 编辑:程序博客网 时间:2024/05/20 12:48

文章系参考,英文原文网址请参考:http://www.albahari.com/threading/

作者: 想念、呐殇

本系列文章可以算是一本很出色的C#线程手册,思路清晰,要点都有介绍,看了后对C#的线程及同步等有了更深入的理解

1.基本概述

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

using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading; namespace AAAAA{    class Program    {        static void Main(string[] args)        {            Thread thread = new Thread(Test);            thread.Start();            for (int i = 0; i < 100; i++)            {                Console.Write("x");            }                 }        public static void Test()        {            for (int i = 0; i < 100; i++)            {                Console.Write("y");               }            Console.ReadKey();        }    }}
输出结果:

主线程创建了一个新线程“thread”,它运行了一个重复打印字母"y"的方法,同时主线程重复但因字母“x”。CLR分配每个线程到它自己的内存堆栈上,分别在各自的内存堆栈中创建,来保证局部变量的分离运行。

  • 当线程们引用了一些公用的目标实例的时候,他们会共享数据。  静态字段提供了另一种在线程间共享数据的方式
  • class Program
    {        bool done;       
             static void Main(string[] args)     
             {           
                   Program tt =new Program();       
                   new Thread(tt.Test).Start();       
                   tt.Test();    
              }       
            void Test()      
             {           
                 if (!done)           
                 {              
                       done = true;             
                       Console.WriteLine("共享数据");          
                 }            
                Console.ReadKey();     
            }  
  }

输出结果:


因为在相同的<B>ThreadTest</B>实例中,两个线程都调用了<B>Go()</B>,它们共享了<B>done</B>字段,这个结果输出的是一个"Done",而不是两个。

上述两个例子足以说明, 另一个关键概念, 那就是线程安全(或反之,它的不足之处! ) 输出实际上是不确定的:它可能(虽然不大可能) , "Done" ,可以被打印两次。然而,如果我们在Go方法里调换指令的顺序, "Done"被打印两次的机会会大幅地上升:

        void Test()        {            if (!done)            {                Console.WriteLine("共享数据");                done = true;            }            Console.ReadKey();        }
输出结果:

问题就是一个线程在判断if块的时候,正好另一个线程正在执行WriteLine语句——在它将done设置为true之前。

补救措施是当读写公共字段的时候,提供一个排他锁;C#提供了lock语句来达到这个目的:

   class Program    {        static bool done;        static object locker = new object();         static void Main(string[] args)        {            new Thread(Test).Start();            Test();        }        static void Test()        {            lock (locker)            {                 if (!done)                {                    Console.WriteLine("共享数据");                    done = true;                 }            }                Console.ReadKey();        }           }
当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以"Done"只被打印了1次。代码以如此方式在不确定的多线程环境中被叫做线程安全

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

          Thread.Sleep(TimeSpan.FromSeconds(30)); 
   一个线程也可以使用它的Join方法来等待另一个线程结束:
          Thread t = new Thread(Test);                    t.Start();          t.Join();  

 线程是如何工作的

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

   在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即通常在几微秒区间)

   在多核的电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。这几乎可以肯定仍然会出现一些时间切片, 由于操作系统的需要服务自己的线程,以及一些其他的应用程序。

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

   线程 vs. 进程

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

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

  何时使用多线程

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

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

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

   一个C#程序称为多线程的可以通过2种方式:明确地创建和运行多线程,或者使用.NET framework的暗中使用了多线程的特性——比如BackgroundWorker类, 线程池threading timer,远程服务器,或Web Services或ASP.NET程序。在后面的情况,人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即使有这样的事情;幸运的是,应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。

  何时不要使用多线程

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

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


2、创建和开始使用多线程
  线程用Thread类来创建, 通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托如何定义的:

     public delegate void ThreadStart(); 
调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法创建TheadStart委托:

    class Program    {        static void Main(string[] args)        {            new Thread(Test).Start();            Test();        }        static void Test()        {            Console.WriteLine("hello");            Console.ReadKey();        }    }

在这个例子中,新线程执行Test()方法,大约与此同时主线程也调用了Test(),结果是两个几乎同时hello被打印出来:

一个线程可以通过C#堆委托简短的语法更便利地创建出来,在这种情况,ThreadStart被编译器自动推断出来

        Thread t=new Thread(Test);        t.Start();

前台和后台线程

  线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。C#也支持后台线程,当所有前台线程结束后,它们不维持程序的存活。

  改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。 

如果程序被调用的时候没有任何参数,工作线程为前台线程,并且将等待ReadLine语句来等待用户的触发回车,这期间,主线程退出,但是程序保持运行,因为一个前台线程仍然活着。

   另一方面如果有参数传入Main(),工作线程被赋值为后台线程,当主线程结束程序立刻退出,终止了ReadLine。

   后台线程终止的这种方式,使任何最后操作都被规避了,这种方式是不太合适的。好的方式是明确等待任何后台工作线程完成后再结束程序,可能用一个timeout(大多用Thread.Join)。如果因为某种原因某个工作线程无法完成,可以用试图终止它的方式,如果失败了,再抛弃线程,允许它与 与进程一起消亡。(记录是一个难题,但这个场景下是有意义的)

   拥有一个后台工作线程是有益的,最直接的理由是它当提到结束程序它总是可能有最后的发言权。交织以不会消亡的前台线程,保证程序的正常退出。抛弃一个前台工作线程是尤为险恶的,尤其对Windows Forms程序,因为程序直到主线程结束时才退出(至少对用户来说),但是它的进程仍然运行着。在Windows任务管理器它将从应用程序栏消失不见,但却可以在进程栏找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行从开始或影响它的特性。

   对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。




0 0