C#多线程(一)

时间:2022-07-11 21:43:07

一、定义与理解

1、定义

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

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

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

2、理解

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

  1. static void Main(string[] args)
  2. {
  3. Thread t = new Thread(Write);
  4. t.Start();
  5. Write();
  6. Console.ReadKey();
  7. }
  8. static void Write()
  9. {
  10. for (int i = 0; i < 5; i++)
  11. Console.Write("@");
  12. }

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

  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. static void Write()
  10. {
  11. if (!done)
  12. {
  13. done = true;
  14. Console.Write("@");
  15. }
  16. }

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

二、线程使用情形

  • 客户端应用程序保持对用户的响应:由于某些应用程序的特定需求,多线程程序一般用来执行需要非常耗时的操作,此时使用主线程创建工作线程在后台执行耗时的任务,而主线程保持运行,例如保持与用户的交互(更新进度条、显示提示文字等),这样可以防止由于程序耗时而被操作系统提示“无响应”而被用户强制关闭进程。
  • 及时处理请求:对于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或者遇到进程同步使用的锁机制而休眠等待。具体过程如下图所示:

C#多线程(一)C#多线程(一)

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

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

四、创建与运行设置

1、创建

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

  1. public delegate void ThreadStart();
  2. public delegate void ParameterizedThreadStart(object obj);

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

  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委托创建的。下面可以进行传递参数创建线程。

  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表达式:

  1. new Thread( () => Go("hello") );

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

  1. new Thread( () => {
  2. Console.Write("hello world!");
  3. ......
  4. }).Start();

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

  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. static void Write(object obj)
  8. {
  9. string msg = Convert.ToString(obj);
  10. Console.Write(msg);
  11. }

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

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

  1. int i = 0;
  2. int tmp = i;
  3. new Thread(()=>Write(tmp)).Start();
  4. int i = 1;
  5. int tmp = i;
  6. new Thread(()=>Write(tmp)).Start();
  7. ...

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

2、设置

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

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

  1. static void Main(string[] args)
  2. {
  3. Thread.CurrentThread.Name = "main";
  4. Thread t = new Thread(Go);
  5. t.Name = "worker";
  6. t.Start();
  7. Go();
  8. Console.ReadKey();
  9. }
  10. static void Go()
  11. {
  12. Console.WriteLine("from " + Thread.CurrentThread.Name);
  13. Console.WriteLine("background status: " + Thread.CurrentThread.IsBackground.ToString());
  14. }

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

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

  1. enum ThreadPriority{Lowest, BelowNormal , Normal, AboveNormal, Highest}

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

  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块中的新线程都与之无关。补救的方式是在每个线程处理的方法中加入自己的异常处理机制。

  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. Console.ReadKey();
  12. }
  13. static void Go()
  14. {
  15. try
  16. {
  17. throw null;
  18. }catch(Exception e){
  19. Console.Write(e.Message);
  20. }
  21. }

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

(后续继续探秘)

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