《CLR Via C# 第3版》笔记之(十八) - 线程池

简介:

利用线程池可以对线程进行有效的控制,使得线程能够更好的协作。

在我们实际使用线程时,应当尽量使用线程池来构造线程,避免直接new一个线程。

主要内容:

  • 控制资源消耗
  • 提高线程性能
  • 取消运行中的线程 

1. 控制资源消耗

线程池(ThreadPool)启动线程的方法很简单,和上一篇直接new一个线程类似,也有带参数和不带参数两种。

1
2
public  static  bool  QueueUserWorkItem(WaitCallback callBack);
public  static  bool  QueueUserWorkItem(WaitCallback callBack, object  state);

 

其中的WaitCallback委托定义如下:

1
public  delegate  void  WaitCallback( object  state);

 

那么,线程池是如何有效的控制系统资源的消耗的呢?

它的原理非常简单:

  • 线程池中维护一个请求队列,当应用程序有异步的请求时,将此请求(比如请求A)发送到线程池。
  • 线程池将请求A放入请求队列中,然后新建一个线程(比如线程A)来处理请求A。
  • 请求A处理完成后,线程池不会销毁线程A,而是使用线程A来处理请求队列中的下一个请求(比如请求B)。
  • 当请求过多时,线程池才会再新建一些线程来加快处理请求队列中的请求。(注1
  • 当请求队列为空时,线程池会销毁一些空闲时间比较长的线程。(注2

注1:保证所有的请求由少量线程处理,减少系统资源的消耗,同时减少了线程新建,销毁的次数。

注2:空闲时间多长才销毁线程是由CLR决定的,不同版本的CLR这个时间可能不同。

 

下面通过一个例子来看看,线程池是如何节约系统资源的。

首先看看直接new线程时,系统资源是如何变化的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using  System;
using  System.Threading;
 
public  class  CLRviaCSharp_18
{
     static  void  Main( string [] args)
     {
         Console.WriteLine( "Main Thread" );
 
         for  ( int  i = 0; i < 100; i++)
         {
             Thread t = new  Thread(ThreadMethod);
             t.Start(i);
         }
 
         Console.ReadKey( true );
     }
 
     private  static  void  ThreadMethod( object  state)
     {
         Console.WriteLine( "This thread's state is {0}" , state);
         Thread.Sleep(2000);
     }
}

代码执行前,系统资源如下图。线程数和占用的内存见下图的红色框。

image

 

代码执行后,系统资源如下图。线程数和占用的内存见下图的红色框。

image

程序运行时,内存一下增加了100多MB,线程也增加了100多个。

 

在看看利用线程池来处理异步请求时,系统资源是如何变化的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using  System;
using  System.Threading;
 
public  class  CLRviaCSharp_18
{
     static  void  Main( string [] args)
     {
         Console.WriteLine( "Main Thread" );
 
         for  ( int  i = 0; i < 100; i++)
         {
             ThreadPool.QueueUserWorkItem(ThreadMethod, i);
         }
 
         Console.ReadKey( true );
     }
 
     private  static  void  ThreadMethod( object  state)
     {
         Console.WriteLine( "This thread's state is {0}" , state);
         Thread.Sleep(2000);
     }
}

代码执行前,系统资源如下图。线程数和占用的内存见下图的红色框。

image

 

代码执行后,系统资源如下图。线程数和占用的内存见下图的红色框。

image

程序运行时,最忙时(刚开始请求队列中线程较多时)多了10个线程,随着请求队列中线程的较少,线程池最终只维持了2,3个线程,消耗的资源最多也就10几MB。

 

通过以上的对比,我们可以看出如果应用程序都采用线程池来管理线程的话,确实可以减轻系统的负担,更有效的利用系统资源,保证多个应用程序可以同时运行。

否则一个应用程序占用太多资源,其他应用程序只能等待。

当然,通过上面两个例子,我们也发现利用线程池的程序执行时间比较长。这就是控制资源的结果,使得应用程序的异步请求逐步处理。

 

2. 提高线程性能

线程虽然轻量(和进程相比),但是毕竟也包含了一些信息(可参见上一篇中的线程开销),所以如果多了也会消耗很多系统资源。

而通过主线程来创建子线程时,主线程的上下文信息还得拷贝一份再传入子线程,比如上面的例子中新建了100个线程,就得将上下文信息拷贝100遍。

在有些情况下,子线程并没有用到主线程的上下文信息,此时,我们就可以通过阻止上下文信息的流动(主线程-->子线程)来提高线程的性能。

 

下面的例子演示如何阻止和恢复上下文信息的传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using  System;
using  System.Threading;
using  System.Runtime.Remoting.Messaging;
 
public  class  CLRviaCSharp_18
{
     static  void  Main( string [] args)
     {
         Console.WriteLine( "Main Thread" );
         CallContext.LogicalSetData( "Info" , "Main Thread's Context info." );
 
         // 阻止上下文的传递
         ExecutionContext.SuppressFlow();
         ThreadPool.QueueUserWorkItem(ThreadMethod, "Thread 1" );
 
         // 恢复上下文的传递
         ExecutionContext.RestoreFlow();
         ThreadPool.QueueUserWorkItem(ThreadMethod, "Thread 2" );
 
         Console.ReadKey( true );
     }
 
     private  static  void  ThreadMethod( object  state)
     {
         Console.WriteLine(state + "'s Context Info is : "  + CallContext.LogicalGetData( "Info" ));
     }
}

运行结果如下:

image

 

3. 取消运行中的线程

3.1 取消线程

对于长时间运行的线程,如果不提供取消操作,那么它就会一直占用系统资源,直至运行完成。

这样的用户体验很糟糕,所以对于有可能运行很长时间的线程,应该提供取消的操作供用户选择。

线程的取消主要使用CancellationTokenSource。

下面通过例子来演示如何取消一个线程的运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using  System;
using  System.Threading;
 
public  class  CLRviaCSharp_18
{
     static  void  Main( string [] args)
     {
         Console.WriteLine( "Main Thread" );
         CancellationTokenSource cts = new  CancellationTokenSource();
         ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts.Token));
 
         Console.WriteLine( "Press any key to cancel." );
         Console.ReadKey( true );
 
         // 取消线程的操作
         cts.Cancel();
 
         Console.ReadKey( true );
     }
 
     private  static  void  ThreadMethod(CancellationToken token)
     {
         do
         {
             // 线程取消前一直运行
             Console.WriteLine( "Now is : {0}" , DateTime.Now.ToString( "HH:mm:ss" ));
             Thread.Sleep(1000);
         } while  (!token.IsCancellationRequested);
 
         Console.WriteLine( "This thread is cancelled!" );
     }
}

