zoukankan      html  css  js  c++  java
  • 《CLR Via C# 第3版》笔记之(十八) 线程池

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

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

    主要内容:

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

    1. 控制资源消耗

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

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

    其中的WaitCallback委托定义如下:

    public delegate void WaitCallback(object state);

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

    它的原理非常简单:

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

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

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

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

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

    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多个。

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

    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遍。

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

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

    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。

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

    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中,使得每个线程取消后都会执行这些操作。

    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属性来禁止某些线程被取消。

    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 关联多个取消操作

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

    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!");
        }
    }
  • 相关阅读:
    蓝桥杯 猴子分苹果
    蓝桥杯 王、后传说 dfs
    蓝桥杯 C*++ Calculations 贪心
    python random模块 分类: python python Module python基础学习 2013-06-26 12:06 383人阅读 评论(0) 收藏
    去除列表中不重复的元素 分类: python 小练习 2013-06-25 14:59 245人阅读 评论(0) 收藏
    去除文件每行的第一个字符 分类: python 2013-06-24 15:03 414人阅读 评论(0) 收藏
    用户输入内容长度限制的异常 分类: python异常 2013-06-24 10:48 335人阅读 评论(0) 收藏
    使用python下载文件 分类: python python下载 2013-06-22 16:58 277人阅读 评论(0) 收藏
    猜数字 分类: python 小练习 python基础学习 2013-06-20 15:16 160人阅读 评论(0) 收藏
    Linux系统中ls命令用法详解 分类: ubuntu 2013-06-20 14:29 261人阅读 评论(0) 收藏
  • 原文地址:https://www.cnblogs.com/wang_yb/p/2239429.html
Copyright © 2011-2022 走看看