• 在某些情况下,我们希望A中的代码块(B)同步的执行,即同一时刻只有一个线程执行代码块B,这就需要用到(lock)。lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。它可以把一段代码定义为互斥段...

            在多线程编程中,可能会有许多线程并发的执行一段代码(代码块A),以提高执行效率。在某些情况下,我们希望A中的代码块(B)同步的执行,即同一时刻只有一个线程执行代码块B,这就需要用到锁(lock)。lock 关键字可以用来确保代码块完成运行,而不会被其他线程中断。它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。

    其写法如下:

    Object  locker = new Object();

    lock(locker)

    {

          B    //同步执行的代码

    }

    其相当于如下代码:

    //System.Threading.Moniter.enter(Object),Moniter提供同步访问对象的机制,enter方法在指定对象上获取排他锁,之后其他线程不能再次获取,直到释放

    Monitor.enter(locker); 
    {
          B
    }
    Monitor.exit(locker); 
    //exit(Object)释放指定对象上的排他锁。

    lock 语句的表达式必须表示一个引用类型的值永远不会为 lock 语句中的表达式执行隐式装箱转换,因此,如果该表达式表示的是一个值类型的值,则会导致一个编译时错误。

    C#中引用类型主要有:数组、类、接口、委托、object、字符串,但是最好不要锁字符串;使用lock同步时,应保证lock的是同一个对象,而给字符串变量赋值并不是修改它,而是重新创建了新的对象,这样多个线程以及每个循环之间所lock的对象都不同,因此达不到同步的效果。常用做法是创建一个object对象,并且永不赋值。

    下面举个简单的例子,一群苦逼程序员被逼捐款,每人100:




    展开全文
  • C#多线程中的使用

    2015-07-22 10:03:17
    这里提到是一个比较简单的 -- lock。 lock是对monitor中的两个函数enter和exit的封装。 当时项目的模式是这样的:有一个类中有个共享的资源(List),这个类会开辟两个线程分别对它进行读和写操作,而且这个类会...

    最近的项目中涉及到实时数据的处理,经常会使用多线程访问共享资源。如果处理不当,资源未能正确在各个线程中同步的话,计算结果将会出现错误。


    关于资源同步最常用的技术就是加锁。这里提到是一个比较简单的锁 -- lock。 lock是对monitor中的两个函数enter和exit的封装。


    当时项目的模式是这样的:有一个类中有个共享的资源(List),这个类会开辟两个线程分别对它进行读和写操作,而且这个类会有多个实例,每个实例都会在一个线程中。


    其工作模式如下图所示:




    这里的写线程将会动态改变list里面的值已经值对应的index,而读的线程会先用一个匹配的函数找出需要的值对应的index,然后根据index取出其中的值加以计算。


    在这种模式中,如果读线程去访问list数据时,写线程对list进行了修改,就会导致结果出错。因此我们需要引入锁的机制。那么问题来了,我们是要在读的线程中加锁,还是在写的线程中加锁。而且加锁的话,是采用什么方式?


    以下有几种方式,将会一一分析其对错。


    1. 使用对象本身作为锁对象,对写线程进行加锁


    使用这种方式,在写线程中,list将会被锁定,执行完毕之后释放锁。然而这种模式还是会得出错误结果,假设在读线程中先对list进行了匹配并取出了需要的index,这时写线程获取了锁,并对list进行了修改,之后释放list,读线程根据index查找list对应的值,然而此时list已被更改。


    2. 使用对象本身作为锁对象,对读线程进行加锁


    使用这种方式,在读线程中,list将会被锁定,所以读的过程中list都将保持不变。


    除了使用对象本身作为锁对象之外,还可以使用其他的引用类型,比如System.Object,此时线程不会访问该类型的任何属性和方法,改对象的作用仅仅是协调各个线程。


    3. 使用System.Object作为锁对象


    根据上面的分析,我们将只对读线程进行加锁。但是这时又有两种情况:object对象可以定义成实例对象和静态对象。目前的这种模式适用于把锁加在实例对象的object上。因为使用object作为锁对象时,操作流程时占有A,操作B,释放A。若将object定义为静态对象,此时对object进行加锁,将协调的是类的每个实例所产生的对象,对应的list也必须是静态的。而这里我们需要协调的是某一个实例中的两个线程,因此需要将object定义为实例对象。


    综上所述,有两种方式可以实现以上模式的线程同步,一种是使用对象本身作为锁对象,对读线程进行加锁,另一种是使用实例对象的System.Object作为锁对象,对读线程进程加锁。



    展开全文
  • C#线程池用法

    2012-01-28 14:59:27
    C#编程语言中,使用线程池可以并行地处理工作,当强制线程和更新进度条时,会使用内建架构的ThreadPool类,为批处理使用多核结构,这里我们来看在C#编程语言中一些关于来自System.Threading的ThreadPool的用法的...

    译自:http://www.dotnetperls.com/threadpool

    C#编程语言中,使用线程池可以并行地处理工作,当强制线程和更新进度条时,会使用内建架构的ThreadPool类,为批处理使用多核结构,这里我们来看在C#编程语言中一些关于来自System.ThreadingThreadPool的用法的例子

    介绍

    .NET Framework提供了包含ThreadPool类的System.Threading 空间,这是一个可直接访问的静态类,该类对线程池是必不可少的。它是公共“线程池”设计样式的实现。对于后台运行许多各不相同的任务是有用的。对于单个的后台线种而言有更好的选项。

    线程的最大数量。这是完全无须知道的。在.NETThreadPool的所有要点是它自己在内部管理线程池中线程。多核机器将比以往的机器有更多的线程。微软如此陈述“线程池通常有一个线程的最大数量,如果所有的线程都忙,增加的任务被放置在队列中直到它们能被服务,才能作为可用的线程。”

    用法位置

    线程池类型能被用于服务器和批处理应用程序中,线程池有更廉价的得到线程的内部逻辑,因为当需要时这些线程已被形成和刚好“连接”,所以线程池风格代码被用在服务器上。

    MSDN表述:“线程池经常用在服务器应用程序中,每一个新进来的需求被分配给一个线程池中的线程,这样该需求能被异步的执行,没有阻碍主线程或推迟后继需求的处理。”

    MSDN 参考

    ThreadPool  VS  BackgroundWorker

    如果你正在使用Windows窗体,宁可使用BackgroundWorker来对付那些更简单的线程需求,BackgroundWorker在网络访问和其他一些简单的事情方面做得很好。但对于多处理器的批处理来说,你需要ThreadPool。

    BackgroundWorker 教程

    当你的程序要批处理时,考虑线程池

    当你的程序产生很多(3个以上)线程时,考虑线程池

    当你的程序使用Windows窗体时,考虑后台执行。

    线程要考虑的事 同样,如何使用线程的细节能帮助发现最好的代码。下面比较线程情形和哪个类是最好的。

       你需要一个额外的线程   使用后台执行

       你有许多短期的线程     使用线程池 

    需求

    线程很重要,但对于那些不会花很长时间来执行且只做一件事情的大多数应用程序来说却并不重要的。线程对于界面可用性不是很重要的的应用程序而言也不是很重要,要尽量避免使用线程(译者注:比如进度条不是很重要的应用程序)。

    连接方法

    可使用QueueUserWorkItem连接方法(methods)到线程池。方法要运行在线程上,则必须把它连接到QueueUserWorkItem。如何实现呢?必须使用WaitCallback。在MSDN中,WaitCallback被描述成当线程池执行时要被调用的委托回调方法,是回调它的参数的委托。

    WaitCallback

    只需指定“new WaitCallback”语句作为ThreadPool.QueueUserWorkItem的第一个参数来使用WaitCallback.不需要任何其他的代码来使用这方法生效。

    使用WaitCallback[c#]的例子

    void Example()
    
    {
    
        // 连接 ProcessFile 方法到线程池.
    
        //注意: 'a' 是一个作为参数的对象
    
        ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile), a);
    
    }
    
    private void ProcessFile(object a)
    
    {
    
        // 我被连接到线程池通过 WaitCallback.
    
    }


    参数

    我们能通过定义一个特定的类并把一些重要的值放在类里面来使用参数,那么,方法接收了对象,就能通过对象向方法传递多个参数了。以下是一个早期的例子。

    使用带参数QueueUserWorkItem 的例子[c#]

    //指定作为线程池方法的参数的类
    
    class ThreadInfo
    
    {
    
        public string FileName { get; set; }
    
        public int SelectedIndex { get; set; }
    
    }
    
    class Example
    
    {
    
        public Example()
    
        {
    
    // 声明一个新的参数对象
    
    ThreadInfo threadInfo = new ThreadInfo();
    
    threadInfo.FileName = "file.txt";
    
    threadInfo.SelectedIndex = 3;
    
    //发送自定义的对象到线程方法
    
    ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile), threadInfo);
    
        }
    
        private void ProcessFile(object a)
    
        {
    
    ThreadInfo threadInfo = a as ThreadInfo;
    
    string fileName = threadInfo.FileName;
    
    int index = thread.SelectedIndex;
    
        }
    
    }


    发生了什么事?我们发送两个值给这个线程化的ProcessFile方法,它需要知道文件名和选择索引,而我们在这个对象中把参数都发送了给它。

    进度条

    能通过从设计器中右边的工具盒面板中增加Windows窗体控件到你的窗体程序来使用进度条并设置 progressBar1.Value, progressBar1.Minimum 和progressBar1.Maximum。 progressBar1.Value是最小值和最大值中间的位置,以下代码用来初始化进度条:

    设置进度条的例子 [C#]

    //设置进度条的长度.
    
    // 这里我们有6个单位来完成,所以6是最大值。
    
    // 最小值通常是0
    
    progressBar1.Maximum = 6; // 或其他数字
    
    progressBar1.Minimum = 0;


    进度条位置 你的进度条中的有颜色部分是当前值与最大值的百分比。所以,如果最大值是6,而值是3即表示做完了一半。

    ProgressBar 例子 (Windows Forms)

    在进度条中调用Invoke(援引)

    让我们看如何在进度条实例中使用Invoke方法。遗憾的是,你不能在辅助线程中访问Windows控件,因为UI线程是分离的,必须使用委托(delegate)Invoke到进度条。

    请求Invoke(调用)的例子[C#]

    public partial class MainWindow : Form
    
    {
    
    // 这是运行在UI线程来更新条的委托
    
     public delegate void BarDelegate();
    
    //该窗体的构造器(由Visual Studio自己产生)
    
        public MainWindow()
    
        {
    
    InitializeComponent();
    
        }
    
    //当按下按钮,启动一个新的线程
    
        private void button_Click(object sender, EventArgs e)
    
        {
    
    // 设定进度条的长度.
    
    progressBar1.Maximum = 6;
    
    progressBar1.Minimum = 0;
    
    // 给线程传递这些值.
    
    ThreadInfo threadInfo = new ThreadInfo();
    
    threadInfo.FileName = "file.txt";
    
    threadInfo.SelectedIndex = 3;
    
    ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessFile), threadInfo);
    
        }
    
    // 运行在后台线程上的东西
    
     private void ProcessFile(object a)
    
        {
    
    // (省略)
    
    // 使用'a'做一些重要的事.
    
    // 告诉UI 我们已经完成了.
    
    try
    
    {
    
        // 在窗体中调用委托 .
    
        this.Invoke(new BarDelegate(UpdateBar));
    
    }
    
    catch
    
    {
    
               //当一些问题发生后我们能使程序恢复正常
    
    }
    
        }
    
        //更新进度条.
    
        private void UpdateBar() 
    
        {
    
    progressBar1.Value++;
    
    if (progressBar1.Value == progressBar1.Maximum)
    
    {
    
        // 结束了,进度条满了.
    
    }
    
        }
    
    }


    委托语法 以上代码的开始处,可以看到声明 UpdateBar 的委托。它告诉Visual Studio C# 需要来使用这个作为对象的方法。

    更多需要的工作 以上程序演示了如何设定进度条的最大值和最小值,如何在工作完成后“Invoke”委托方法来增加进度条的大小。

    在调试器中的线程

     这儿要显示如何在Visual Studio的调试器中查看线程。一旦你有一个运行的程序,你能采取这些措施来可视化你的线程。首先,以调试模式打开你的线程应用程序,一旦你的应用程序运行在调试器,告知它去做它的工作而且运行这些线程,通过绿色箭头运行调试器,当线程正在运行,在工具条中单击“pause"按钮。

    下一步 调试>窗口>线程.该菜单项将打开一个类似下图的窗口,你能看见有多少线程正在线程池中运行。

     

    四个辅助线程 上图显示了共有10个线程,但只有四个辅助线程(Worker Thread)在程序中被分配给MainWindow.ProcessFile.

    约束辅助线程

    如果你有一个双核或四核系统,你将考虑最多两个四个很费力的线程。我们能在运行的线程中保持一个_threadCount 字段并且跟踪它的数值。用这个线程计数字段,你将需要在C#语言中使用一个锁来避免造成这个字段读和写的错误,锁保护你的线程被其他线程所改变。

    计数线程的例子 [C#]

    // 锁住这个对象.
    
    readonly object _countLock = new object();
    
    private void ProcessFile(object argument)
    
    {
    
    // 约束辅助线程的数量
    
    while (true)
    
        {
    
    lock (_countLock)
    
    {
    
        if (_threadCount < 4)
    
        {
    
    // Start the processing
    
    _threadCount++;
    
    break;
    
        }
    
    }
    
    Thread.Sleep(50);
    
        }
    
        // Do work...
    
    }


    我们看到什么 以是代码是异步执行的方法。只有其他辅助线程少于4个时它才会工作。这对于一个四核机器是好的。请看描述锁声明的更多上下文的文章

    Lock Statement

    控制线程计数器

    你可以在ThreadPool上使用SetMinThreads 来在连发活动中提高吞吐量和性能。以下是关于可使用的最佳的最小线程数量的材料。

    ThreadPool.SetMinThreads Method

    总结

    我们了解了如何在C#程序中使用线程池来有效管理多个线程,在Windows 窗体应用程序的进度条和用户界面中能给人留很深印象并且也不难实现。然而,线程带来了很多的复杂性并导致漏洞,线程池是一个有用的简化,但它仍然是困难的。

    线程概要 

    展开全文
  • C#线程池

    2011-06-23 18:28:00
    摘要深度探索 Microsoft .NET提供的线程池, 揭示什么情况下你需要用线程池以及 .NET框架下的线程池是如何实现的,并告诉你如何去使用线程池。 内容介绍.NET中的线程池线程池中执行的函数使用定时器同步对象的执行...

    摘要

    深度探索 Microsoft .NET 提供的线程池, 揭示什么情况下你需要用线程池以及 .NET 框架下的线程池是如何实现的,并告诉你如何去使用线程池。

     

    内容

    介绍

    .NET中的线程池

    线程池中执行的函数

    使用定时器

    同步对象的执行

    异步I/O操作

    监视线程池

    死锁

    有关安全性

    结束

     

    介绍

             如 果你有在任何编程语言下的多线程编程经验的话,你肯定已经非常熟悉一些典型的范例。通常,多线程编程与基于用户界面的应用联系在一起,它们需要在不影响终 端用户的情况下,执行一些耗时的操作。取出任何一本参考书,打开有关线程这一章:你能找到一个能在你的用户界面中并行执行数学运算的多线程示例吗?

    我的目的不是让你扔掉你的书,不要这样做!多线程编程技术使基于用户界面的应用更完美。实际上, Microsoft .NET 框架支持在任何语言编写的窗口下应用多线程编程技术,允许开发人员设计非常丰富的界面,提供给终端用户一个更好的体验。但是,多线程编程技术不仅仅是为了用户界面的应用,在没有任何用户界面的应用中,一样会出现多个执行流的情况。

    我们用一个“硬件商店”的客户 / 服务器应用系统作为例子。客户端是收银机,服务端是运行在仓库里一台独立的机器上的应用系统。你可以想象一下,服务器没有任何的用户界面,如果不用多线程技术你将如何去实现?

    服务端通过通道( http, sockets, files 等等)接收来自客户端的请求并处理它们,然后发送一个应答到客户端。图 1 显示了它是如何运作的。

     

    1 单线程的服务端应用系统

    为了让客户端的请求不会遗漏,服务端应用系统实现了某种队列来存放这些请求。图 1 显示了三个请求同时到达,但只有其中的一个被服务端处理。当服务端开始执行 "Decrease stock of monkey wrench," 这个请求时,其它两个必须在队列中等待。当第一个执行完成后,接着是第二个,以此类推。这种方法普遍用于许多现有的系统,但是这样做系统的资源利用率很低。假设 decreasing the stock ”请求修改磁盘上的一个文件,而这个文件正在被修改中, CPU 将不会被使用,即使这个请求正处在待处理阶段。这类系统的一个普遍特征就是低 CPU 利用时间导致出现很长的响应时间,甚至是在访问压力很大的环境里也这样。

             另外一个策略就是在当前的系统中为每一个请求创建不同的线程。当一个新的请求到达之后,服务端为进入的请求创建一个新线程,执行结束时,再销毁它。下图说明了这个过程:

             2 :多线程服务端应用系统

    就像如图 2 所示的那样。我们有了较高的 CPU 利 用率。即使它已经不再像原来的那样慢了,但创建线和销毁程也不是最恰当的方法。假设线程的执行操作不复杂,由于需要花额外的时间去创建和销毁线程,所以最 终会严重影响系统的响应时间。另外一点就是在压力很大的环境下,这三个线程会给系统带来很多的冲击。多个线程同时执行请求处理将导致 CPU 的利用率达到 100% ,而且大多数时间会浪费在上下文切换过程中,甚至会超过处理请求的本身。这类系统的典型特征是大量的访问会导致响应时间呈指数级增长和很高的 CUP 使用时间。

             一个最优的实现是综合前面两种方案而提出的观点 ---- 线程池( Thread Pool ),当一个请求达到时,应用系统把置入接收队列,一组的线程从队列提取请求并处理之。这个方案如下图所示:

    3 :启用线程池的服务端应用系统

    在这个例子中,我们用了一个含有两个线程的线程 池。当三个请求到达时,它们立刻安排到队列等待被处理,因为两个线程都是空闲的,所以头两个请求开始执行。当其中任何一个请求处理结束后,空闲的线程就会 去提取第三个请求并处理之。在这种场景中,系统不需要为每个请求创建和销毁线程。线程之间能互相利用。而且如果线程池的执行高效的话,它能增加或删除线程 以获得最优的性能。例如当线程池在执行两个请求时,而 CPU 的利用率才达到 50% ,这表明执行请求正等待某个事件或者正在做某种 I/O 操作。线程池可以发现这种情况,并增加线程的数量以使系统能在同一时间处理更多的请求。相反的,如果 CPU 利用率达到 100% ,线程池可以减少线程的数量以获得更多的 CPU 时间,而不要浪费在上下文切换上面。

    .NET 中的线程池

             基于上面的例子,在企业级应用系统中有一个高效执行的线程池是至关重要的。 Microsoft .NET 框架的开发环境中已经实现了这个,该系统的核心提供了一个现成可用的最优线程池。

    这个线程池不仅对应用程序可用,而且还融合到框架中的多数类中。 .NET 建立在同一个池上是一个很重要的功能特性。比如 .NET Remoting 用它来处理来自远程对象的请求。

             当一个托管应用程序开始执行时,运行时环境( runtime )提供一个线程池,它将在代码第一次访问时被创建。这个池与应用程序所在运行的物理进程关联在一起,当你用 .NET 框架下的同一进程中运行多个应用程序的功能特性时(称之为应用程序域),这将是一个很重要的细节。在这种情况下,由于它们都使用同样的线程池,一个坏的应用程序会影响进程中的其它应用程序。

             你可以通过 System.Threading 名称空间的 Thread Pool 类 来使用线程池,如果你查看一下这个类,就会发现所有的成员都是静态的,而且没有公开的构造函数。这是有理由这样做的,因为每个进程只有一个线程池,并且我 们不能创建新的。这个限制的目的是为了把所有的异步编程技术都集中到同一个池中。所以我们不能拥有一个通过第三方组建创建的无法管理的线程池。

    线程池中执行的函数

    ThreadPool.QueueUserWorkItem 方法运行我们在系统线程池上启动一个函数,它的声明如下:

    public static bool QueueUserWorkItem (WaitCallback callBack, object state)
    
    第一个参数指明我们将在池中执行的函数,它的声明必须与WaitCallback
    代理(delegate)互相匹配:public delegate void WaitCallback (object state);
    
    

    State 参数允许任何类型的信息传递到该方法中,它在调用 QueueUserWorkItem 时传入。

    让我们结合这些新概念,看看“硬件商店”的另一个实现。

    using System;
    
    using System.Threading;
    
    namespace ThreadPoolTest
    
    {
    
       class MainApp
    
       {
    
          static void Main()
    
          {
    
             WaitCallback callBack;
    
             callBack = new WaitCallback(PooledFunc);
    
             ThreadPool.QueueUserWorkItem(callBack,
    
                "Is there any screw left?");
    
             ThreadPool.QueueUserWorkItem(callBack,
    
                "How much is a 40W bulb?");
    
             ThreadPool.QueueUserWorkItem(callBack,
    
                "Decrease stock of monkey wrench");   
    
             Console.ReadLine();
    
          }
    
     
          static void PooledFunc(object state)
    
          {
    
             Console.WriteLine("Processing request '{0}'", (string)state);
    
             // Simulation of processing time
    
             Thread.Sleep(2000);
    
             Console.WriteLine("Request processed");
    
          }
    
       }
    
    }
    

    为了简化例子,我们在 Main 类中创建一个静态方法用于处理请求。由于代理的灵活性,我们可以指定任何实例方法去处理请求,只要这些方法的声明与代理相同。在这里范例中,通过调用 Thread.Sleep ,实现延迟两秒以模拟处理时间。

    你如果编译和执行这个范例,将会看到下面的输出:

    Processing request 'Is there any screw left?'
    
    Processing request 'How much is a 40W bulb?'
    
    Processing request 'Decrease stock of monkey wrench'
    
    Request processed
    
    Request processed
    
    Request processed
    

    注意,所有的请求都被不同的线程并行处理了。

    我们可以通过在两个方法中加入如下的代码,以此看到更多的信息。

     // Main method
    
       Console.WriteLine("Main thread. Is pool thread: {0}, Hash: {1}",
    
                Thread.CurrentThread.IsThreadPoolThread, 
    
                Thread.CurrentThread.GetHashCode());
    
       // Pool method
    
       Console.WriteLine("Processing request '{0}'." + 
    
          " Is pool thread: {1}, Hash: {2}",
    
          (string)state, Thread.CurrentThread.IsThreadPoolThread, 
    
          Thread.CurrentThread.GetHashCode());
    
     

    我们增加了一个 Thread.CurrentThread.IsThreadPoolThread 的调用。如果目标线程属于线程池,这个属性将返回 True 。另外,我们还显示了用 GetHashCode 方法从当前线程返回的结果。它是唯一标识当前执行线程的值。现在看一看这个输出结果:

    Main thread. Is pool thread: False, Hash: 2
    
    Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 4
    
    Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8
    
    Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 9
    
    Request processed
    
    Request processed
    
    Request processed
    
     

    你可以看到所有的请求都被系统线程池中的不同线程执行。再次运行这个例子,注意系统 CPU 的利用率,如果你没有任何其它应用程序在后台运行的话,它几乎是 0% 。因为系统唯一正在做的是每执行 2 秒后就挂起的处理。

             我们来修改一下这个应用,这次我们不挂起处理请求的线程,相反我们会一直让系统忙,为了做到这点,我们用 Environment.TickCount . 构建一个每隔两秒就对请求执行一次的循环。

    int ticks = Environment.TickCount;
    
    while(Environment.TickCount - ticks < 2000);
    

    现在打开任务管理器,看一看 CPU 的使用率,你将看到应用程序占有了 CPU 100 %的使用率。再看一下我们程序的输出结果:

    Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 7
    
    Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8
    
    Request processed
    
    Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 7
    
    Request processed
    
    Request processed
    
     

    注意第三个请求是在第一个请求处理结束之后执行的,而且线程的号码仍然用原来的 7 ,这个原因是线程池检测到 CPU 的使用率已经达到 100 %,一直等待某个线程空闲。它并不会重新创建一个新的线程,这样就会减少线程间的上下文切换开销,以使总体性能更佳。

    使用定时器

    假如你曾经开发过 Microsoft Win32 的应用程序,你知道 SetTimer 函数是 API 之一,通过这个函数可以指定的一个窗口接收到来自系统时间周期的 WM_TIMER 消息。用这个方法遇到的第一个问题是你需要一个窗口去接收消息,所以你不能用在控制台应用程序中。另外,基于消息的实现并不是非常精确,假如你的应用程序正在处理其它消息,情况有可能更糟糕。

    相对基于 Win32 的定时器来说, .NET 中一个很重要的改进就是创建不同的线程,该线程阻塞指定的时间,然后通知一个回调函数。这里的定时器不需要 Microsoft 的消息系统,所以这样就更精确,而且还能用于控制台应用程序中。以下代码显示了这个技术的一种实现:

    class MainApp
    
    {
    
       static void Main()
    
       {
    
          MyTimer myTimer = new MyTimer(2000);
    
          Console.ReadLine();
    
       }
    
    }
    
    class MyTimer
    
    {
    
       int m_period;
    
       public MyTimer(int period)
    
       {
    
          Thread thread;
    
          m_period = period;
    
          thread = new Thread(new ThreadStart(TimerThread));
    
          thread.Start();
    
       }
    
       void TimerThread()
    
       {
    
          Thread.Sleep(m_period);
    
          OnTimer();
    
       }
    
       void OnTimer()
    
       {
    
          Console.WriteLine("OnTimer");
    
       }
    
    }
    

    这个代码一般用于 Wn32 应用中。每个定时器创建独立的线程,并且等待指定的时间,然后呼叫回调函数。犹如你看到的那样,这个实现的成本会非常高。如果你的应用程序使用了多个定时器,相对的线程数量也会随着使用定时器的数量而增长。

    现在我们有 .NET 提供的线程池,我们可以从池中改变请求的等待函数,这样就十分有效,而且会提升系统的性能。我们会遇到两个问题:

    n          假如线程池已满(所有的线程都在运行中),那么这个请求排到队列中等待,而且定时器不在精确。

    n          假如创建了多个定时器,线程池会因为等待它们时间片失效而非常忙。

    为了避免这些问题, .NET 框架的线程池提供了独立于时间的请求。用了这个函数,我们可以不用任何线程就可以拥有成千上万个定时器,一旦时间片失效,这时,线程池将会处理这些请求。

    这些特色出现在两个不同的类中:

             System.Threading.Timer

                       定时器的简单版本,它运行开发人员向线程池中的定期执行的程序指定一个代理( delegate .

    System.Timers.Timer

    System.Threading.Timer 的组件版本,允许开发人员把它拖放到一个窗口表单( form )中,可以把一个事件作为执行的函数。

    这非常有助于理解上述两个类与另外一个称为 System.Windows.Forms.Timer .的类。这个类只是封装了 Win32 中消息机制的计数器,如果你不准备开发多线程应用,那么就可以用这个类。

    在下面的例子中,我们将用 System.Threading.Timer 类,定时器的最简单实现,我们只需要如下定义的构造方法

    public Timer(TimerCallback callback,
    
       object state,
    
       int dueTime,
    
       int period);
    

    对于第一个参数( callback ),我们可以指定定时执行的函数;第二个参数是传递给函数的通用对象;第三个参数是计时器开始执行前的延时;最后一个参数 period ,是两个执行之间的毫秒数。

    下面的例子创建了两个定时器, timer1 timer2

    class MainApp
    
    {
    
       static void Main()
    
       {
    
          Timer timer1 = new Timer(new TimerCallback(OnTimer), 1, 0, 2000);
    
          Timer timer2 = new Timer(new TimerCallback(OnTimer), 2, 0, 3000);
    
          Console.ReadLine();
    
       }
    
       static void OnTimer(object obj)
    
       {
    
          Console.WriteLine("Timer: {0} Thread: {1} Is pool thread: {2}", 
    
             (int)obj,
    
             Thread.CurrentThread.GetHashCode(),
    
             Thread.CurrentThread.IsThreadPoolThread);
    
       }
    
    }
    

    输出:

    Timer: 1 Thread: 2 Is pool thread: True
    
    Timer: 2 Thread: 2 Is pool thread: True
    
    Timer: 1 Thread: 2 Is pool thread: True
    
    Timer: 2 Thread: 2 Is pool thread: True
    
    Timer: 1 Thread: 2 Is pool thread: True
    
    Timer: 1 Thread: 2 Is pool thread: True
    
    Timer: 2 Thread: 2 Is pool thread: True
    

    犹如你看到的那样,两个定时器中的所有函数调用都在同一个线程中执行( ID = 2 ),应用程序使用的资源最小化了。

    同步对象的执行

    相对于定时器, .NET 线程池允许在执行函数上同步对象,为了在多线程环境中的各线程之间共享资源,我们需要用 .NET 同步对象。

    如果我们没有线程,或者线程必须阻塞直到事件收到信号,就像我前面提到一样,这会增加应用程序中总的线程数量,结果导致系统需要更多的资源和 CPU 时间。

    线程池允许我们把请求进行排队,直到某个特殊的同步对象收到信号后执行。如果这个信号没有收到,请求函数将不需要任何线程,所以可以保证系统性能最优化。 ThreadPool 类提供了下面的方法:

    public static RegisteredWaitHandle RegisterWaitForSingleObject(
    
       WaitHandle waitObject,
    
       WaitOrTimerCallback callBack,
    
       object state,
    
       int millisecondsTimeOutInterval,
    
       bool executeOnlyOnce);
    

    第一个参数 ,waitObject 可以是任何继承于 WaitHandle 的对象:

             Mutex

         ManualResetEvent

         AutoResetEvent

    就像你看到的那样,只有系统的同步对象才能用在这里,就是继承自 WaitHandle 的对象。你不能用其它任何的同步机制,比如 moniter 或者 read-write 锁。 剩余的参数允许我们指明当一个对象收到信号后执行的函数( callBack ;一个传递给函数的状态 (state ); 线程池等待对象的最大时间 ( millisecondsTimeOutInterval ) 和一个标识表明对象收到信号时函数只能执行一次, (executeOnlyOnce ). 下面的代理声明目的是用在函数的回调:

    delegate void WaitOrTimerCallback(
    
       object state,
    
       bool timedOut);
    
     
    如果参数 timeout 设置的最大时间已经失效,但是没有同步对象收到信号的花,这个函数就会被调用。
    
    下面的例子用了一个手工事件和一个互斥量来通知线程池中的执行函数:
    
    class MainApp
    
    {
    
       static void Main(string[] args)
    
       {
    
          ManualResetEvent evt = new ManualResetEvent(false);
    
          Mutex mtx = new Mutex(true);
    
          ThreadPool.RegisterWaitForSingleObject(evt,
    
             new WaitOrTimerCallback(PoolFunc),
    
             null, Timeout.Infinite, true);
    
          ThreadPool.RegisterWaitForSingleObject(mtx,
    
             new WaitOrTimerCallback(PoolFunc),
    
             null, Timeout.Infinite, true);
    
          for(int i=1;i<=5;i++)
    
          {
    
             Console.Write("{0}...", i);
    
             Thread.Sleep(1000);
    
          }
    
          Console.WriteLine();
    
          evt.Set();
    
          mtx.ReleaseMutex();
    
          Console.ReadLine();
    
       }
    
       static void PoolFunc(object obj, bool TimedOut)
    
       {
    
          Console.WriteLine("Synchronization object signaled, Thread: {0} Is pool: {1}", 
    
             Thread.CurrentThread.GetHashCode(),
    
             Thread.CurrentThread.IsThreadPoolThread);
    
       }
    
    }
    

    结束显示两个函数都在线程池的同一线程中执行:

    1...2...3...4...5...
    
    Synchronization object signaled, Thread: 6 Is pool: True
    
    Synchronization object signaled, Thread: 6 Is pool: True
    

    异步 I/O 操作

    线程池常见的应用场景就是 I/O 操作。多数应用系统需要读磁盘,数据发送到 Sockets ,因特网连接等等。所有的这些操作都有一些特征,直到他们执行操作时,才需要 CPU 时间。 .NET 框架为所有这些可能执行的异步操作提供了 I/O 类。当这些操作执行完后,线程池中特定的函数会执行。尤其是在服务器应用程序中执行多线程异步操作,性能会更好。

    在第一个例子中,我们将把一个文件异步写到硬盘中。看一看 FileStream 的构造方法是如何使用的:

    public FileStream(
    
       string path,
    
       FileMode mode,
    
       FleAccess access,
    
       FleShare share,
    
       int bufferSize,
    
       bool useAsync);
    

    最后一个参数非常有趣,我们应该对异步执行文件的操作设置 useAsync True 。如果我们没有这样做,即使我们用了异步函数,它们的操作仍然会被主叫线程阻塞。

    下面的例子说明了用一旦 FileStream BeginWrite 方法写文件操作结束,线程池中的一个回调函数将会被执行。注意我们可以在任何时候访问 IAsyncResult 接口,它可以用来了解当前操作的状态。我们可以用 CompletedSynchronously 属性指示一个异步操作是否完成,而当一个操作结束时, IsCompleted 属性会设上一个值。 IAsyncResult 提供了很多有趣的属性,比如: AsyncWaitHandle ,一旦操作完成,一个异步对象将会被通知。

    class MainApp
    
    {
    
       static void Main()
    
       {
    
          const string fileName = "temp.dat";
    
          FileStream fs;
    
          byte[] data = new Byte[10000];
    
          IAsyncResult ar;
    
     
          fs = new FileStream(fileName, 
    
             FileMode.Create, 
    
             FileAccess.Write, 
    
             FileShare.None, 
    
             1, 
    
             true);
    
          ar = fs.BeginWrite(data, 0, 10000,
    
             new AsyncCallback(UserCallback), null);
    
          Console.WriteLine("Main thread:{0}",
    
             Thread.CurrentThread.GetHashCode());
    
          Console.WriteLine("Synchronous operation: {0}",
    
             ar.CompletedSynchronously);
    
          Console.ReadLine();
    
       }
    
       static void UserCallback(IAsyncResult ar)
    
       {
    
          Console.Write("Operation finished: {0} on thread ID:{1}, is pool: {2}", 
    
             ar.IsCompleted, 
    
             Thread.CurrentThread.GetHashCode(), 
    
             Thread.CurrentThread.IsThreadPoolThread);
    
       }
    
    }
    

    输出的结果显示了操作是异步执行的,一旦操作结束后,用户的函数就在线程池中执行。

    Main thread:9
    
    Synchronous operation: False
    
    Operation finished: True on thread ID:10, is pool: True
    

    在应用 Sockets 的场景中,由于 I/O 操作通常比磁盘操作慢,这时用线程池就显得尤为重要。过程跟前面提到的差不多, Socket 类提供了多个方法用于执行异步操作:

             BeginRecieve

             BeginSend

             BeginConnect

             BeginAccept

    假如你的服务器应用使用了 Socket 来与客户端通讯,一定会用到这些方法。这种方法取代了对每个客户端连接都启用一个线程的做法,所有的操作都在线程池中异步执行。

             下面的例子用另外一个支持异步操作的类, HttpWebRequest 用这个类,我们可以建立一个到 Web 服务器的连接。这个方法叫 BeginGetResponse , 但在这个例子中有一个很重要的区别。在上面最后一个示例中,我们没有用到从操作中返回的结果。但是,我们现在需要当一个操作结束时从 Web 服务器返回的响应,为了接收到这个信息, .NET 中所有提供异步操作的类都提供了成对的方法。在 HttpWebRequest 这个类中,这个成对的方法就是: BeginGetResponse EndGetResponse 用了 End 版本,我们可以接收操作的结果。在我们的示例中, EndGetResponse 会从 Web 服务器接收响应。

    虽然可以在任何时间调用 EndGetResponse 方法,但在我们的例子中是在回调函数中做的。仅仅是因为我们想知道已经做了异步请求。如果我们在之前调用 EndGetResponse , 这个调用将一直阻塞到操作完成。

             在下面的例子中,我们发送一个请求到 Microsoft Web ,然后显示了接收到响应的大小。

    class MainApp
    
    {
    
       static void Main()
    
       {
    
          HttpWebRequest request;
    
          IAsyncResult ar;
    
     
          request = (HttpWebRequest)WebRequest.CreateDefault(
    
             new Uri("http://www.microsoft.com"));
    
          ar = request.BeginGetResponse(new AsyncCallback(PoolFunc), request);
    
          Console.WriteLine("Synchronous: {0}", ar.CompletedSynchronously);
    
          Console.ReadLine();
    
       }
    
       static void PoolFunc(IAsyncResult ar)
    
       {
    
          HttpWebRequest request;
    
          HttpWebResponse response;
    
     
          Console.WriteLine("Response received on pool: {0}",
    
             Thread.CurrentThread.IsThreadPoolThread);
    
          request = (HttpWebRequest)ar.AsyncState;
    
          response = (HttpWebResponse)request.EndGetResponse(ar);
    
          Console.WriteLine(" Response size: {0}",
    
             response.ContentLength);
    
       }
    
    }
    

    下面刚开始结果信息表明,异步操作正在执行:

    Synchronous: False
    

    过了一会儿,响应接收到了。下面的结果显示:

    Response received on pool: True
    
       Response size: 27331
    

    就像你看到的那样,一旦收到响应,线程池的异步函数就会执行。

    监视线程池

    ThreadPool 
    类提供了两个方法用来查询线程池的状态。第一个是我们可以从线程池获取当前可用的线程数量:
    
    public static void GetAvailableThreads(
    
       out int workerThreads,
    
       out int completionPortThreads);
    

    从方法中你可以看到两种不同的线程:

             WorkerThreads

           工作线程是标准系统池的一部分。它们是被 .NET 框架托管的标准线程,多数函数是在这里执行的。显式的用户请求( QueueUserWorkItem 方法),基于异步对象的方法( RegisterWaitForSingleObject )和定时器( Timer 类)

    CompletionPortThreads

    这种线程常常用来 I/O 操作, Windows NT, Windows 2000 Windows XP 提供了一个步执行的对象,叫做 IOCompletionPort API 和异步对象关联起来,用少量的资源和有效的方法,我们就可以调用系统线程池的异步 I/O 操作。但是在 Windows 95, Windows 98, Windows Me 有一些局限。比如: 在某些设备上,没有提供 IOCompletionPorts 功能和一些异步操作,如磁盘和邮件槽。在这里你可以看到 .NET 框架的最大特色:一次编译,可以在多个系统下运行。根据不同的目标平台, .NET 框架会决定是否使用 IOCompletionPorts API ,用最少的资源达到最好的性能。

    这节包含一个使用 Socket 类的例子。在这个示例中,我们将异步建立一个连接到本地的 Web 服务器,然后发送一个 Get 请求。通过这个例子,我们可以很容易地鉴别这两种不同的线程。

    using System;
    
    using System.Threading;
    
    using System.Net;
    
    using System.Net.Sockets;
    
    using System.Text;
    
     
    namespace ThreadPoolTest
    
    {
    
       class MainApp
    
       {
    
          static void Main()
    
          {
    
             Socket s;
    
             IPHostEntry hostEntry;
    
             IPAddress ipAddress;
    
             IPEndPoint ipEndPoint;
    
             
    
             hostEntry = Dns.Resolve(Dns.GetHostName());
    
             ipAddress = hostEntry.AddressList[0];
    
             ipEndPoint = new IPEndPoint(ipAddress, 80);
    
             s = new Socket(ipAddress.AddressFamily,
    
                SocketType.Stream, ProtocolType.Tcp);
    
             s.BeginConnect(ipEndPoint, new AsyncCallback(ConnectCallback),s);
    
             
    
             Console.ReadLine();
    
          }
    
          static void ConnectCallback(IAsyncResult ar)
    
          {
    
             byte[] data;
    
             Socket s = (Socket)ar.AsyncState;
    
             data = Encoding.ASCII.GetBytes("GET /"n");
    
     
             Console.WriteLine("Connected to localhost:80");
    
             ShowAvailableThreads();
    
             s.BeginSend(data, 0,data.Length,SocketFlags.None,
    
                new AsyncCallback(SendCallback), null);
    
          }
    
          static void SendCallback(IAsyncResult ar)
    
          {
    
             Console.WriteLine("Request sent to localhost:80");
    
             ShowAvailableThreads();
    
          }
    
          static void ShowAvailableThreads()
    
          {
    
             int workerThreads, completionPortThreads;
    
     
             ThreadPool.GetAvailableThreads(out workerThreads,
    
                out completionPortThreads);
    
             Console.WriteLine("WorkerThreads: {0}," + 
    
                " CompletionPortThreads: {1}",
    
                workerThreads, completionPortThreads);
    
          }
    
       }
    
    }
    

    如果你在 Microsoft Windows NT, Windows 2000, or Windows XP 下运行这个程序,你将会看到如下结果:

    Connected to localhost:80
    
    WorkerThreads: 24, CompletionPortThreads: 25
    
    Request sent to localhost:80
    
    WorkerThreads: 25, CompletionPortThreads: 24
    

    如你所看到地那样,连接用了工作线程,而发送数据用了一个完成端口( CompletionPort ),接着看下面的顺序:

    1.   我们得到一个本地 IP 地址,然后异步连接到那里。

    2.   Socket 在工作线程上执行异步连接操作,因为在 Socket 上,不能用 Windows IOCompletionPorts 来建立连接。

    3.   一旦连接建立了, Socket 类调用指明的函数 ConnectCallback ,这个回调函数显示了线程池中可用的线程数量。我们可以看到这些是在工作线程中执行的。

    4.   在用 ASCII 码对 Get 请求进行编码后,我们用 BeginSend 方法从同样的函数 ConnectCallback 中发送一个异步请求。

    5.   Socket 上的发送和接收操作可以通过 IOCompletionPort 来执行异步操作,所以当请求做完后,回调函数就会在一个 CompletionPort 类型的 线程中执行。因为函数本身显示了可用的线程数量,所以我们可以通过这个来查看,对应的完成端口数已经减少了多少。

    如果我们在 Windows 95, Windows 98, 或者 Windows Me 平台上运行相同的代码,会出现相同的连接结果,请求将被发送到工作线程,而非完成端口。你应该知道的很重要的一点就是, Socket 类总是会利用最优的可用机制,所以你在开发应用时,可以不用考虑目标平台是什么。

           你已经看到在上面的例子中每种类型的线程可用的最大数是 25 。我们可以用 GetMaxThreads 返回这个值:

    public static void GetMaxThreads(
    
       out int workerThreads,
    
       out int completionPortThreads);
    

    一旦到了最大的数量,就不会创建新线程,所有的请求都将被排队。假如你看过 ThreadPool 类的所有方法,你将发现没有一个允许我们更改最大数的方法。就像我们前面提到的那样,线程池是每个处理过程的唯一共享资源。这就是为什么不可能让应用程序域去更改这个配置的原因。想象一下出现这种情况的后果,如果有第三方组件把线程池中线程的最大数改为 1 ,整个应用都会停止工作,甚至在进程中其它的应用程序域都将受到影响。同样的原因,公共语言运行时的宿主也有可能去更改这个配置。比如: ASP.NET 允许系统管理员更改这个数字。

     

    死锁

    在你的应用程序使用线程池之前,还有一个东西你应该知道:死锁。在线程池中执行一个实现不好的异步对象可能导致你的整个应用系统中止运行。

           设想你的代码中有个方法,它需要通过 Socket 连接到一个 Web 服务器上。一个可能的实现就是用 Socket 类中的 BeginConnect 方法异步打开一个连接,然后用 EndConnect 方法等待连接的建立。代码如下:

            
    class ConnectionSocket
    
    {
    
       public void Connect()
    
       {
    
          IPHostEntry ipHostEntry = Dns.Resolve(Dns.GetHostName());
    
          IPEndPoint ipEndPoint = new IPEndPoint(ipHostEntry.AddressList[0],
    
             80);
    
          Socket s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream,
    
             ProtocolType.Tcp);
    
          IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null);
    
          s.EndConnect(ar);
    
       }
    
    }
    

    多快,多好。调用 BeginConnect 使异步操作在线程池中执行,而 EndConnect 一直阻塞到连接被建立。

           如果线程池中的一个执行函数中用了这个类的方法,将会发生什么事情呢?设想线程池的大小只有两个线程,然后用我们的连接类创建了两个异步对象。当这两个函数同时在池中执行时,线程池已经没有用于其它请求的空间了,除非直到某个函数结束。问题是这些函数调用了我们类中的 Connect 方法,这个方法在线程池中又发起了一个异步操作。但线程池一直是满的,所以请求就一直等待任何空闲线程的出现。不幸的是,这将永远不会发生,因为使用线程池的函数正等待队列函数的结束。结论就是:我们的应用系统已经阻塞了。

           我们以此推断 25 个线程的线程池的行为。假如 25 个函数都等待异步对象操作的结束。结果将是一样的,死锁一样会出现。

           在下面的代码片断中,我们使用了这个类来说明问题:

    class MainApp
    
    {
    
       static void Main()
    
       {
    
          for(int i=0;i<30;i++)
    
          {
    
             ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc));
    
          }
    
          Console.ReadLine();
    
       }
    
     
       static void PoolFunc(object state)
    
      {
    
          int workerThreads,completionPortThreads;
    
          ThreadPool.GetAvailableThreads(out workerThreads,
    
             out completionPortThreads);
    
          Console.WriteLine("WorkerThreads: {0}, CompletionPortThreads: {1}", 
    
             workerThreads, completionPortThreads);
    
     
          Thread.Sleep(15000);
    
          ConnectionSocket connection = new ConnectionSocket();
    
          connection.Connect();
    
       }
    
    }
    

    如果你运行这个例子,你将看到池中的线程是如何把线程的可用数量减少到零的,接着应用中止,死锁出现了。

           如果你想在你的应用中避免出现死锁,永远不要阻塞正在等待线程池中的其它函数的线程。这看起来很容易,但记住这个规则意味着有两条:

    n          不要创建这样的类,它的同步方法在等待异步函数。因为这种类可能被线程池中的线程调用。

    n          不要在任何异步函数中使用这样的类,如果它正等待着这个异步函数。

    如果你想检测到应用中的死锁情况,那么就当你的系统挂起时,检查线程池中的线程可用数。线程的可用数量已经没有并且 CPU 的使用率为 0 ,这是很明显的死锁症状。你应该检查你的代码,以确定哪个在线程中执行的函数正在等待异步操作,然后删除它。

          

    有关安全性

             如果你再看看 ThreadPool 类,你会看到有两个方法我们没有用到, UnsafeQueueUserWorkItem UnsafeRegisterWaitForSingleObject 为了完全理解这些方法,首先,我们必须回忆 .NET 框架中安全策略是怎么运作的。

              Windows 安全机制是关注资源。操作系统本身允许对文件,用户,注册表键值和任何其它的系统资源设定权限。这种方法对应用系统的用户认证非常有效,但当出现用户对他使用的系统产生 不信任的情况时,这就会有些局限性。例如这些程序是从 Internet 下载的。在这种情况下,一旦用户安装了这个程序,它就可以执行用户权限范围内的任何操作。举个例子,假如用户可以删除他公司内的任何共享文件,任何从 Internet 下载的程序也都可以这样做。

             .NET 提供了应用到程序的安全性策略,而不是用户。这就是说,在用户权限的范围内,我们可以限制任何执行单元(程序集)使用的资源。通过 MMC ,我们可以根据条件定义一组程序集,然后为每组设置不同的策略,一个典型的例子就是限制从 Internet 下载的程序访问磁盘的权限。

             为了让这个功能运转起来, .NET 框架必须维护一个不同程序集之间的调用栈。假设一个应用没有权限访问磁盘,但是它调用了一个对整个系统都可以访问的类库,当第二个程序集执行一个磁盘的操作时,设置到这个程序集的权限允许这样做,但是权限不会被应用到主叫程序集, .NET 不仅要检查当前程序集的权限,而且会检查整个调用栈的权限。这个栈已经被高度优化了,但是它们给两个不同程序集之间的调用增加了额外的负担。

             UnsafeQueueUserWorkItem , UnsafeRegisterWaitForSingleObject QueueUserWorkItem , RegisterWaitForSingleObject 两个方法 类似。由于是非安全版本不会维护它们执行函数之间的调用栈,所以非安全版本运行的更快些。但是回调函数将只在当前程序集的安全策略下执行,它就不能应用权限到整个调用栈中的程序集。

             我 的建议是仅在性能非常重要的、安全已经控制好的极端情况下才用非安全版本。例如,你构建的应用程序不会被其它的程序集调用,或者仅被很明确清楚的程序集使 用,那么你可以用非安全版本。如果你开发的类库会被第三方应用程序中使用,那么你就不应该用这些方法,因为它们可能用你的库获取访问系统资源的权限。

             在下面例子中,你可以看到用 UnsafeQueueUserWorkItem 方法的风险。我们将构建两个单独的程序集,在第一个程序集中我们将在线程池中创建一个文件,然后我们将导出一个类以使这个操作可以被其它的程序集执行。

    using System;
    
    using System.Threading;
    
    using System.IO;
    
    namespace ThreadSecurityTest
    
    {
    
       public class PoolCheck
    
       {
    
          public void CheckIt()
    
          {
    
             ThreadPool.QueueUserWorkItem(new WaitCallback(UserItem), null);
    
          }
    
          private void UserItem(object obj)
    
          {
    
             FileStream fs = new FileStream("test.dat", FileMode.Create);
    
             fs.Close();
    
             Console.WriteLine("File created");
    
          }
    
       }
    
    }
    

    第二个程序集引用了第一个,并且用了 CheckIt 方法去创建一个文件:

    using System;
    
    namespace ThreadSecurityTest
    
    {
    
       class MainApp
    
       {
    
          static void Main()
    
          {
    
             PoolCheck pc = new PoolCheck();
    
             pc.CheckIt();
    
             Console.ReadLine();
    
          }
    
       }
    
    }
    

    编译这两个程序集,然后运行 main 应用。默认情况下,你的应用被配置为允许执行磁盘操作,所以系统成功生成文件。

         File created
    
    

    现在,打开 .NET 框架的配置。为了简化这个例子,我们仅创建一个代码组关联到 main 应用。接着展开 运行库安全策略 / 计算机 / 代码组 / All_Code / ,增加一个叫 ThreadSecurityTest 的组。在向导中,选择 Hash 条件并导入 Hash 到我们的应用中,设置为 Internet 级别,并选择“该策略级别将只具有与此代码组关联的权限集中的权限”选项。

    运行应用程序,看看会发生什么情况:

    Unhandled Exception: System.Security.SecurityException: Request for the 
    
       permission of type System.Security.Permissions.FileIOPermission, 
    
          mscorlib, Version=1.0.3300.0, Culture=neutral, 
    
             PublicKeyToken=b77a5c561934e089 failed.
    

    我们的策略开始工作,系统已经不能创建文件了。这是因为 .NET 框架为我们维护了一个调用栈才使它成为了可能,虽然创建文件的库有权限去访问系统。

    现在把库中的 QueueUserWorkItem 替换为 UnsafeQueueUserWorkItem 再次编译程序集,然后运行 Main 程序。现在的结果是:

    File created
    

    即使我们的系统没有足够的权限去访问磁盘,但我们已经创建了一个向整个系统公开它的功能的库,却没有维护它的调用栈。记住一个金牌规则: 仅在你的代码不允许让其它的应用系统调用,或者当你想要严格限制访问很明确清楚的程序集,才使用非安全的函数。

     

    结束

             在这篇文章中,我们知道了为什么在我们的服务器应用中需要使用线程池来优化资源和 CPU 的利用。我们学习了一个线程池是如何实现的,需要考虑多个因素如: CPU 使用的百分比,队列请求或者系统的处理器数量。

             .NET 提供了丰富的线程池的功能以让我们的应用程序使用, 并且与 .NET 框架的类紧密地集成在一起。这个线程池是高度优化了的,它只需要最少的 CPU 时间和资源,而且总能适应目标平台。

             因为与框架集成在一起,所以框架中的大部分类都提供了使用线程池的内在功能,给开发人员提供了集中管理和监视应用中的线程池的功能。鼓励第三方组件使用线程池,这样它们的客户就可以享受 .NET 所提供的全部功能。允许执行用户函数,定时器, I/O 操作和同步对象。

             假如你在开发服务器应用系统,只要有可能就在你的请求处理系统中使用线程池。或者你开发了一个让服务器程序使用的库,那么尽可能提供系统线程池的异步对象处理。

    展开全文
  • C#:多线程和线程池

    2019-01-20 20:55:52
     进程是线程的容器,一个C#客户端程序开始于一个单独的线程,CLR(公共语言运行库)为该进程创建了一个线程,该线程称为主线程。例如当我们创建一个C#控制台程序,程序的入口是Main()函数,Main()函数是始于一个主线...

    一、基本概念

    1. 基础

      Windows系统是一个多线程的操作系统。

            一个程序至少有一个进程,一个进程至少有一个线程。

            进程是线程的容器,一个C#客户端程序开始于一个单独的线程,CLR(公共语言运行库)为该进程创建了一个线程,该线程称为主线程。例如当我们创建一个C#控制台程序,程序的入口是Main()函数,Main()函数是始于一个主线程的。它的功能主要是产生新的线程和执行程序。  C#是一门支持多线程的编程语言,通过Thread类创建子线程,引入using System.Threading命名空间。 

    2. 优缺点

    优点

    ①多线程可以提高CPU的利用率,因为当一个线程处于等待状态的时候,CPU会去执行另外的线程;

    ②提高了CPU的利用率,就可以直接提高程序的整体执行速度;

    缺点

    ①线程开的越多,内存占用越大

    ②协调和管理代码的难度加大,需要CPU时间跟踪线程

    ③线程之间对资源的共享可能会产生可不遇知的问题

    3. 前台线程和后台线程

    C#中的线程分为前台线程和后台线程,

    线程创建时不做设置默认是前台线程。即线程属性IsBackground=false。

    Thread.IsBackground = false;//false:设置为前台线程,系统默认为前台线程;

    区别在于:

    ①应用程序必须运行完所有的前台线程才可以退出;

    ②而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。

    ③一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序。

    ④线程是寄托在进程上的,进程都结束了,线程也就不复存在了!

    只要有一个前台线程未退出,进程就不会终止!即程序不会关闭!(即在资源管理器中可以看到进程未结束。)

    4. 多线程的创建

    通过Thread类来创建子线程,Thread类有 ThreadStart 和 ParameterizedThreadStart类型的委托参数,也可以直接写方法的名字。线程执行的方法可以传递参数(可选),参数的类型为object,写在Start()里。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            //主线程
            static void Main(string[] args)
            {
                Thread thread = new Thread(ThreadMethod);     //执行的必须是无返回值的方法
                thread.Name = "子线程";
                //thread.Start("王建"); //方法内传递参数,类型为object,发送和接收涉及到拆装箱操作
                thread.Start();
                Console.ReadKey();
            }
    
            public static void ThreadMethod(object parameter) //方法内可以有参数,也可以没有参数
            {
                Console.WriteLine("{0}开始执行。", Thread.CurrentThread.Name);
            }
        }
    }
    
    
    
    

    说明:

    ①首先使用new Thread()创建出新的线程,

    ②然后调用Start方法使得线程进入就绪状态,得到系统资源后就执行,在执行过程中可能有等待、休眠、死亡和阻塞四种状态。

    ③正常执行结束时间片后返回到就绪状态。如果调用Suspend方法会进入等待状态,调用Sleep或者遇到进程同步使用的锁机制而休眠等待。

    具体过程如下图所示:

    二、线程的基本操作

    1. 线程和其它常见的类一样,有着很多属性和方法

    2. 线程的相关属性

    我们可以通过上面表中的属性获取线程的一些相关信息,下面是代码展示和输出结果:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            //主线程
            static void Main(string[] args)
            {
                Thread thread = new Thread(ThreadMethod);//执行的必须是无返回值的方法
                thread.Name = "子线程";
                thread.Start();
                StringBuilder threadInfo = new StringBuilder();
                threadInfo.Append(" 线程当前的执行状态: " + thread.IsAlive);
                threadInfo.Append("\n 线程当前的名字: " + thread.Name);
                threadInfo.Append("\n 线程当前的优先级: " + thread.Priority);
                threadInfo.Append("\n 线程当前的状态: " + thread.ThreadState);
                Console.Write(threadInfo);
                Console.ReadKey();
            }
    
            public static void ThreadMethod(object parameter)
            {
                Console.WriteLine("{0}开始执行。", Thread.CurrentThread.Name);
            }
        }
    }
    

    3. 线程的相关操作

    ① Abort()方法用来终止线程,调用此方法强制停止正在执行的线程,它会抛出一个ThreadAbortException异常从而导致目标线程的终止。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread thread = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                thread.Name = "小A";
                thread.Start();
                Console.ReadKey();
            }
    
            public static void ThreadMethod(object parameter)
            {
                Console.WriteLine("我是:{0},我要终止了", Thread.CurrentThread.Name);
                //开始终止线程
                Thread.CurrentThread.Abort();
                //下面的代码不会执行
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                }
            }
        }
    }
    

    执行结果:和我们想象的一样,下面的循环没有被执行

    ②ResetAbort()方法可以通过跑出ThreadAbortException异常中止线程,而使用ResetAbort方法可以取消中止线程的操作。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread thread = new Thread(ThreadMethod);//执行的必须是无返回值的方法 
                thread.Name = "小A";
                thread.Start();
                Console.ReadKey();
            }
    
            public static void ThreadMethod(object parameter)
            {
                try
                {
                    Console.WriteLine("我是:{0},我要终止了", Thread.CurrentThread.Name);
                    //开始终止线程
                    Thread.CurrentThread.Abort();
                }
                catch (ThreadAbortException ex)
                {
                    Console.WriteLine("我是:{0},我又恢复了", Thread.CurrentThread.Name);
                    //恢复被终止的线程
                    Thread.ResetAbort();
                }
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                }
            }
        }
    }
    

    ③Sleep()方法:已阻塞线程,使当前线程进入休眠状态,在休眠过程中占用系统内存但是不占用系统时间,当休眠期过后,继续执行,声明如下:

    public static void Sleep(TimeSpan timeout);          //时间段
    public static void Sleep(int millisecondsTimeout);   //毫秒数
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "小A";
                threadA.Start();
                Console.ReadKey();
            }
            public static void ThreadMethod(object parameter)
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);         //休眠300毫秒              
                }
            }
        }
    }
    

    将上面的代码执行以后,可以清楚的看到每次循环之间相差300毫秒的时间。

    ④join()方法主要是用来阻塞调用线程,直到某个线程终止或经过了指定时间为止。官方的解释比较乏味,通俗的说就是创建一个子线程,给它加了这个方法,其它线程就会暂停执行,直到这个线程执行完为止才去执行(包括主线程)。她的方法声明如下:

    public void Join();
    public bool Join(int millisecondsTimeout);    //毫秒数
    public bool Join(TimeSpan timeout);       //时间段
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread threadA = new Thread(ThreadMethod);     //执行的必须是无返回值的方法 
                threadA.Name = "小A";
                Thread threadB = new Thread(ThreadMethod);     //执行的必须是无返回值的方法  
                threadB.Name = "小B";
                threadA.Start();
                //threadA.Join();      
                threadB.Start();
                //threadB.Join();
    
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:主线程,我循环{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);          //休眠300毫秒                                                
                }
                Console.ReadKey();
            }
            public static void ThreadMethod(object parameter)
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);         //休眠300毫秒              
                }
            }
        }
    }
    

    因为线程之间的执行是随机的,所有执行结果和我们想象的一样,杂乱无章!但是说明他们是同时执行的,如下:

     现在我们把代码中的  ThreadA.join()、ThreadB.join()方法注释取消,首先程序中有三个线程,ThreadA、ThreadB和主线程,首先主线程先阻塞,然后线程ThreadB阻塞,ThreadA先执行,执行完毕以后ThreadB接着执行,最后才是主线程执行。

    ⑤Suspent()和Resume()方法:在C# 2.0以后, Suspent()和Resume()方法已经过时了。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被”挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend()。

    static void Main(string[] args)
    {
      Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
      threadA.Name = "小A";  
      threadA.Start();  
      Thread.Sleep(3000);         //休眠3000毫秒      
      threadA.Resume();           //继续执行已经挂起的线程
      Console.ReadKey();
    }
    public static void ThreadMethod(object parameter)
    {
      Thread.CurrentThread.Suspend();  //挂起当前线程
      for (int i = 0; i < 10; i++)
       {
           Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i); 
       }
    }

    执行上面的代码。窗口并没有马上执行 ThreadMethod方法输出循环数字,而是等待了三秒钟之后才输出,因为线程开始执行的时候执行了Suspend()方法挂起。然后主线程休眠了3秒钟以后又通过Resume()方法恢复了线程threadA。

    线程的优先级:如果在应用程序中有多个线程在运行,但一些线程比另一些线程重要,这种情况下可以在一个进程中为不同的线程指定不同的优先级。线程的优先级可以通过Thread类Priority属性设置,Priority属性是一个ThreadPriority型枚举,列举了5个优先等级:

    AboveNormal、BelowNormal、Highest、Lowest、Normal。

    公共语言运行库默认是Normal类型的。见下图:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "A";
                Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadB.Name = "B";
                threadA.Priority = ThreadPriority.Highest;
                threadB.Priority = ThreadPriority.BelowNormal;
                threadB.Start();
                threadA.Start();
                Thread.CurrentThread.Name = "C";
                ThreadMethod(new object());
                Console.ReadKey();
            }
            public static void ThreadMethod(object parameter)
            {
                for (int i = 0; i < 500; i++)
                {
                    Console.Write(Thread.CurrentThread.Name);
                }
            }
        }
    }
    

    上面的代码中有三个线程,threadA,threadB和主线程,threadA优先级最高,threadB优先级最低。

    这一点从运行结果中也可以看出,线程B 偶尔会出现在主线程和线程A前面。当有多个线程同时处于可执行状态,系统优先执行优先级较高的线程,但这只意味着优先级较高的线程占有更多的CPU时间,并不意味着一定要先执行完优先级较高的线程,才会执行优先级较低的线程。

    【注】:

    优先级越高表示CPU分配给该线程的时间片越多,执行时间就多

    优先级越低表示CPU分配给该线程的时间片越少,执行时间就少

    三、线程同步

    1. 什么是线程安全: 线程安全是指在当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。

    线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。

    当多个线程同时读写同一份共享资源的时候,可能会引起冲突。

    这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。

    线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

    为什么要实现同步呢,下面的例子我们拿著名的单例模式来说吧。看代码

        public class Singleton
        {
            private static Singleton instance; 
            private Singleton()   //私有函数,防止实例
            {
    
            } 
            public static Singleton GetInstance()
            {
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }

    单例模式就是保证在整个应用程序的生命周期中,在任何时刻,被指定的类只有一个实例,并为客户程序提供一个获取该实例的全局访问点。但上面代码有一个明显的问题,那就是假如两个线程同时去获取这个对象实例,那。。。。。。。。

    我们对代码进行修改:

    public class Singleton
    {
           private static Singleton instance;
           private static object obj=new object(); 
           private Singleton() //私有化构造函数
           {
    
           } 
           public static Singleton GetInstance()
           {
                   if(instance==null)
                   {
                          lock(obj)      //通过Lock关键字实现同步
                          {
                                 if(instance==null)
                                 {
                                         instance=new Singleton();
                                 }
                          }
                   }
                   return instance;
           }
    }

    经过修改后的代码。加了一个 lock(obj)代码块。这样就能够实现同步了。

    2.使用Lock关键字实现线程同步:首先创建两个线程,两个线程执行同一个方法,参考下面的代码:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "王文建";
                Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadB.Name = "生旭鹏";
                threadA.Start();
                threadB.Start();
                Console.ReadKey();
            }
            public static void ThreadMethod(object parameter)
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);
                }
            }
        }
    }
    

    通过上面的执行结果,可以很清楚的看到,两个线程是在同时执行ThreadMethod这个方法,这显然不符合我们线程同步的要求。

    我们对代码进行修改如下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Program pro = new Program();
                Thread threadA = new Thread(pro.ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "王文建";
                Thread threadB = new Thread(pro.ThreadMethod); //执行的必须是无返回值的方法 
                threadB.Name = "生旭鹏";
                threadA.Start();
                threadB.Start();
                Console.ReadKey();
            }
            public void ThreadMethod(object parameter)
            {
                lock (this)  //添加lock关键字
                {
                    for (int i = 0; i < 10; i++)
                    {
                      Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                        Thread.Sleep(300);
                    }
                }
            }
        }
    }
    

    我们通过添加了 lock(this) {...}代码,查看执行结果实现了我们想要的线程同步需求。

    但是我们知道this表示当前类实例的本身,那么有这么一种情况,我们把需要访问的方法所在的类型进行两个实例A和B,线程A访问实例A的方法ThreadMethod,线程B访问实例B的方法ThreadMethod,这样的话还能够达到线程同步的需求吗。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            static void Main(string[] args)
            {
                Program pro1 = new Program();
                Program pro2 = new Program();
                Thread threadA = new Thread(pro1.ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "王文建";
                Thread threadB = new Thread(pro2.ThreadMethod); //执行的必须是无返回值的方法 
                threadB.Name = "生旭鹏";
                threadA.Start();
                threadB.Start();
                Console.ReadKey();
            }
            public void ThreadMethod(object parameter)
            {
                lock (this)
                {
                    for (int i = 0; i < 10; i++)
                    {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                        Thread.Sleep(300);
                    }
                }
            }
        }
    }
    

    我们会发现,线程又没有实现同步了!

    lock(this)对于这种情况是不行的!所以需要我们对代码进行修改!

    修改后的代码如下: 

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace sss
    {
        class Program
        {
            private static object obj = new object();
            static void Main(string[] args)
            {
                Program pro1 = new Program();
                Program pro2 = new Program();
                Thread threadA = new Thread(pro1.ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "王文建";
                Thread threadB = new Thread(pro2.ThreadMethod); //执行的必须是无返回值的方法 
                threadB.Name = "生旭鹏";
                threadA.Start();
                threadB.Start();
                Console.ReadKey();
            }
            public void ThreadMethod(object parameter)
            {
                lock (obj)
                {
                    for (int i = 0; i < 10; i++)
                    {
                     Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                        Thread.Sleep(300);
                    }
                }
            }
        }
    }
    

    通过查看执行结果。会发现代码实现了我们的需求。

    那么 lock(this) 和lock(Obj)有什么区别呢?

    lock(this) 锁定:当前实例对象,如果有多个类实例的话,lock锁定的只是当前类实例,对其它类实例无影响。所有不推荐使用。
    lock(typeof(Model)):锁定的是model类的所有实例。
    lock(obj):锁定的对象是全局的私有化静态变量。外部无法对该变量进行访问。
    lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
    所以,lock的结果好不好,还是关键看锁的谁,如果外边能对这个谁进行修改,lock就失去了作用。所以一般情况下,使用私有的、静态的并且是只读的对象。

    4.总结:

    ①lock的是必须是引用类型的对象,string类型除外。

    ②lock推荐的做法是使用静态的、只读的、私有的对象。

    ③保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。

     ④ 不能锁定字符串,锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。lock(typeof(Class))与锁定字符串一样,范围太广了。

    5. 使用Monitor类实现线程同步      

       Lock关键字是Monitor的一种替换用法,lock在IL代码中会被翻译成Monitor. 

                 lock(obj)

                  {
                        //代码段
                 } 
        就等同于 
               Monitor.Enter(obj); 
                          //代码段
              Monitor.Exit(obj);  

        Monitor的常用属性和方法:

        Enter(Object) 在指定对象上获取排他锁。

        Exit(Object) 释放指定对象上的排他锁。 

        Pulse 通知等待队列中的线程锁定对象状态的更改。

        PulseAll 通知所有的等待线程对象状态的更改。

        TryEnter(Object) 试图获取指定对象的排他锁。

        TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。

        Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。

          常用的方法有两个,Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。

    Enter(Object)的用法很简单,看代码:

    static void Main(string[] args)
            {                
                Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "A";
                Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadB.Name = "B";
                threadA.Start();
                threadB.Start();
                Thread.CurrentThread.Name = "C";
                ThreadMethod();
                Console.ReadKey();
            }
            static object obj = new object();
            public static void ThreadMethod()
            {
                Monitor.Enter(obj);      //Monitor.Enter(obj)  锁定对象
                try
                {
                    for (int i = 0; i < 500; i++)
                    {
                        Console.Write(Thread.CurrentThread.Name); 
                    }
                }
                catch(Exception ex){   }
                finally
                { 
                    Monitor.Exit(obj);  //释放对象
                } 
            }

        TryEnter(Object)TryEnter() 方法在尝试获取一个对象上的显式锁方面和 Enter() 方法类似。然而,它不像Enter()方法那样会阻塞执行。如果线程成功进入关键区域那么TryEnter()方法会返回true. 和试图获取指定对象的排他锁。看下面代码演示:

          我们可以通过Monitor.TryEnter(monster, 1000),该方法也能够避免死锁的发生,我们下面的例子用到的是该方法的重载,Monitor.TryEnter(Object,Int32),。

           static void Main(string[] args)
            {                
                Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadA.Name = "A";
                Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
                threadB.Name = "B";
                threadA.Start();
                threadB.Start();
                Thread.CurrentThread.Name = "C";
                ThreadMethod();
                Console.ReadKey();
            }
            static object obj = new object();
            public static void ThreadMethod()
            {
                bool flag = Monitor.TryEnter(obj, 1000);   //设置1S的超时时间,如果在1S之内没有获得同步锁,则返回false
            //上面的代码设置了锁定超时时间为1秒,也就是说,在1秒中后,
           //lockObj还未被解锁,TryEntry方法就会返回false,如果在1秒之内,lockObj被解锁,TryEntry返回true。我们可以使用这种方法来避免死锁
                try
                {
                    if (flag)
                    {
                        for (int i = 0; i < 500; i++)
                        {
                            Console.Write(Thread.CurrentThread.Name); 
                        }
                    }
                }
                catch(Exception ex)
                {
    
                }
                finally
                {
                    if (flag)
                        Monitor.Exit(obj);
                } 
            }

    Monitor.Wait和Monitor()Pause()

    Wait(object)方法:释放对象上的锁并阻止当前线程,直到它重新获取该锁,该线程进入等待队列。
     Pulse方法:只有锁的当前所有者可以使用 Pulse 向等待对象发出信号,当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。
    另外Wait 和 Pulse 方法必须写在 Monitor.Enter 和Moniter.Exit 之间

    上面是MSDN的解释。不明白看代码:

     首先我们定义一个攻击类,

        /// <summary>
        /// 怪物类
        /// </summary>
        internal class Monster
        {
            public int Blood { get; set; }
            public Monster(int blood)
            {
                this.Blood = blood;
                Console.WriteLine("我是怪物,我有{0}滴血",blood);
            }
        }

    然后在定义一个攻击类

        /// <summary>
        /// 攻击类
        /// </summary>
        internal class Play
        {
            /// <summary>
            /// 攻击者名字
            /// </summary>
            public string Name { get; set; } 
            /// <summary>
            /// 攻击力
            /// </summary>
            public int Power{ get; set; }
            /// <summary>
            /// 法术攻击
            /// </summary>
            public void magicExecute(object monster)
            {
                Monster m = monster as Monster;
                Monitor.Enter(monster);
                while (m.Blood>0)
                {
                    Monitor.Wait(monster);
                    Console.WriteLine("当前英雄:{0},正在使用法术攻击打击怪物", this.Name);
                    if(m.Blood>= Power)
                    {
                        m.Blood -= Power;
                    }
                    else
                    {
                        m.Blood = 0;
                    }
                    Thread.Sleep(300);
                    Console.WriteLine("怪物的血量还剩下{0}", m.Blood);
                    Monitor.PulseAll(monster);
                }
                Monitor.Exit(monster);
            }
            /// <summary>
            /// 物理攻击
            /// </summary>
            /// <param name="monster"></param>
            public void physicsExecute(object monster)
            {
                Monster m = monster as Monster;
                Monitor.Enter(monster);
                while (m.Blood > 0)
                {
                    Monitor.PulseAll(monster);
                    if (Monitor.Wait(monster, 1000))     //非常关键的一句代码
                    {
                        Console.WriteLine("当前英雄:{0},正在使用物理攻击打击怪物", this.Name);
                        if (m.Blood >= Power)
                        {
                            m.Blood -= Power;
                        }
                        else
                        {
                            m.Blood = 0;
                        }
                        Thread.Sleep(300);
                        Console.WriteLine("怪物的血量还剩下{0}", m.Blood);
                    }
                }
                Monitor.Exit(monster);
            }
        }

    执行代码:

            static void Main(string[] args)
            {
                //怪物类
                Monster monster = new Monster(1000);
                //物理攻击类
                Play play1 = new Play() { Name = "无敌剑圣", Power = 100 };
                //魔法攻击类
                Play play2 = new Play() { Name = "流浪法师", Power = 120 };
                Thread thread_first = new Thread(play1.physicsExecute);    //物理攻击线程
                Thread thread_second = new Thread(play2.magicExecute);     //魔法攻击线程
                thread_first.Start(monster);
                thread_second.Start(monster);
                Console.ReadKey();
            }

    总结:

      第一种情况:

    1. thread_first首先获得同步对象的锁,当执行到 Monitor.Wait(monster);时,thread_first线程释放自己对同步对象的锁,流放自己到等待队列,直到自己再次获得锁,否则一直阻塞。

    2. 而thread_second线程一开始就竞争同步锁所以处于就绪队列中,这时候thread_second直接从就绪队列出来获得了monster对象锁,开始执行到Monitor.PulseAll(monster)时,发送了个Pulse信号。

    3. 这时候thread_first接收到信号进入到就绪状态。然后thread_second继续往下执行到 Monitor.Wait(monster, 1000)时,这是一句非常关键的代码,thread_second将自己流放到等待队列并释放自身对同步锁的独占,该等待设置了1S的超时值,当B线程在1S之内没有再次获取到锁自动添加到就绪队列。

    4. 这时thread_first从Monitor.Wait(monster)的阻塞结束,返回true。开始执行、打印。执行下一行的Monitor.Pulse(monster),这时候thread_second假如1S的时间还没过,thread_second接收到信号,于是将自己添加到就绪队列。

    5. thread_first的同步代码块结束以后,thread_second再次获得执行权, Monitor.Wait(m_smplQueue, 1000)返回true,于是继续从该代码处往下执行、打印。当再次执行到Monitor.Wait(monster, 1000),又开始了步骤3。

    6. 依次循环。。。。

     第二种情况:thread_second首先获得同步锁对象,

    首先执行到Monitor.PulseAll(monster),因为程序中没有需要等待信号进入就绪状态的线程,所以这一句代码没有意义,当执行到 Monitor.Wait(monster, 1000),自动将自己流放到等待队列并在这里阻塞,1S 时间过后thread_second自动添加到就绪队列,线程thread_first获得monster对象锁,执行到Monitor.Wait(monster);时发生阻塞释放同步对象锁,线程thread_second执行,执行Monitor.PulseAll(monster)时通知thread_first。于是又开始第一种情况...

    Monitor.Wait是让当前进程睡眠在临界资源上并释放独占锁,它只是等待,并不退出,当等待结束,就要继续执行剩下的代码。

    6. 使用Mutex类实现线程同步

           Mutex的突出特点是可以跨应用程序域边界对资源进行独占访问,即可以用于同步不同进程中的线程,这种功能当然这是以牺牲更多的系统资源为代价的。

      主要常用的两个方法:

     public virtual bool WaitOne()   阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号获取互斥锁。

     public void ReleaseMutex()     释放 System.Threading.Mutex 一次。

            static void Main(string[] args)
            {
                Thread[] thread = new Thread[3];
                for (int i = 0; i < 3; i++)
                {
                    thread[i] = new Thread(ThreadMethod1);
                    thread[i].Name = i.ToString();
                }
                for (int i = 0; i < 3; i++)
                {
                    thread[i].Start();
                }
                Console.ReadKey(); 
            } 
    
            public static void ThreadMethod1(object val)
            {
                mutet.WaitOne();    //获取锁
                for (int i = 0; i < 500; i++)
                {
                    Console.Write(Thread.CurrentThread.Name); 
                } 
                mutet.ReleaseMutex();  //释放锁
            }

    四. 线程池

          上面介绍了介绍了平时用到的大多数的多线程的例子,但在实际开发中使用的线程往往是大量的和更为复杂的,这时,每次都创建线程、启动线程。

    从性能上来讲,这样做并不理想(因为每使用一个线程就要创建一个,需要占用系统开销);从操作上来讲,每次都要启动,比较麻烦。为此引入的线程池的概念。

     好处:

      1.减少在创建和销毁线程上所花的时间以及系统资源的开销 
      2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。

    在什么情况下使用线程池? 

        1.单个任务处理的时间比较短 
        2.需要处理的任务的数量大 

    线程池最多管理线程数量=“处理器数 * 250”。也就是说,如果您的机器为2个2核CPU,那么CLR线程池的容量默认上限便是1000

    通过线程池创建的线程默认为后台线程,优先级默认为Normal。

    static void Main(string[] args)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadMethod1), new object());    //参数可选
                Console.ReadKey();
            }
    
            public static void ThreadMethod1(object val)
            { 
                for (int i = 0; i <= 500000000; i++)
                {
                    if (i % 1000000 == 0)
                    {
                        Console.Write(Thread.CurrentThread.Name);
                    } 
                } 
            }

    参考文献:

    1. http://www.cnblogs.com/JeffreyZhao/archive/2009/07/22/thread-pool-1-the-goal-and-the-clr-thread-pool.html

    2. https://www.cnblogs.com/wwj1992/p/5976096.html

    展开全文
  • C#线程池设计

    2019-04-17 14:08:43
    1.使用回调函数设计线程池 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ThreadPool { public class...
  • 如何实现C#线程池

    2012-03-05 10:08:33
    如何实现C#线程池?线程池具体的需求是 在某一时间点,只有N个线程在并发执行,如果有多余的线程,则排队等候~下面我就来说说具体的实现 :  C#提供了Mutex与Interlocked这两个与线程相关的类,都在Threading命名空间...
  • C#多线程和线程池

    2019-05-08 08:43:13
    进程是线程的容器,一个C#客户端程序开始于一个单独的线程,CLR(公共语言运行库)为该进程创建了一个线程,该线程称为主线程。例如当我们创建一个C#控制台程序,程序的入口是Main()函数,Main()函数是始于一个主线程...
  • C#线程池的代码

    2014-03-29 17:49:42
    sealed class MyThreadPool { //线程对象 private static object lockObj = new object(); //任务队列 private static Queue threadStartQueue = new Queue(); //记录当前工作
  • Quartz 线程池

    2020-07-10 23:31:26
    Quartz 线程池 中剥离出来的代码 不例子
  • 最近在与大部分C#人员聊天的过程中发现一个问题,许多人都对线程有一定程度的理解与不理解,之所以这么说,是因为有很多人都分不清楚线程池到底是什么东西,想要了解线程池什么,那么就要了解线程的机制与线程的工作...
  • c# socket线程池实现

    2014-03-14 17:38:33
    服务器端:   PoolServer.cs类   using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.IO; using System.Threading;...name
  • C#线程池的实现

    2008-05-09 11:04:00
    具体的需求是 在某一时间点,只有N个线程在并发执行,如果有多余的线程,则排队等候~ 还真是费尽心思啊~最终还是被我攻克了,下面我就来说说具体的实现 : C#提供了Mutex与Interlocked这两个与线程相关的类,都在...
  • 书接上文,Java线程池。 接下来记录一下线程池的工作机制和原理线程池的两个核心队列: - 线程等待池,即线程队列BlockingQueue。 - 任务处理池(PoolWorker),即正在工作的Thread列表(HashSet)。线程池的核心...
  • 在上一篇文章中,我们简单讨论了线程池的作用,以及CLR线程池的一些特性。不过关于线程池的基本概念还没有结束,这次我们再来补充一些必要的信息,有助于我们在程序中选择合适的使用方式。独立线程池上次我们讨论到...
  • C#线程池和进度条

    2014-11-25 11:24:30
    C#编程语言中,使用线程池可以并行地处理工作,当强制线程和更新进度条时,会使用内建架构的ThreadPool类,为批处理使用多核结构,这里我们来看在C#编程语言中一些关于来自System.Threading的ThreadPool的用法的...
  • 线程池(ThreadPool)使用起来很简单,但它有一些限制:1. 线程池中所有线程都是后台线程,如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线 程。2. 不能给入池的线程设置优先级...
  • C# 线程池同步

    2017-04-17 13:58:30
    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Diagnostics; using System.IO; using System.Windows; using System.Runtime;...na
  • 线程池底层原理

    2019-06-24 12:12:49
    线程池底层原理 大厂面试题: 1、请你谈谈对volatile的理解? 2、CAS你知道吗? 3、原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗? 4、我们都知道ArrayList是线程不安全的,请编码写一个不安全的案例...
  • 自定义线程池c#的简单实现 下面是代码,希望大家提出更好的建议:1.ThreadManager.csusing System;using System.Threading;using System.Collections; namespace CustomThreadPool{ /// /// 线程管理器,会开启...
1 2 3 4 5 ... 20
收藏数 4,980
精华内容 1,992
热门标签