只要输入任意键就可以取消线程。

 

3.2 取消线程时,注册一些额外的操作

主线程取消子线程后,可能需要进行一些操作来回收资源,释放对象等等。我们可以将这些操作注册到CancellationTokenSource的Token中,使得每个线程取消后都会执行这些操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using  System;
using  System.Threading;
 
public  class  CLRviaCSharp_18
{
     static  void  Main( string [] args)
     {
         Console.WriteLine( "Main Thread" );
         CancellationTokenSource cts = new  CancellationTokenSource();
         // 注册线程取消后的操作,执行操作的顺序与注册的顺利相反
         // 比如以下2个操作,第二个操作先执行
         cts.Token.Register(() => Console.WriteLine( "sub thread's object is disposed!" ));  // 后执行
         cts.Token.Register(() => Console.WriteLine( "sub thread's garbage is collected!" )); // 先执行
 
         ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts.Token));
 
         Console.WriteLine( "Press any key to cancel." );
         Console.ReadKey( true );
 
         // 取消线程的操作
         cts.Cancel();
 
         Console.ReadKey( true );
     }
 
     private  static  void  ThreadMethod(CancellationToken token)
     {
         do
         {
             // 线程取消前一直运行
             Console.WriteLine( "Now is : {0}" , DateTime.Now.ToString( "HH:mm:ss" ));
             Thread.Sleep(1000);
         } while  (!token.IsCancellationRequested);
 
         Console.WriteLine( "This thread is cancelled!" );
     }
}

运行结果如下:(在子线程打印了五次时间后,键盘输入任意按键) 

image

 

3.3 禁止取消线程

为了防止某些线程被意外取消,可以通过CancellationToken.None属性来禁止某些线程被取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using  System;
using  System.Threading;
 
