• C#多线程(一)


    C#多线程(一)

    一、定义与理解

    1、定义

    线程是操作系统分配CPU时间片的基本单位,每个运行的引用程序为一个进程,这个进程可以包含一个或多个线程。

    线程是进程中的执行流程,每个线程可以得到一小段程序的执行时间,在单核处理器中,由于切换线程速度很快因此感觉像是线程同时允许,其实任意时刻都只有一个线程运行,但是在多核处理器中,可以实现混合时间片和真实的并发执行。但是由于操作系统自己的服务或者其他应用程序执行,也不能保证一个进程中的多个线程同时运行。

    线程被一个CLR委托给操作系统的进程协调函数管理,确保所有线程都可以被分配适当的执行时间,同时保证在等待或阻止的线程不占用执行时间。

    2、理解

    线程与进程的关键区别是:进程是彼此隔离的,进程是操作系统分配资源的基本单位,而同一个进程中的多个线程是共享该进程内存堆区(Heap)的数据的,可以进行直接的数据共享。但是对于同一进程内的不同线程维护各自的内存栈(Stack),因此各线程的局部变量是隔离的。通过下面的例子可以看出。

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. static void Main(string[] args)  
    2. {  
    3.     Thread t = new Thread(Write);  
    4.     t.Start();  
    5.     Write();  
    6.     Console.ReadKey();  
    7. }  
    8.   
    9. static void Write()  
    10. {  
    11.     for (int i = 0; i < 5; i++)  
    12.         Console.Write("@");  
    13. }  


    结果输出的是10个“@”,在两个线程中都有局部变量i,是彼此隔离的。但是对于共享的引用变量和静态数据,多个线程是会产生不可预知的结果的,这里共享的数据也就是“临界数据”,从而引发了线程安全的概念。

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. static bool done;  
    2. static void Main(string[] args)  
    3. {  
    4.     Thread t = new Thread(Write);  
    5.     t.Start();  
    6.     Write();  
    7.     Console.ReadKey();  
    8. }  
    9.   
    10. static void Write()  
    11. {  
    12.     if (!done)  
    13.     {  
    14.         done = true;  
    15.         Console.Write("@");  
    16.     }  
    17. }  


    这里输出的只有一个字符,但是很可能在极少数情况下会出现输出两个字符的情况,而且这是不可预知的。但是,对于共享的引用就不会出现这种情况。

    二、线程使用情形

    • 客户端应用程序保持对用户的响应:由于某些应用程序的特定需求,多线程程序一般用来执行需要非常耗时的操作,此时使用主线程创建工作线程在后台执行耗时的任务,而主线程保持运行,例如保持与用户的交互(更新进度条、显示提示文字等),这样可以防止由于程序耗时而被操作系统提示“无响应”而被用户强制关闭进程。
    • 及时处理请求:对于Web应用程序,主线程相应客户端用户的请求,返回数据的同时,工作线程从数据库选出最新数据。这样可以对某些实时性要求高的应用非常有效,同时可以查询工作量被单独线程分开执行,特别是在多核处理器上,可以提高程序的性能。同时对于服务器需要处理多种类型的请求的时候,如ASP.NET、WCF、Remoting等,从而可以实现并发响应。
    • 防止一个线程长时间没有响应而阻塞CPU来提高效率:例如WebService服务,对于没有用户交互界面的访问,在等待提供webservice服务(比较耗时)的电脑的响应的同时可以执行其他工作,以提高效率。

    问题:

    多线程的问题是使程序中的多个线程的交互变得过于复杂,会带来较长的开发时间和间歇性或非重复性的bug。同时线程数目不能太多,否则频繁的分配和切换线程会带来资源和CPU的开销,一般有一个到两个工作线程就足够。

    三、C#中的线程

    C#中主要使用Thread类进行线程操作,位于System.Threading命名空间下,提供了一系列进行多线程编程的类和接口,有线程同步和数据访问的Mutex、Monitor、Interlocked和AutoResetEvent类,以及ThreadPool类和Timer类等。

    首先使用new Thread()创建出新的线程,然后调用Start方法使得线程进入就绪状态,得到系统资源后就执行,在执行过程中可能有等待、休眠、死亡和阻塞四种状态。正常执行结束时间片后返回到就绪状态。如果调用Suspend方法会进入等待状态,调用Sleep或者遇到进程同步使用的锁机制而休眠等待。具体过程如下图所示:

    Thread类主要用来创建并控制线程,设置线程的状态、优先级等。创建线程的时候使用ThreadStart委托或者ParameterizedThreadStart委托来执行线程所关联的部分代码(也就是工作线程的运行代码)。

    Thread类属性
    属性说明            
    CurrentThread 获取当前正在运行的线程
    IsAlive 获取当前线程的执行状态
    Name 获取或设置线程的名称
    Priority 获取或设置线程的优先级
    ThreadState 获取包含当前线程状态的值
    Thread类常用方法
    方法说明
    Abort 调用此方法的线程引发ThreadAbortException
    终止线程
    Join 阻止调用线程,知道某个线程终止时为止
    Resume 继续已挂起的线程
    Sleep 将线程阻止指定的毫秒数
    Start 将线程安排被进行执行
    Suspent 挂起线程,如果已经挂起则不起作用

    四、创建与运行设置

    1、创建

    使用Thread类的构造函数创建线程的时候,需要传递一个新线程开始执行的代码块,提供了使用无参数的TheadStart委托和带有一个参数的ParameterizedTheadStart委托。他们的定义如下:

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. public delegate void ThreadStart();  
    2. public delegate void ParameterizedThreadStart(object obj);  

    任何时候C#使用上述两个委托中的一个自动进行线程的创建。

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. static void Main()  
    2. {  
    3.     Thread t = new Thread(new TheadStart(Go));  
    4.     t.Start();  
    5.     Go();  
    6. }  
    7. static void Go()  
    8. {  
    9.     Console.Write("hello!");  
    10. }  

    上述方式不传递参数,可以使用new Thead(Go)的方式直接创建,此时C#会在编译时自动匹配使用的是ThreadStart委托创建的。下面可以进行传递参数创建线程。

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. static void Main()  
    2. {  
    3.     Thread t = new Thread(Go);  
    4.     t.Start("hello");  
    5.     Go();  
    6. }  
    7. static void Go(object msg)  
    8. {  
    9.     string message = (string)msg;  
    10.     Console.Write(message);  
    11. }  

    此时实际在编译时使用的new Thread(new ParameterizedThreadStart(Go("hello")))创建的,上述使用Start方法传递的参数会默认采用这种方式构建。

    第二种方法是使用Lambda表达式:

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. new Thread( () => Go("hello") );  

    第三种方法是使用匿名方法:

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. new Thread( () => {  
    2.     Console.Write("hello world!");  
    3.     ......  
    4. }).Start();  

    注意问题:使用Lambda表达式的时候会存在变量捕获的问题,如果捕获的变量是共享的,会出现线程不安全的问题。看下面的例子:

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. static void Main(string[] args)  
    2. {  
    3.     for (int i = 0; i < 10; i++)  
    4.         new Thread(() => Write(i)).Start();  
    5.     Console.ReadKey();  
    6. }  
    7.   
    8. static void Write(object obj)  
    9. {  
    10.     string msg = Convert.ToString(obj);  
    11.     Console.Write(msg);  
    12. }  

    上述由于使用Lambda表达式传递参数,在for循环的作用域内,新建的十个线程共享了局部变量i,传递进入i参数可能被多个线程已经修改,因此每次输出结果都是不确定的,两次结果如下:



    上述问题,可以使用在循环体内使用一个tmp变量保存每次的变量i值,这样输出的就是0到9这十个数。因为使用tmp变量之后的代码可以用下面的来理解:

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. int i = 0;  
    2. int tmp = i;  
    3. new Thread(()=>Write(tmp)).Start();  
    4.   
    5. int i = 1;  
    6. int tmp = i;  
    7. new Thread(()=>Write(tmp)).Start();  
    8.   
    9. ...  

    上述使用Lambda表达式传递参数的问题,使用Start方法传递参数也会出现这样的线程不安全的问题,需要使用特殊的线程同步手段进行避免。

    2、设置

    通过使用Thread.CurrentThread属性获取正在运行的线程对象。每个线程都有一个Name属性,可以设置和修改,但是只能设置一次。这样可在调试窗口看到每个线程的工作状态,便于调试。

    线程有前台和后台之分,可以使用IsBackground属性设置,但是这个属性与线程的优先级是没有关联的。前台线程只要有一个在运行应用程序就在运行,当没有前台线程运行后应用程序终止,也就是在任务管理器中的程序一栏中没有了此程序,但是此时后台线程任然运行直到其完成操作结束,因此在任务管理器的进程一栏中会找到。

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. static void Main(string[] args)  
    2. {  
    3.     Thread.CurrentThread.Name = "main";  
    4.   
    5.     Thread t = new Thread(Go);  
    6.     t.Name = "worker";  
    7.     t.Start();  
    8.   
    9.     Go();  
    10.     Console.ReadKey();  
    11. }  
    12. static void Go()  
    13. {  
    14.     Console.WriteLine("from " + Thread.CurrentThread.Name);  
    15.     Console.WriteLine("background status: " + Thread.CurrentThread.IsBackground.ToString());  
    16. }  


    前台或主线程明确等待任何后台线程完成后再结束才是最好的方式,这大多使用Join方式实现,如果某个工作线程无法实现,可以先终止它,如果失败再抛弃线程,从而与进程一起消亡。

    线程的优先级使用Priority设置或获取,只有在运行时才有作用。分为5个级别:

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. enum ThreadPriority{Lowest, BelowNormal , Normal, AboveNormal, Highest}  

    线程优先级设置高并不意味着能执行实时的工作,这受限于所属进程的级别,要执行实时的工作需要提示System.Diagnostics命名空间下的Process级别:

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. using (Process p = Process.GetCurrentProcess())  
    2.   p.PriorityClass = ProcessPriorityClass.High;  

    设置为High是一个短暂的最高优先级别,如果设置为Realtime,那么将让操作系统不然该进程被其他进程抢占,因此如果此程序一旦出现故障将耗尽操作系统资源。因此设置为High就是被认为最高和最有用的进程级别了。

    对于有用户界面的程序不适合提升进程级别,因为界面UI的更新需要耗费CPU很多时间,从而拖慢电脑。最好的方式是实时工作和用户界面使用不同的进程,有不同的进程优先级,通过Remoting或者共享内存的方式进行进程通信。

    线程执行先运行最高优先级的线程,高优先级的线程执行完之后才开始执行低优先级的线程。

    3、休眠

    Thread.Sleep(int ms); Thread.Sleep(TimeSpan timeout);

    上述方法为Thread类的两个静态方法,用来阻止当前线程指定的时间。

    4、终止

    使用Abort和Join两个方法实现。Join会等待另一个线程执行完后再执行。而Abort会引发ThreadAbortException异常,同时可以传递一个终止的参数信息。

    Thread.Abort();或者Thread.Abort(Object  stateInfo)。

    5、异常处理

    每个线程都有独立的执行路径,因此放在try/catch/finally块中的新线程都与之无关。补救的方式是在每个线程处理的方法中加入自己的异常处理机制。

    [csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
     
    1. static void Main(string[] args)  
    2. {  
    3.     try  
    4.     {  
    5.         new Thread(Go).Start();  
    6.     }  
    7.     catch (Exception ex)  
    8.     {  
    9.         Console.Write(ex.Message);  
    10.     }  
    11.       
    12.     Console.ReadKey();  
    13. }  
    14. static void Go()  
    15. {  
    16.      try  
    17.     {  
    18.           throw null;  
    19.     }catch(Exception e){  
    20.           Console.Write(e.Message);  
    21.     }  
    22. }  

    上述处理过程在单独的线程运行中进行异常处理是可以被捕获到的。同时任何线程内的未处理的异常都会导致整个程序关闭,对于WPF和WinForm程序中的全局异常仅仅在主界面线程执行,对于工作线程的异常需要手动处理。有三种情况可以不用处理工作线程的异常:异步委托、BackgroundWroker、Task Parallel Library。

    (后续继续探秘)

    参考:http://www.albahari.com/threading/

     
     
    分类: C#.Net
    标签: C#多线程
  • 相关阅读:
    Opencv(1)介绍篇
    植被覆盖度制图
    GIS应用开发AO(2)-空间分析ITopologicalOperate
    GIS应用开发AO(1)_普通几何图形绘制
    初识机器学习-人脸识别
    ArcGIS API for javascript4.3——RouteTask
    javascript学习(1)随机点名应用
    生活感悟之六
    生活感悟之五
    生活感悟之四
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3629586.html
Copyright © 2020-2023  润新知