public  class  CLRviaCSharp_18
{
     static  void  Main( string [] args)
     {
         Console.WriteLine( "Main Thread" );
         CancellationTokenSource cts = new  CancellationTokenSource();
 
         // 线程"Thread 1"不会被取消
         ThreadPool.QueueUserWorkItem(o => ThreadMethod(CancellationToken.None, "Thread 1" ));
         // 线程"Thread 2"会被取消
         ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts.Token, "Thread 2" ));
 
         Console.WriteLine( "Press any key to cancel." );
         Console.ReadKey( true );
 
         // 取消线程的操作
         cts.Cancel();
 
         Console.ReadKey( true );
     }
 
     private  static  void  ThreadMethod(CancellationToken token, object  state)
     {
         do
         {
             // 线程取消前一直运行
             Console.WriteLine(state + " Now is : {0}" , DateTime.Now.ToString( "HH:mm:ss" ));
             Thread.Sleep(1000);
         } while  (!token.IsCancellationRequested);
 
         Console.WriteLine(state + " is cancelled!" );
     }
}

运行结果如下:

image

 

3.4 关联多个取消操作

可以将多个取消操作关联起来,这样主线程可以很容易的检验是否发生了取消操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using  System;
using  System.Threading;
 
public  class  CLRviaCSharp_18
{
     static  void  Main( string [] args)
     {
         Console.WriteLine( "Main Thread" );
         CancellationTokenSource cts1= new  CancellationTokenSource();
         CancellationTokenSource cts2= new  CancellationTokenSource();
 
         ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts1.Token, "Thread 1" ));
         ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts2.Token, "Thread 2" ));
 
         // 将ctsLink与cts1和cts2关联起来
         CancellationTokenSource ctsLink = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
 
         Console.WriteLine( "Press any key to cancel." );
         Console.ReadKey( true );
 
         // 取消线程2的操作,cts2.IsCancellationRequested属性变为True
         // 同时ctsLink的IsCancellationRequested也变为True
         cts2.Cancel();
 
         // 通过检验ctsLink就可以知道是否有线程被取消
         if  (ctsLink.IsCancellationRequested)
             Console.WriteLine( "Some thread has been cancelled!" );
         else
             Console.WriteLine( "No thread has been cancelled!" );
 
         Console.ReadKey( true );
     }
 
     private  static  void  ThreadMethod(CancellationToken token, object  state)
     {
         do
         {
             // 线程取消前一直运行
             Console.WriteLine(state + " Now is : {0}" , DateTime.Now.ToString( "HH:mm:ss" ));
             Thread.Sleep(1000);
         } while  (!token.IsCancellationRequested);
 
         Console.WriteLine(state + " is cancelled!" );
     }
}
标签:  CLR via C#笔记


本文转自wang_yb博客园博客,原文链接:http://www.cnblogs.com/wang_yb/archive/2011/11/07/2239429.html,如需转载请自行联系原作者

目录
相关文章
|
2月前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
|
2月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
2月前
|
Java 调度 C#
C#学习系列相关之多线程(一)----常用多线程方法总结
C#学习系列相关之多线程(一)----常用多线程方法总结
|
2月前
|
C#
C#学习相关系列之多线程---ConfigureAwait的用法
C#学习相关系列之多线程---ConfigureAwait的用法
|
2月前
|
C#
C#学习相关系列之多线程---TaskCompletionSource用法(八)
C#学习相关系列之多线程---TaskCompletionSource用法(八)
|
2月前
|
Java C#
C#学习系列相关之多线程(五)----线程池ThreadPool用法
C#学习系列相关之多线程(五)----线程池ThreadPool用法
|
9天前
|
关系型数据库 C# 数据库
技术笔记:MSCL超级工具类(C#),开发人员必备,开发利器
技术笔记:MSCL超级工具类(C#),开发人员必备,开发利器
13 3
|
26天前
|
并行计算 算法 C#
C# Mandelbrot和Julia分形图像生成程序更新到2010-9-14版 支持多线程计算 多核处理器
此文档是一个关于分形图像生成器的介绍,作者分享了个人开发的M-J算法集成及色彩创新,包括源代码和历史版本。作者欢迎有兴趣的读者留言交流,并提供了邮箱(delacroix_xu@sina.com)以分享资源。文中还展示了程序的发展历程,如增加了真彩色效果、圈选放大、历史记录等功能,并分享了几幅精美的分形图像。此外,还提到了程序的新特性,如导入ini文件批量输出图像和更新一批图片的功能。文档末尾附有多张程序生成的高分辨率分形图像示例。
|
9天前
|
Java BI C#
技术笔记:SM4加密算法实现Java和C#相互加密解密
技术笔记:SM4加密算法实现Java和C#相互加密解密
10 0
|
1月前
|
大数据 C#
C#实现多线程的几种方式
C#实现多线程的几种方式