• C#线程之线程池篇1

    2018-05-28 17:22:49
    C#线程之线程池篇中,我们将学习多线程访问共享资源的一些通用的技术,我们将学习到以下知识点:在线程池中调用委托在线程池中执行异步操作线程池和并行度实现取消选项使用等待句柄和超时使用计时器使用...

      在C#多线程之线程池篇中,我们将学习多线程访问共享资源的一些通用的技术,我们将学习到以下知识点:

    • 在线程池中调用委托
    • 在线程池中执行异步操作
    • 线程池和并行度
    • 实现取消选项
    • 使用等待句柄和超时
    • 使用计时器
    • 使用BackgroundWorker组件

      在前面的“C#多线程之基础篇”以及“C#多线程之线程同步篇”中,我们学习了如何创建线程以及如何使用多线程协同工作,在这一篇中,我们将学习另外一种场景,就是我们需要创建许多花费时间非常短的异步操作来完成某些工作。我们知道创建一个线程是非常昂贵的,因此,对于每个花费时间非常短的异步操作都创建一个线程是不合适的。

      我们可以使用线程池来解决以上问题,我们可以在线程池中分配一定数量的线程,每当我们需要一个线程时,我们只需要在线程池中取得一个线程即可,而不需要创建一个新的线程,当我们使用完一个线程时,我们仅仅需要把线程重新放入线程池中即可。

      我们可以使用System.Threading.ThreadPool类型来利用线程池。线程池由Common Language Runtime(CLR)进行管理,这意味着每一个CLR只能有一个线程池实例。ThreadPool类型有一个“QueueUserWorkItem”静态方法,这个静态方法接收一个委托,该委托代表一个用户自定义的异步操作。当这个方法被调用时,这个委托就进入内部队列,这个时候,如果线程池中没有线程,则会创建一个新的工作线程,然后将这个委托(第一个)放入队列中。

      如果先前的操作执行完毕后,我们又放置了一个新的操作到线程池,那么我们可能会重用上一次操作的那个工作线程。如果我们放置新的操作的时候,线程池中的线程数已达到上限,那么新的操作会在队列中等待,直到线程池中有可用工作线程为止。

      需要注意的是,我们尽量在线程池中放置一些需要花费较少时间既能完成的操作,而不要放置需要花费大量时间才能完成的操作,同时不要阻塞工作线程。如果不是这样,工作线程会变得非常繁忙,以至于不能响应用户操作,同时也会导致性能问题以及难以调试的错误。

      另外,线程池中的工作线程都是后台线程,这意味着当所有的前台线程执行完毕后,后台线程会被停止执行。

      在这一篇中,我们将学习如何使用线程池执行异步操作、如何取消一个操作以及如何防止长时间运行一个线程。

    一、在线程池中调用委托

      在这一小节中,我们将学习如何在线程池中异步执行一个委托。为了演示如何在线程池中调用一个委托,执行以下操作步骤:

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,编写代码如下所示:

    复制代码
     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 using static System.Threading.Thread;
     5 
     6 namespace Recipe01
     7 {
     8     class Program
     9     {
    10         private delegate string RunOnThreadPool(out int threadId);
    11 
    12         private static string Test(out int threadId)
    13         {
    14             WriteLine("Starting...");
    15             WriteLine($"Is thread pool thread: {CurrentThread.IsThreadPoolThread}");
    16             Sleep(TimeSpan.FromSeconds(2));
    17             threadId = CurrentThread.ManagedThreadId;
    18             return $"Thread pool worker thread id was : {threadId}";
    19         }
    20 
    21         private static  void Callback(IAsyncResult ar)
    22         {
    23             WriteLine("Starting a callback...");
    24             WriteLine($"State passed to a callback: {ar.AsyncState}");
    25             WriteLine($"Is thread pool thread: {CurrentThread.IsThreadPoolThread}");
    26             WriteLine($"Thread pool worker thread id: {CurrentThread.ManagedThreadId}");
    27         }
    28 
    29         static void Main(string[] args)
    30         {
    31             int threadId = 0;
    32             var t = new Thread(() => Test(out threadId));
    33             t.Start();
    34             t.Join();
    35             WriteLine($"Thread id: {threadId}");
    36 
    37             RunOnThreadPool poolDelegate = Test;
    38             IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "a delegate asynchronous call");
    39             r.AsyncWaitHandle.WaitOne();
    40             string result = poolDelegate.EndInvoke(out threadId, r);
    41             WriteLine($"Thread pool worker thread id: {threadId}");
    42             WriteLine(result);
    43 
    44             Sleep(TimeSpan.FromSeconds(2));
    45         }
    46     }
    47 }
    复制代码

    3、运行该控制台应用程序,运行效果如下图所示:

      在第32行代码处,我们使用老办法创建了一个线程,然后启动它,并等待它执行完毕。因为thread的构造方法只接收不带返回值的委托方法,因此,我们给它传递一个lambda表达式,在该表达式中我们调用了“Test”方法。在“Test”方法中,我们使用“Thread.CurrentThread.IsThreadPoolThread”属性值来判断线程是不是来自线程池。我们还使用“CurrentThread.ManagedThreadId”属性值打印出运行当前代码的线程ID。

      在第10行代码处,我们定义了一个委托,该委托表示的方法的返回值为字符串类型,并且接收一个整型类型的输出参数。

      在第37行代码处,我们将Test方法赋值给poolDelegate委托,并在第38行代码处,使用委托的“BeginInvoke”方法运行该委托指向的方法(Test)。“BeginInvoke”接收一个回调方法,该方法将在异步操作完成之后被调用。“BeginInvoke”的第三个参数是传递给回调方法的一个用户自定义的状态。通常使用这个状态来分辨一个异步调用。我们使用“IAsyncResult”接口来保存“BeginInvoke”方法的返回值。

      “BeginInvoke”方法立即返回,这允许我们可以在线程池中的工作线程执行的同时,继续执行调用“BeginInvoke”方法的线程中的下一条代码。

      在第40行代码处,我们可以使用“BeginInvoke”方法的返回值以及对“EndInvoke”方法的调用来获得异步操作的结果。

      注意,第39行代码不是必须的,如果我们注释掉这一行代码,程序仍然运行成功,这是因为“EndInvoke”方法会一直等待异步操作完成。调用“EndInvoke”方法的另一个好处是在工作线程中任何未处理的异常都会抛给调用线程。

      如果我们注释掉第44行代码,回调方法“Callback”将不会被执行,这是因为主线程已经结束,所有的后台线程都会被停止。

    二、在线程池中执行异步操作

      在这一小节中,我们将学习如何在线程池中执行异步操作,具体步骤如下:

    1、使用Visual Studio 2015创建一个新的控制台应用程序。

    2、双击打开“Program.cs”文件,编写代码如下所示:

    复制代码
     1 using System;
     2 using System.Threading;
     3 using static System.Console;
     4 using static System.Threading.Thread;
     5 
     6 namespace Recipe02
     7 {
     8     class Program
     9     {
    10         private static void AsyncOperation(object state)
    11         {
    12             WriteLine($"Operation state: {state ?? "(null)"}");
    13             WriteLine($"Worker thread id: {CurrentThread.ManagedThreadId}");
    14             Sleep(TimeSpan.FromSeconds(2));
    15         }
    16 
    17         static void Main(string[] args)
    18         {
    19             const int x = 1;
    20             const int y = 2;
    21             const string lambdaState = "lambda state 2";
    22 
    23             ThreadPool.QueueUserWorkItem(AsyncOperation);
    24             Sleep(TimeSpan.FromSeconds(2));
    25 
    26             ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
    27             Sleep(TimeSpan.FromSeconds(2));
    28 
    29             ThreadPool.QueueUserWorkItem(state =>
    30             {
    31                 WriteLine($"Operation state: {state}");
    32                 WriteLine($"Worker thread id: {CurrentThread.ManagedThreadId}");
    33                 Sleep(TimeSpan.FromSeconds(2));
    34             }, "lambda state");
    35 
    36             ThreadPool.QueueUserWorkItem(state =>
    37            {
    38                WriteLine($"Operation state: {x + y}, {lambdaState}");
    39                WriteLine($"Worker thread id: {CurrentThread.ManagedThreadId}");
    40                Sleep(TimeSpan.FromSeconds(2));
    41            }, "lambda state");
    42 
    43             Sleep(TimeSpan.FromSeconds(2));
    44         }
    45     }
    46 }
    复制代码

    3、运行该控制台应用程序,运行效果(每次运行效果可能不同)如下图所示:

      在第10~15行代码处,我们定义了一个带有object类型参数的“AsyncOperation”方法,然后在第23行代码处,我们使用ThreadPool的“QueueUserWorkItem”静态方法在线程池中执行“AsyncOperation”方法。

      在第26行代码处,我们又一次使用了“QueueUserWorkItem”静态方法在线程池中执行“AsyncOperation”方法,只不过这次我们给“AsyncOperation”方法传递了state参数。

      在第24行和第27行代码处,我们让主线程阻塞2秒钟,以重用线程池中的工作线程。如果我们注释掉这两行代码,那么工作线程的线程ID大部分情况先将会不一样。

      在第29~41行代码中,我们演示了如何使用lambda表达式来进行线程池中的异步操作,请自行分析结果。

    展开全文
  • 线程的意义在于一个应用程序中,有多个执行部分可以同时执行:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。 C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它...

     

    除非另有说明,否则所有的例子都假定以下命名空间被引用:

    using System;

    using System.Threading;

     

    1      基本概念

    在描述多线程之前,首先需要明确一些基本概念。

    1.1     进程

    进程指一个应用程序所运行的操作系统单元,它是操作系统环境中的基本成分、是系统进行资源分配的基本单位。它最初定义在Unix等多用户、多任务操作系统环境下,用于表示应用程序在内存环境中执行单元的概念。

     

    进程是执行程序的实例。当运行一个应用程序后,就生成了一个进程,这个进程拥有自己的独立内存空间。每一个进程对应一个活动的程序,当进程激活时,操作系统就将系统的资源包括内存、I/O和CPU等分配给它,使它执行。进程在运行时创建的资源随着进程的终止而死亡。

     

    进程间获得专用数据或内存的唯一途径就是通过协议来共享内存块,这是一种协作策略。由于进程之间的切换非常消耗资源和时间,为了提高操作系统的并发性,提高CPU的利用率,在进程下面又加入了线程的概念。

    一个Process可以创建多个Thread及子Process(启动外部程序)。

     

    一个进程内部的线程可以共享该进程所分配的资源,线程的创建与撤销、线程之间的切换所占用的资源比进程少很多。

     

    1.2     线程

    进程可以分为若干个独立执行流(路径),这些执行流被称为线程。线程是指进程内的一个执行单元,也是进程内的可调度实体。线程是进程的一个实体,是CPU调度和分配时间的基本单位。

    线程基本不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同一进程的其它线程共享进程所拥有的全部资源。所以线程间共享内存空间很容易做到,多线程协作也很容易和便捷。

    一个线程可以创建和撤销另一个线程,同一个进程中的多个线程间可以并发执行。

    线程提供了多任务处理的能力。

     

    1.3     线程与进程的异同

    地址空间:进程拥有自己独立的内存地址空间;而线程共享进程的地址空间;换句话说就是进程间彼此是完全隔绝的,同一进程的所有线程共享(堆heap)内存;

    资源拥有:进程是资源分配和拥有的单位,同一进程内的线程共享进程的资源;

    系统粒度:进程是分配资源的基本单位,而线程则是系统(处理器)调度的基本单位;

    执行过程:每个独立的进程都有一个程序运行的入口、顺序执行序列和程序的出口;线程不能独立执行,必须依存于进程中;

    系统开销:创建或撤销进程时,系统都要为之分配或回收资源(如内存空间、IO设备),进程间的切换也要消耗远大于线程切换的开销。

    二者均可并发执行。

     

             一个程序至少有一个进程,一个进程至少有一个线程(主线程)。主线程以函数地址的形式,如Main或WinMain函数,提供程序的启动点,当主线程终止时,进程也随之终止。一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。

     

    1.4     程序域

    在.Net中Process由AppDomain对象所取代。

    虽然AppDomain在CLR中被视为Process的替代品,但实际上AppDomain跟Process是属于主从关系的,AppDomain被放置在一个Process中,每个Process可以拥有多个AppDomain,每个AppDomain又可拥有多个Thread对象。

     

    Process、AppDomain、Thread的关系如下图所示:

    图 1进程、域、线程关系图

     

    AppDomain定义了一些事件供程序员使用。

    事件

    说明

    AssemblyLoad

    触发于AppDomain载入一个Assembly时

    DomainUnLoad

    触发于AppDomain卸载时,也就是Unload函数被调用或是该AppDomain被消灭前

    ProcessExit

    当默认的AppDomain被卸载时触发,多半是应用程序退出时

    各AppDomain间互不影响。

     

    1.5     并发/并行

    在单CPU系统中,系统调度在某一刻只能让一个线程运行,虽然这种调度机制有多种形式(时分/频分),但无论如何,要通过不断切换需要运行的线程,这种运行模式称为并发(Concurrent)。而在多CPU系统中,可以让两个以上的线程同时运行,这种运行模式称为并行(Parallel)。

     

    1.6     异步操作

    所有的程序最终都会由计算机硬件来执行,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。这些无须消耗CPU时间的I/O操作是异步操作的硬件基础。硬盘、光驱、网卡、声卡、显卡都具有DMA功能。

    DMA(DirectMemory Access)是直接内存访问的意思,它是不经过CPU而直接进行内存数据存储的数据交换模式。

    I/O操作包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.Net Remoting等跨进程的调用。

    异步操作可达到避免调用线程堵塞的目的,从而提高软件的可响应性。

     

    1.7     任务管理器

    映射名称列:进程并不拥有独立于其所属实例的映射名称;如果运行5个Notepad拷贝,你会看到5个称为Notepad.exe的进程;它们是根据进程ID进行区分的,该进程ID是由系统维护,并可以循环使用。

    CPU列:它是进程中线程所占用的CPU时间百分比

    每个任务管理器中的进程,其实内部都包含若干个线程,每个时间点都是某个程序进程中的某个线程在运行。

     

    2      多线程基础

    2.1     为什么要使用多线程

    Ø  并发需要

    在C/S或B/S模式下的服务端需要处理来自不同终端的并发请求,使用单线程是不可思议的。

    Ø  提高应用程序的响应速度

    当一个耗时的操作进行时,当前程序都会等待这个操作结束,此时程序不会响应键盘、鼠标、菜单等操作,程序处于假死状态;使用多线程可将耗时长的操作(Time Consuming)置于一个新的线程,此时程序仍能响应用户的其它操作。

    Ø  提高CPU利用率

    在多CPU体系中,操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

    Ø  改善程序结构

    一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

    Ø  花销小、切换快

    线程间的切换时间很小,可以忽略不计

    Ø  方便的通信机制

    线程间共享内存,互相间交换数据很简单。

     

    多线程的意义在于一个应用程序中,有多个执行部分可以同时执行:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。

    C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行。一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并具有多线程创建额外的线程。

     

    2.2     何时使用多线程

    多线程程序一般被用来在后台执行耗时的任务:主线程保持运行,而工作线程执行后台工作。

    对于Windows Forms程序来说,如果主线程执行了一个冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应,进入假死的状态,可能导致用户强制结束程序进程而出现错误。有鉴于此,应该在主线程运行一个耗时任务时另外添加一个工作线程,同时在主线程上有一个友好的提示“处理中...”,允许继续接收事件(比如响应鼠标、键盘操作)。同时程序还应该实现“取消”功能,允许取消/结束当前工作线程。BackgroundWorker类就提供这一功能。

    在没有用户界面的程序里,比如说WindowsService中使用多线程特别的有意义。当一个任务有潜在的耗时(在等待被请求方的响应——比如应用服务器,数据库服务器),用工作线程完成任务意味着主线程可以在发送请求后立即做其它的事情。

    另一个多线程的用途是在需要完成一个复杂的计算工作时。它会在多核的电脑上运行得更快,如果工作量被多个线程分开的话(C#中可使用Environment.ProcessorCount属性来侦测处理芯片的数量)。

    一个C#程序成为多线程可以通过2种方式来实现:明确地创建和运行多线程,或者使用.NET Framework中封装了多线程的类——比如BackgroundWorker类。

    线程池,Threading Timer,远程服务器,或WebServices或ASP.NET程序将别无选择,必须使用多线程;一个单线程的ASP.NET Web Service是不可想象的;幸运的是,应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。

     

    2.3     何时不用多线程

    多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程本身并不复杂,复杂是的线程的交互作用,无论交互是否是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的Bugs。因此,要么多线程的交互设计简单一些,要么就根本不使用多线程,除非你有强烈的重写和调试欲望。

    当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。在某些情况下,太多的I/O操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务快的多。

     

    2.4     创建和开始使用多线程

    线程可以使用Thread类来创建,通过ThreadStart委托来指明方法从哪里开始运行,下面是ThreadStart委托定义:

    public delegate void ThreadStart();

    调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。下面是一个例子,使用了C#的语法创建TheadStart委托:

    Class ThreadTest {

    static void Main() {

    Thread t = new Thread(new ThreadStart(Go));

    t.Start();  // 在新线程中运行Go()

    Go();  // 同时在主线程中运行Go()

    }

    static void Go() { Console.WriteLine ("hello!"); }

     

    在这个例子中,线程t执行Go()方法,大约与此同时主线程也调用了Go(),结果是两个几乎同时hello被打印出来:

    hello!

    hello!

     

    线程有一个IsAlive属性,在调用Start()之后直到线程结束之前一直为true。

    一个线程一旦结束便不能重新开始,只能重新创建一个新的线程。

     

    2.5     带参数启动线程

    在上面的例子里,我们想更好地区分开每个线程的输出结果,让其中一个线程输出大写字母。我们传入一个状态字到Go中来完成整个任务,但不能使用ThreadStart委托,因为它不接受参数。

    2.5.1     ParameterizedThreadStart

    .NET framework定义了另一个版本的委托叫做ParameterizedThreadStart,它可以接收一个单独的object类型参数:

    public delegate void ParameterizedThreadStart(object obj);

     

    之前的例子看起来是这样的:

    Class ThreadTest {

    static void Main() {

    Thread t = new Thread(Go);

    t.Start (true);  // == Go (true)

    Go (false);

    }

    static void Go (object upperCase) {

    bool upper = (bool) upperCase;

    Console.WriteLine (upper ? "HELLO!" : "hello!");

    }

     

    输出结果:

    hello!

    HELLO!

    在整个例子中,编译器自动推断出ParameterizedThreadStart委托,因为Go方法接收一个单独的object参数,就像这样写:

    Thread t = new Thread(new ParameterizedThreadStart(Go));

    t.Start (true);

     

    ParameterizedThreadStart的特性是在使用之前我们必需对我们想要的类型(这里是bool)进行装箱操作,并且它只能接收一个参数

    2.5.2     匿名方法

    需要接收多个参数的解决方案是使用一个匿名方法调用,方法如下:

    static void Main() {

    Thread t = new Thread(delegate() { WriteText ("Hello"); });

    t.Start();

    }

    static void WriteText (stringtext) { Console.WriteLine (text); }

     

    它的优点是目标方法(这里是WriteText),可以接收任意数量的参数,并且没有装箱操作。不过这需要将一个外部变量放入到匿名方法中,如下示例:

    static voidMain() {

    stringtext = "Before";

    Threadt = new Thread(delegate() { WriteText (text); });

    text = "After";

    t.Start();

    }

    static void WriteText (stringtext) { Console.WriteLine (text); }

     

    需要注意的是,当外部变量的值被修改,匿名方法可能进行无意的互动,导致一些古怪的现象。一旦线程开始运行,外部变量最好被处理成只读的——除非有人愿意使用适当的锁。

     

    2.5.3     对象实例方法

    另一种较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性可以告诉线程要做什么,如下列重写了原来的例子:

    Class ThreadTest {

    Bool upper;

    static void Main() {

    ThreadTest instance1 = new ThreadTest();

    instance1.upper = true;

    Thread t = new Thread(instance1.Go);

    t.Start();

    ThreadTest instance2 = new ThreadTest();

    instance2.Go();  // 主线程——运行 upper=false

    }

    Void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }

     

     

    2.6     命名线程

    线程可以通过它的Name属性进行命名,这非常有利于调试:可以用Console.WriteLine打印出线程的名字,Microsoft Visual Studio可以将线程的名字显示在调试工具栏的位置上。线程的名字可以在被任何时间设置——但只能设置一次,重命名会引发异常。

    程序的主线程也可以被命名,下面例子里主线程通过CurrentThread命名:

    Class ThreadNaming {

    static void Main() {

    Thread.CurrentThread.Name= "main";

    Thread worker = new Thread(Go);

    worker.Name= "worker";

    worker.Start();

    Go();

    }

    static void Go() {

    Console.WriteLine ("Hello from "+ Thread.CurrentThread.Name);

    }

    }

    输出

    Hellofrom main

    Hellofrom worker

     

     

    图 2 .Net框架中监控线程

    上图为.Net框架中监控当前线程,可通过名称找到某个线程,查看它的执行情况。

     

    2.7     前台和后台线程

    线程分为两种:用户界面线程(前台线程)和工作线程(后台线程)。

    用户界面线程通常用来处理用户的输入并响应各种事件和消息;工作线程用来执行程序的后台处理任务,比如计算、调度、对串口的读写操作等。

    线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。C#也支持后台线程,当所有前台线程结束后,它们不维持程序的存活。

    改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。

    线程的IsBackground属性控制它的前后台状态,如下实例:

    Class PriorityTest {

    static void Main (string[] args) {

    Thread worker = new Thread(delegate() { Console.ReadLine(); });

    if(args.Length > 0) worker.IsBackground= true;

    worker.Start();

    }

    }

     

    如果程序被调用的时候没有任何参数,工作线程为前台线程,并且将等待ReadLine语句来等待用户的触发回车,这期间,主线程退出,但是程序保持运行,因为一个前台线程仍然活着。

    另一方面如果有参数传入Main(),工作线程被赋值为后台线程,当主线程结束程序立刻退出,终止了ReadLine。

    后台线程终止的这种方式,使任何最后操作都被规避了,这种方式是不太合适的。好的方式是明确等待任何后台工作线程完成后再结束程序,可能用一个timeout(大多用Thread.Join)。如果因为某种原因某个工作线程无法完成,可以用试图终止它的方式,如果失败了,再抛弃线程,允许它与进程一起消亡。

    拥有一个后台工作线程是有益的,最直接的理由是当提到结束程序它总是可能有最后的发言权。交织以不会消亡的前台线程,保证程序的正常退出。抛弃一个前台工作线程是尤为险恶的,尤其对Windows Forms程序,因为程序直到主线程结束时才退出(至少对用户来说),但是它的进程仍然运行着。在Windows任务管理器它将从应用程序栏消失不见,但却可以在进程栏找到它。除非用户找到并结束它,它将继续消耗资源,并可能阻止一个新的实例的运行从开始或影响它的特性。

    对于程序失败退出的普遍原因就是存在“被忘记”的前台线程。

    线程类型

    动作

    结束

    后续处理

    前台线程

    主程序关闭

    显示关闭线程/杀掉当前进程

    后台线程

    主程序关闭

     

     

    2.8     线程优先级

    线程的Priority 属性确定了线程相对于其它同一进程的活动的线程拥有多少执行时间,以下是级别:

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

    只有多个线程同时为活动时,优先级才有作用。

    设置一个线程的优先级为高一些,并不意味着它能执行实时的工作,因为它受限于程序的进程的级别。要执行实时的工作,必须提升在System.Diagnostics 命名空间下Process的级别,像下面这样:

    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;

    ProcessPriorityClass.High其实是一个短暂缺口的过程中的最高优先级别:Realtime。设置进程级别到Realtime通知操作系统:你不想让你的进程被抢占了。如果你的程序进入一个偶然的死循环,可以预期,操作系统被锁住了,除了关机没有什么可以拯救你了!基于此,High大体上被认为最高的有用进程级别。

    如果一个实时的程序有一个用户界面,提升进程的级别是不太好的,因为当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。(虽然在写这篇文章的时候,在互联网电话程序Skype侥幸地这么做, 也许是因为它的界面相当简单吧。)降低主线程的级别、提升进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑越来越慢,因为操作系统仍会拨出过多的CPU给整个进程。最理想的方案是使实时工作和用户界面在不同的进程(拥有不同的优先级)运行,通过Remoting或共享内存方式进行通信,共享内存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和MapViewOfFile)

    2.9     线程异常处理机制

    任何线程在创建时使用try/catch/finally语句块都是没有意义的,当线程开始执行便不再与其有任何关系。考虑下面的程序:

    public static void Main() {

    try{

    new Thread(Go).Start();

    }

    catch(Exception ex) {

    // 不会在这得到异常

    Console.WriteLine ("Exception!");

    }

    static void Go() { throw null; }

    }

     

    示例中的try/catch语句一点用也没有,新创建的线程将引发NullReferenceException异常,而主线程无法捕获到。这是因为每个线程都有独立的执行路径。最好的补救方法是在线程处理的方法内加入异常处理

    public static void Main() {

    new Thread(Go).Start();

    }

    static void Go() {

    try{

    ...

    throw null;  // 这个异常会被捕捉到

    ...

    }

    catch(Exceptionex) {

    记录异常日志,或通知另一个线程错误发生

    ...

    }

     

    从.NET 2.0开始,任何线程内未处理的异常都将导致整个程序关闭,这意味着忽略线程异常将是一个灾难。

    为了避免由未处理异常引起的程序崩溃,try/catch语句块需要出现在每个线程具体实现的方法内。对于经常使用“全局”异常处理的Windows Forms程序员来说,这将很不习惯,就像下面这样的代码:

    Using System;

    Using System.Threading;

    Using System.Windows.Forms;

    static class Program{

    static void Main() {

    Application.ThreadException += HandleError;

    Application.Run (new MainForm());

    }

    static void HandleError (object sender, ThreadExceptionEventArgs e) {

    记录异常或者退出程序或者继续运行...

    }

    }

    Application.ThreadException事件在异常被抛出时触发,以一个Windows信息(比如:键盘,鼠标或者 "paint" 等信息)的方式。简而言之,覆盖了一个Windows Forms程序的几乎所有代码异常。这使开发者产生一种虚假的安全感——所有的异常都被主线程异常处理机制捕获。但实际的情况却是,由工作线程抛出的异常不会被Application.ThreadException捕捉到。

    .NET framework提供了一个更低级别的异常捕获事件:AppDomain.UnhandledException,这个事件在任何类型的程序(有或没有用户界面)中的任何线程有任何未处理的异常抛出时被触发。尽管它提供了比较完善的异常处理解决机制,但是这并不意味着程序不会崩溃,也不意味着能取消.NET异常对话框。

     

    在产品程序中,明确地使用异常处理在所有线程进入的方法中是必要的,可以使用包装类和帮助类来分解工作来完成任务,比如使用BackgroundWorker类(在第三部分进行讨论)

     

    2.10  线程是如何工作的

    线程被一个线程协调程序管理着——一个CLR委托给操作系统的函数。线程协调程序确保将所有活动的线程被分配适当的执行时间;并且那些等待或阻止的线程——比如说在排它锁中、或在用户输入——都是不消耗CPU时间的。

    在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即通常在几微秒区间)

    在多核的电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。但这仍然会出现一些时间切片,因为操作系统的服务线程、以及一些其他的应用程序都会争夺对CPU的使用权。

    线程由于外部因素(比如时间片)被中断被称为被抢占,在大多数情况下,一个线程在被抢占的那一刻就失去了对它的控制权。

     

    2.11  线程安全

    当使用线程(Thread)时,程序员必须注意同步处理的问题,理论上每个Thread都是独立运行的个体,由CLR来主导排程,视Thread的优先权的设置,每个Thread会分到特定的运行时间,当某个Thread的运行时间用完时,CLR就会强制将运行权由该Thread取回,转交给下个Thread,这也就意味着Thread本身无法得知自己何时会丧失运行权,所以会发生所谓的race condition(竞速状态)。

     

    当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以"Done"只被打印了1次。代码以如此方式在不确定的多线程环境中被叫做线程安全。

    临时暂停,或阻止是多线程的协同工作,同步活动的本质特征。等待一个排它锁被释放是一个线程被阻止的原因,另一个原因是线程想要暂停或Sleep一段时间:

    Thread.Sleep (TimeSpan.FromSeconds (30));  // 阻止30秒

     

    一个线程也可以使用它的Join方法来等待另一个线程结束:

    Threadt = new Thread(Go);  // 假设Go是某个静态方法

    t.Start();

    t.Join();  // 等待(阻止)直到线程t结束

     

     

    2.12  异步模式对比

    线程不是一个计算机的硬件功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。

    异步模式无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必共享变量,减少了死锁的可能。不过,编写异步操作的复杂程度比较高,程序主要使用回调方式进行处理,与人的思维方式有出入,而且难以调试。

    计算密集型工作使用多线程(如图形处理、算法);IO密集型工作使用异步机制。

     

            /// <summary>

            /// 异步调用返回事件

            /// </summary>

            public event OnAsynCallBack OnCallBack = null;

     

            /// <summary>

            /// 构造函数,用于异步CallBiz

            /// </summary>

            /// <param name="onCallBack">异步调用返回事件</param>

            /// <param name="callId">调用ID</param>

            /// <param name="timeOutMs">超时(毫秒)</param>

            /// <param name="bizName">业务名称</param>

            /// <param name="funName">方法名称</param>

            /// <param name="argsJson">参数JSON数组</param>

            public AsynCall(OnAsynCallBack onCallBack, string callId, int timeOutMs,string bizName, string funName,params string[] argsJson)

            {

                TimeCall = TimeHelper.GetTicks();

                TimeBack = -1;

                TimeUse = -1;

     

                OnCallBack = onCallBack;

                ReturnType = null;

                CallId = callId;

                TimeOutMs = timeOutMs;

               

                BizName = bizName;

                FunName = funName;

                ArgsJson = argsJson;

     

                this.InFiles = OtCom.Thread_ClsInFiles();

            }

     

     

    展开全文
  • 正如前面所看到的一样,多个线程同时...在使用C#中的lock关键字,我们遇到了一个叫作竞争条件的问题。导致这问题的原因是多线程的执行并没有正确同步。当一个线程执行递增和递减操作时,其他线程需要依次等待。这种...

    原文链接:https://www.cnblogs.com/wyt007/p/9486752.html

    正如前面所看到的一样,多个线程同时使用共享对象会造成很多问题。同步这些线程使得对共享对象的操作能够以正确的顺序执行是非常重要的。在使用C#中的lock关键字,我们遇到了一个叫作竞争条件的问题。导致这问题的原因是多线程的执行并没有正确同步。当一个线程执行递增和递减操作时,其他线程需要依次等待。这种常见问题通常被称为线程同步。
    有多种方式来实现线程同步。首先,如果无须共享对象,那么就无须进行线程同步。令,人惊奇的是大多数时候可以通过重新设计程序来除移共享状态,从而去掉复杂的同步构造。请尽可能避免在多个线程间使用单一对象。
    如果必须使用共享的状态,第二种方式是只使用原子操作。这意味着一个操作只占用一个量子的时间,一次就可以完成。所以只有当前操作完成后,其他线程才能执行其他操作。因此,你无须实现其他线程等待当前操作完成,这就避免了使用锁,也排除了死锁的情况。
    如果上面的方式不可行,并且程序的逻辑更加复杂,那么我们不得不使用不同的方式来协调线程。方式之一是将等待的线程置于阻塞状态。当线程处于阻塞状态时,只会占用尽可能少的CPU时间。然而,这意味着将引入至少一次所谓的上下文切换( context switch),上下文切换是指操作系统的线程调度器。该调度器会保存等待的线程的状态,并切换到另一个线程,依次恢复等待的线程的状态。这需要消耗相当多的资源。然而,如果线程要被挂起很,长时间,那么这样做是值得的。这种方式又被称为内核模式(kernel-mode),因为只有操作系,统的内核才能阻止线程使用CPU时间。
    万一线程只需要等待一小段时间,最好只是简单的等待,而不用将线程切换到阻塞状,态。虽然线程等待时会浪费CPU时间,但我们节省了上下文切换耗费的CPU时间。该方式又被称为用户模式(user-mode),该方式非常轻量,速度很快,但如果线程需要等待较长时间则会浪费大量的CPU时间。
    为了利用好这两种方式,可以使用混合模式(hybrid),混合模式先尝试使用用户模式等,待,如果线程等待了足够长的时间,则会切换到阻塞状态以节省CPU资源。

    执行基本的原子操作(Interlocked)

    本节将展示如何对对象执行基本的原子操作,从而不用阻塞线程就可避免竞争条件。

    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("Incorrect counter");
    
            var c = new Counter();
    
            var t1 = new Thread(() => TestCounter(c));
            var t2 = new Thread(() => TestCounter(c));
            var t3 = new Thread(() => TestCounter(c));
            t1.Start();
            t2.Start();
            t3.Start();
            t1.Join();
            t2.Join();
            t3.Join();
    
            Console.WriteLine("Total count: {0}", c.Count);
            Console.WriteLine("--------------------------");
    
            Console.WriteLine("Correct counter");
    
            var c1 = new CounterNoLock();
    
            t1 = new Thread(() => TestCounter(c1));
            t2 = new Thread(() => TestCounter(c1));
            t3 = new Thread(() => TestCounter(c1));
            t1.Start();
            t2.Start();
            t3.Start();
            t1.Join();
            t2.Join();
            t3.Join();
    
            Console.WriteLine("Total count: {0}", c1.Count);
    
            Console.ReadKey();
        }
    
        static void TestCounter(CounterBase c)
        {
            for (int i = 0; i < 100000; i++)
            {
                c.Increment();
                c.Decrement();
            }
        }
    
        class Counter : CounterBase
        {
            private int _count;
    
            public int Count { get { return _count; } }
    
            public override void Increment()
            {
                _count++;
            }
    
            public override void Decrement()
            {
                _count--;
            }
        }
    
        class CounterNoLock : CounterBase
        {
            private int _count;
    
            public int Count { get { return _count; } }
    
            public override void Increment()
            {
                Interlocked.Increment(ref _count);
            }
    
            public override void Decrement()
            {
                Interlocked.Decrement(ref _count);
            }
        }
    
        abstract class CounterBase
        {
            public abstract void Increment();
    
            public abstract void Decrement();
        }
    }

    当程序运行时,会创建三个线程来运行TestCounter方法中的代码。该方法对一个对象按序执行了递增或递减操作。起初的Counter对象不是线程安全的,我们会遇到竞争条件。所以第一个例子中计数器的结果值是不确定的。我们可能会得到数字0,然而如果运行程序多次,你将最终得到一些不正确的非零结果。在第1部分中,我们通过锁定对象解决了这个问题。在一个线程获取旧的计数器值并计,算后赋予新的值之前,其他线程都被阻塞了。然而,如果我们采用上述方式执行该操作中途不能停止。而借助于Interlocked类,我们无需锁定任何对象即可获取到正确的结果。Interlocked提供了Increment, Decrement和Add等基本数学操作的原子方法,从而帮助我们在编写Counter类时无需使用锁。

    使用Mutex类

    本节将描述如何使用Mutex类来同步两个单独的程序。Mutex是一种原始的同步方式,其只对一个线程授予对共享资源的独占访问。

    class Program
    {
        static void Main(string[] args)
        {
            const string MutexName = "CSharpThreadingCookbook";
    
            using (var m = new Mutex(false, MutexName))
            {
                if (!m.WaitOne(TimeSpan.FromSeconds(5), false))
                {
                    Console.WriteLine("Second instance is running!");
                }
                else
                {
                    Console.WriteLine("Running!");
                    Console.ReadLine();
                    m.ReleaseMutex();
                }
            }
        }
    }

    当主程序启动时,定义了一个指定名称的互斥量,设置initialOwner标志为false。这意.味着如果互斥量已经被创建,则允许程序获取该互斥量。如果没有获取到互斥量,程序则简单地显示Running,等待直到按下了任何键,然后释放该互斥量并退出。
    如果再运行同样一个程序,则会在5秒钟内尝试获取互斥量。如果此时在第一个程序中,按下了任何键,第二个程序则会开始执行。然而,如果保持等待5秒钟,第二个程序将无法,获取到该互斥量。

    使用SemaphoreSlim类

    本节将展示SemaphoreSlim类是如何作为Semaphore类的轻量级版本的。该类限制了同时访问同一个资源的线程数量。

    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 1; i <= 6; i++)
            {
                string threadName = "Thread " + i;
                int secondsToWait = 2 + 2 * i;
                var t = new Thread(() => AccessDatabase(threadName, secondsToWait));
                t.Start();
            }
        }
    
        static SemaphoreSlim _semaphore = new SemaphoreSlim(4);
    
        static void AccessDatabase(string name, int seconds)
        {
            Console.WriteLine("{0} waits to access a database", name);
            _semaphore.Wait();
            Console.WriteLine("{0} was granted an access to a database", name);
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine("{0} is completed", name);
            _semaphore.Release();
    
        }
    }

    当主程序启动时,创建了SemaphoreSlim的一个实例,并在其构造函数中指定允许的并发线程数量。然后启动了6个不同名称和不同初始运行时间的线程。
    每个线程都尝试获取数据库的访问,但是我们借助于信号系统限制了访问数据库的并发,数为4个线程。当有4个线程获取了数据库的访问后,其他两个线程需要等待,直到之前线,程中的某一个完成工作并调用semaphore.Release方法来发出信号。
    这里我们使用了混合模式,其允许我们在等待时间很短的情况下无需使用上下文切换。然而,有一个叫作Semaphore的SemaphoreSlim类的老版本。该版本使用纯粹的内核时间 ( kernel-time)方式。一般没必要使用它,除非是非常重要的场景。我们可以创建一个具名的semaphore,就像一个具名的mutex一样,从而在不同的程序中同步线程。SemaphoreSlim并不使用Windows内核信号量,而且也不支持进程间同步。所以在跨程序同步的场景下可以使用Semaphore。

    使用AutoResetEvent类

    本示例借助于AutoResetEvent类来从一个线程向另一个线程发送通知。AutoResetEvent类可以通知等待的线程有某事件发生。

    class Program
    {
        static void Main(string[] args)
        {
            var t = new Thread(() => Process(10));
            t.Start();
    
            Console.WriteLine("Waiting for another thread to complete work");
            _workerEvent.WaitOne();
            Console.WriteLine("First operation is completed!");
            Console.WriteLine("Performing an operation on a main thread");
            Thread.Sleep(TimeSpan.FromSeconds(5));
            _mainEvent.Set();
            Console.WriteLine("Now running the second operation on a second thread");
            _workerEvent.WaitOne();
            Console.WriteLine("Second operation is completed!");
    
            Console.ReadKey();
        }
    
        private static AutoResetEvent _workerEvent = new AutoResetEvent(false);
        private static AutoResetEvent _mainEvent = new AutoResetEvent(false);
    
        static void Process(int seconds)
        {
            Console.WriteLine("Starting a long running work...");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine("Work is done!");
            _workerEvent.Set();
            Console.WriteLine("Waiting for a main thread to complete its work");
            _mainEvent.WaitOne();
            Console.WriteLine("Starting second operation...");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine("Work is done!");
            _workerEvent.Set();
        }
    }

    当主程序启动时,定义了两个AutoResetEvent实例。其中一个是从子线程向主线程发信号,另一个实例是从主线程向子线程发信号。我们向AutoResetEvent构造方法传人false,定义了这两个实例的初始状态为unsignaled。这意味着任何线程调用这两个对象中的任何一个的WaitOne方法将会被阻塞,直到我们调用了Set方法。如果初始事件状态为true,那么 AutoResetEvent实例的状态为signaled,如果线程调用WaitOne方法则会被立即处理。然后事件状态自动变为unsignaled,所以需要再对该实例调用一次Set方法,以便让其他的线程对,该实例调用WaitOne方法从而继续执行。
    然后我们创建了第二个线程,其会执行第一个操作10秒钟,然后等待从第二个线程发,出的信号。该信号意味着第一个操作已经完成。现在第二个线程在等待主线程的信号。我们对主线程做了一些附加工作,并通过调用mainEvent.Set方法发送了一个信号。然后等待从第二个线程发出的另一个信号。
    AutoResetEvent类采用的是内核时间模式,所以等待时间不能太长。使用ManualResetEventslim类更好,因为它使用的是混合模式。

    使用ManualResetEventSlim类

    本节将描述如何使用ManualResetEventSlim类来在线程间以更灵活的方式传递信号。

    class Program
    {
        static void Main(string[] args)
        {
            var t1 = new Thread(() => TravelThroughGates("Thread 1", 5));
            var t2 = new Thread(() => TravelThroughGates("Thread 2", 6));
            var t3 = new Thread(() => TravelThroughGates("Thread 3", 12));
            t1.Start();
            t2.Start();
            t3.Start();
            Thread.Sleep(TimeSpan.FromSeconds(6));
            Console.WriteLine("The gates are now open!");
            _mainEvent.Set();
            Thread.Sleep(TimeSpan.FromSeconds(2));
            _mainEvent.Reset();
            Console.WriteLine("The gates have been closed!");
            Thread.Sleep(TimeSpan.FromSeconds(10));
            Console.WriteLine("The gates are now open for the second time!");
            _mainEvent.Set();
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine("The gates have been closed!");
            _mainEvent.Reset();
    
            Console.ReadKey();
        }
    
        static void TravelThroughGates(string threadName, int seconds)
        {
            Console.WriteLine("{0} falls to sleep", threadName);
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine("{0} waits for the gates to open!", threadName);
            _mainEvent.Wait();
            Console.WriteLine("{0} enters the gates!", threadName);
        }
    
        static ManualResetEventSlim _mainEvent = new ManualResetEventSlim(false);
    }

    当主程序启动时,首先创建了ManualResetEventSlim类的一个实例。然后启动了三个线程,等待事件信号通知它们继续执行。
    ManualResetEvnetSlim的整个工作方式有点像人群通过大门。而AutoResetEvent事件像一个旋转门,一次只允许一人通过。ManualResetEventSlim是ManualResetEvent的混合版本,一直保持大门敞开直到手动调用Reset方法。当调用mainEvent.Set时,相当于打开了大门从而允许准备好的线程接收信号并继续工作。然而线程3还处于睡眠 "状态,没有赶上时间。当调用mainEvent.Reset相当于关闭了大门。最后一个线程已经准备好执行,但是不得不等待下一个信号,即要等待好几秒钟。

    使用CountdownEvent类

    本节将描述如何使用CountdownEvent信号类来等待直到一定数量的操作完成。

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Starting two operations");
            var t1 = new Thread(() => PerformOperation("Operation 1 is completed", 4));
            var t2 = new Thread(() => PerformOperation("Operation 2 is completed", 8));
            t1.Start();
            t2.Start();
            _countdown.Wait();
            Console.WriteLine("Both operations have been completed.");
            _countdown.Dispose();
    
            Console.ReadKey();
        }
    
        static CountdownEvent _countdown = new CountdownEvent(2);
    
        static void PerformOperation(string message, int seconds)
        {
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine(message);
            _countdown.Signal();
        }
    }

    当主程序启动时,创建了一个CountdownEvent实例,在其构造函数中指定了当两个操,作完成时会发出信号。然后我们启动了两个线程,当它们执行完成后会发出信号。一旦第二个线程完成,主线程会从等待CountdownEvent的状态中返回并继续执行。针对需要等待多个异步操作完成的情形,使用该方式是非常便利的。
    然而这有一个重大的缺点。如果调用countdown.Signal()没达到指定的次数,那么countdown.Wait()将一直等待。请确保使用CountdownEvent时,所有线程完成后都要调用Signal方法。

    使用Barrier类

    本节将展示另一种有意思的同步方式,被称为Barrier, Barrier类用于组织多个线程及时, 在某个时刻碰面。其提供了一个回调函数,每次线程调用了SignalAndWait方法后该回调函数会被执行。

    class Program
    {
        static void Main(string[] args)
        {
            var t1 = new Thread(() => PlayMusic("the guitarist", "play an amazing solo", 5));
            var t2 = new Thread(() => PlayMusic("the singer", "sing his song", 2));
    
            t1.Start();
            t2.Start();
    
            Console.ReadKey();
        }
    
        static Barrier _barrier = new Barrier(2,b => Console.WriteLine("End of phase {0}", b.CurrentPhaseNumber + 1));
    
        static void PlayMusic(string name, string message, int seconds)
        {
            for (int i = 1; i < 3; i++)
            {
                Console.WriteLine("----------------------------------------------");
                Thread.Sleep(TimeSpan.FromSeconds(seconds));
                Console.WriteLine("{0} starts to {1}", name, message);
                Thread.Sleep(TimeSpan.FromSeconds(seconds));
                Console.WriteLine("{0} finishes to {1}", name, message);
                _barrier.SignalAndWait();
            }
        }
    }

    我们创建了Barrier类,指定了我们想要同步两个线程。在两个线程中的任何一个调用了barrier.SignalAndWait方法后,会执行一个回调函数来打印出阶段。
    每个线程将向Barrier发送两次信号,所以会有两个阶段。每次这两个线程调用Signal AndWait方法时, Barrier将执行回调函数。这在多线程迭代运算中非常有用,可以在每个迭代,结束前执行一些计算。当最后一个线程调用SignalAndWait方法时可以在迭代结束时进行交互。

    使用ReaderWriterLockSlim类

    本节将描述如何使用ReaderWriterLockSlim来创建一个线程安全的机制,在多线程中对,一个集合进行读写操作。ReaderWriterLockSlim代表了一个管理资源访问的锁,允许多个线程同时读取,以及独占写。

    class Program
    {
        static void Main(string[] args)
        {
            new Thread(Read){ IsBackground = true }.Start();
            new Thread(Read){ IsBackground = true }.Start();
            new Thread(Read){ IsBackground = true }.Start();
    
            new Thread(() => Write("Thread 1")){ IsBackground = true }.Start();
            new Thread(() => Write("Thread 2")){ IsBackground = true }.Start();
    
            Thread.Sleep(TimeSpan.FromSeconds(30));
    
            Console.ReadKey();
        }
    
        static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
        static Dictionary<int, int> _items = new Dictionary<int, int>();
    
        static void Read()
        {
            Console.WriteLine("Reading contents of a dictionary");
            while (true)
            {
                try
                {
                    _rw.EnterReadLock();
                    foreach (var key in _items.Keys)
                    {
                        Thread.Sleep(TimeSpan.FromSeconds(0.1));
                    }
                }
                finally
                {
                    _rw.ExitReadLock();
                }
            }
        }
    
        static void Write(string threadName)
        {
            while (true)
            {
                try
                {
                    int newKey = new Random().Next(250);
                    _rw.EnterUpgradeableReadLock();
                    if (!_items.ContainsKey(newKey))
                    {
                        try
                        {
                            _rw.EnterWriteLock();
                            _items[newKey] = 1;
                            Console.WriteLine("New key {0} is added to a dictionary by a {1}", newKey, threadName);
                        }
                        finally
                        {
                            _rw.ExitWriteLock();
                        }
                    }
                    Thread.Sleep(TimeSpan.FromSeconds(0.1));
                }
                finally
                {
                    _rw.ExitUpgradeableReadLock();
                }
            }
        }
    }

    当主程序启动时,同时运行了三个线程来从字典中读取数据,还有另外两个线程向该字典中写入数据。我们使用ReaderWriterLockSlim类来实现线程安全,该类专为这样的场景而设计。
    这里使用两种锁:读锁允许多线程读取数据,写锁在被释放前会阻塞了其他线程的所有操作。获取读锁时还有一个有意思的场景,即从集合中读取数据时,根据当前数据而决,定是否获取一个写锁并修改该集合。一旦得到写锁,会阻止阅读者读取数据,从而浪费大量的时间,因此获取写锁后集合会处于阻塞状态。为了最小化阻塞浪费的时间,可以使用EnterUpgradeableReadLock和ExitUpgradeableReadLock方法。先获取读锁后读取数据。如果发现必须修改底层集合,只需使用EnterWriteLock方法升级锁,然后快速执行一次写操作,最后使用ExitWriteLock释放写锁。

    在本例中,我们先生成一个随机数。然后获取读锁并检查该数是否存在于字典的键集合中。如果不存在,将读锁更新为写锁然后将该新键加入到字典中。始终使用tyr/finally代码块来确保在捕获锁后一定会释放锁,这是一项好的实践。所有的线程都被创建为后台线程。
    主线程在所有后台线程完成后会等待30秒。

    使用SpinWait类

    本节将描述如何不使用内核模型的方式来使线程等待。另外,我们介绍了SpinWait,它是一个混合同步构造,被设计为使用用户模式等待一段时间,然后切换到内核模式以节省CPU时间。

    class Program
    {
        static void Main(string[] args)
        {
            var t1 = new Thread(UserModeWait);
            var t2 = new Thread(HybridSpinWait);
    
            Console.WriteLine("Running user mode waiting");
            t1.Start();
            Thread.Sleep(20);
            _isCompleted = true;
            Thread.Sleep(TimeSpan.FromSeconds(1));
            _isCompleted = false;
            Console.WriteLine("Running hybrid SpinWait construct waiting");
            t2.Start();
            Thread.Sleep(5);
            _isCompleted = true;
    
            Console.ReadKey();
        }
    
        static volatile bool _isCompleted = false;
    
        static void UserModeWait()
        {
            while (!_isCompleted)
            {
                Console.Write(".");
            }
            Console.WriteLine();
            Console.WriteLine("Waiting is complete");
        }
    
        static void HybridSpinWait()
        {
            var w = new SpinWait();
            while (!_isCompleted)
            {
                w.SpinOnce();
                Console.WriteLine(w.NextSpinWillYield);
            }
            Console.WriteLine("Waiting is complete");
        }
    }

    当主程序启动时,定义了一个线程,将执行一个无止境的循环,直到20毫秒后主线程,设置_isCompleted变量为true,我们可以试验运行该周期为20-30秒,通过Windows任务管理器测量CPU的负载情况。取决于CPU内核数量,任务管理器将显示一个显著的处理时间。
    我们使用volatile关键字来声明isCompleted静态字段。Volatile关键字指出一个字段可能会被同时执行的多个线程修改。声明为volatile的字段不会被编译器和处理器优化为只能被单个线程访问。这确保了该字段总是最新的值。
    然后我们使用了SpinWait版本,用于在每个迭代打印一个特殊标志位来显示线程是否切换为阻塞状态。运行该线程5毫秒来查看结果。刚开始, SpinWait尝试使用用户模式,在9 个迭代后,开始切换线程为阻塞状态。如果尝试测量该版本的CPU负载,在Windows任务管理器将不会看到任何CPU的使用。

    展开全文
  • C#线程基础概念

    2018-12-26 10:06:15
    C#线程1 :信号量Semaphore 通过使用一个计数器对共享资源进行访问控制,Semaphore构造器需要提供初始化的计数器(信号量)大小以及最大的计数器大小 访问共享资源时,程序首先申请一个向Semaphore申请一个许可...

    C#多线程1 :信号量Semaphore

    通过使用一个计数器对共享资源进行访问控制,Semaphore构造器需要提供初始化的计数器(信号量)大小以及最大的计数器大小

    访问共享资源时,程序首先申请一个向Semaphore申请一个许可证,Semaphore的许可证计数器相应的减一,当计数器为0时,其他申请该信号量许可证的线程将被堵赛,直到先前已经申请到许可证的线程释放他占用的许可证让计数器加一,这样最近去申请许可证的线程将会得到竞争得到被释放的许可证。

    常见的操作方法 WaitOne():申请一个许可证  Release():释放占用的许可证

    通过使用一个计数器对共享资源进行访问控制,Semaphore构造器需要提供初始化的计数器(信号量)

    使用线程的理由

    1 使用线程可以使代码同其他代码隔离,提高应用程序的可靠性

    2 可以使用线程来简化编码

    3 可以使用线程来实现并发

    基础知识

    1 进程与线程:进程作为操作系统执行程序的基本单位,拥有应用程序的资源,进程包含线程,进程的资源被线程共享,不会抛出异常。

    2 前台线程和后天线程:通过Thread类新建线程默认为前台线程。当所有前台线程关闭时,所有的后台线程也会被终止,不会抛出异常

    3 挂起(Suspend)和唤醒(Resume):由于线程的执行顺序和程序的执行顺序不可预知,所以使用挂求和唤醒容易发生死锁情况,在实际情况中应当尽量少用。

    4 阻塞线程: John,阻塞调用线程,直到该线程终止

    5 终止线程:Abort:抛出ThreadAbortException异常让线程终止,终止后线程不可唤醒。

    Interrupt:抛出ThreadInterruptExeception异常,让线程终止,通过捕获异常可以继续执行。

    6、线程优先级:AboveNormal BelowNormal Highest Lowest Normal,默认为Normal。

    线程池

    由于线程的创建和销毁需要耗费一定的开销,过多的使用线程会造成内存资源的浪费,出于对性能的考虑,引入了线程池的概念。线程池维护一个请求队列,线程池的代码从队列提取任务,然后委派给线程池的一个线程执行,线程执行完不会立即销毁,这样既可以在后台执行任务,又可以减少线程创建和销毁所带来的开销

    线程池线程默认为后天线程(IsBackground)

    任务

    使用ThreadPool的QueueUserWorkItem()方法发起一次异步的线程执行很简单,但是该方法最大的问题是没有一个内建的机制让你知道操作什么时候完成,有没有一个内建的机制在操作完成后获得一个返回值。为此,可以使用System.Threading.Tasks中的Task类。

     

    构造一个Task<TResult>对象,并为泛型TResult参数传递一个操作的返回类型。

    线程同步

     

    1)原子操作(Interlocked):所有方法都是执行一次原子读取或一次写入操作。

     

    2)lock()语句:避免锁定public类型,否则实例将超出代码控制的范围,定义private对象来锁定。

     

    3)Monitor实现线程同步

     

    通过Monitor.Enter() 和 Monitor.Exit()实现排它锁的获取和释放,获取之后独占资源,不允许其他线程访问。

     

    还有一个TryEnter方法,请求不到资源时不会阻塞等待,可以设置超时时间,获取不到直接返回false。

     

    4)ReaderWriterLock

     

    当对资源操作读多写少的时候,为了提高资源的利用率,让读操作锁为共享锁,多个线程可以并发读取资源,而写操作为独占锁,只允许一个线程操作。

     

    5)事件(Event)类实现同步

     

    事件类有两种状态,终止状态和非终止状态,终止状态时调用WaitOne可以请求成功,通过Set将时间状态设置为终止状态。

     

    1)AutoResetEvent(自动重置事件)

     

    2)ManualResetEvent(手动重置事件)

     

    6)信号量(Semaphore)

     

    信号量是由内核对象维护的int变量,为0时,线程阻塞,大于0时解除阻塞,当一个信号量上的等待线程解除阻塞后,信号量计数+1。

     

    线程通过WaitOne将信号量减1,通过Release将信号量加1,使用很简单。

     

    7)互斥体(Mutex)

     

    独占资源,用法与Semaphore相似。

     

    8)跨进程间的同步

     

    通过设置同步对象的名称就可以实现系统级的同步,不同应用程序通过同步对象的名称识别不同同步对象。

     

     

    展开全文
  • 本文是一篇读书笔记,由《C#线程编程实战》一书中的内容整理而来,主要梳理了.NET中多线程编程相关的知识脉络,从Thread、ThreadPool、Task、async/await、并发集合、Parallel、PLINQ到Rx及异步I/O等内容,均有所...

    本文是一篇读书笔记,由《C#多线程编程实战》一书中的内容整理而来,主要梳理了.NET中多线程编程相关的知识脉络,从Thread、ThreadPool、Task、async/await、并发集合、Parallel、PLINQ到Rx及异步I/O等内容,均有所覆盖。为了帮助大家理解本文内容,首先给出博主在阅读该书过程中绘制的思维导图,大家可以根据个人需要针对性的查漏补缺。

    《多线程编程实战》思维导图

    线程基础

    • Tips1:暂停线程,即通过Thread.Sleep()方法让线程等待一段时间而不用消耗操作系统资源。当线程处于休眠状态时,它会占用尽可能少的CPU时间。
    • Tips2:线程等待,即通过Join()方法等待另一个线程结束,因为不知道执行所需要花费的时间,此时Thread.Sleep()方法无效,并且第一个线程等待时是处于阻塞状态的。
    • Tips3:终止线程,调用Abort()方法会给线程注入ThreadAbortException异常,该异常会导致程序崩溃,且该方法不一定总是能终止线程,目标线程可以通过处理该异常并调用Thread.ResetAbort()方法来拒绝被终止,因此不推荐使用Abort()方法来终止线程,理想的方式是通过CancellationToken来实现线程终止。
    • Tips4:线程优先级,线程优先级决定了该线程可占用多少CPU时间,通过设置IsBackground属性可以指定一个线程是否为后台线程,默认情况下,显式创建的线程都是前台线程。其主要区别是:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。需要注意的是,如果程序定义了一个不会赞成的前台线程,主程序并不会正常结束。
    • Tips5:向线程传递参数,可以通过ThreadStart或者lambda表达式来向一个线程传递参数,需要注意的是,由lambda表达式带来的闭包问题
    • Tips6:竞争条件是多线程环境中非常常见的导致错误的原因,通过lock关键字锁定一个静态对象(static&readonly)时,需要访问该对象的所有其它线程都会处于阻塞状态,并等待直到该对象解除锁定,这可能会导致严重的性能问题,
    • Tips7:发生死锁的原因是锁定的静态对象永远无法解除锁定,通常Monitor类用以解除死锁,而lock关键字用以创建死锁,Monitor类的TryEnter()方法可以用以检测静态对象是否可以解锁,lock关键字本质上是Monitor类的语法糖。
    bool acquiredLock = false;
    try
    {
      Monitor.Enter(lockObject, ref acquiredLock)
    }
    finally
    {
      if(acquiredLock)
      {
        Monitor.Exit(lockObject)
      }
    }
    • Tips8:不要在线程中抛出异常,而是在线程代码中使用try…catch代码块。

    线程同步

    • Tips9:无须共享对象,则无须进行线程同步,通过重新设计程序来移除共享状态,从而避免复杂的同步构造;使用原子操作,这意味着一个操作只占用一个量子的时间,一次就可以完成,并且只有当前操作完成后,其它线程方可执行其它操作,因此,无须实现其它线程等待当前操作完成,进而避免了使用锁,排除了死锁。
    • Tips10:为了实现线程同步,我们不得不使用不同的方式来协调线程,方式之一是将等待的线程设为阻塞,当线程处于阻塞状态时,会占用尽可能少的CPU时间,然而这意味着会引入至少一次的上下文切换。上下文切换,是指操作系统的线程调度器,该调度器会保存等待的线程状态,并切换到另一个线程,依次恢复等待的线程状态,而这需要消耗更多的资源。
    • Tips11:线程调度模式,当线程挂起很长时间时,需要操作系统内核来阻止线程使用CPU时间,这种模式被称为内核模式;当线程只需要等待一小段时间,而不需要将线程切换到阻塞状态,这种模式被称为用户模式;先尝试按照用户模式进行等待,如线程等待足够长时间,则切换到阻塞状态以节省CPU资源,这种模式被称为混合模式。
    • Tips12:Mutex是一种原始的同步方法,其只对一个线程授予对共享资源的独占访问,Mutex可以在不同的程序中同步线程。
    • Tips13:SemaphoreSlim是Semaphore的轻量级版本,用以限制同时访问同一个资源的线程数量,超过该数量的线程需要等待,直到之前的线程中某一个完成工作,并调用Release()方法发出信号,其使用了混合模式,而Semaphore则使用内核模式,可以在跨程序同步的场景下使用。
    • Tips14:AutoResetEvent类用以从一个线程向另一个线程发送通知,该类可以通知等待的线程有某个事件发生,其实例在默认情况下初始状态为unsignaled,调用WaitOne()方法时将会被阻塞,直到我们调用了Set方法;相反地,如果初始状态为signaled,调用WaitOne()方法时将会被立即处理,需要我们再调用一次Set方法,以便向其它线程发出信号。
    • Tips15:ManualResetEventSlim类是使用混合模式的线程信号量,相比使用内核模式的AutoResetEvent类更好(因为等待时间不能太长),AutoResetEvent像一个旋转门,一次仅允许一个人通过,而ManualResetEventSlim是ManualResetEvent的混合版本,一直保持大门开启直到手动屌用Reset方法。
    • Tips16:EventWaitHandle类是AutoResetEvent和ManualResetEvent的基类,可以通过调用其WaitOne()方法来阻塞线程,直到Set()方法被调用,它有两种状态,即终止态和非终止态,这两种状态可以相互转换,调用Set()方法可将其实例设为终止态,调用Reset()方法可以将其实例设为非终止态。
    • Tips17:CountdownEvent类可以用以等到直到一定数量的操作完成,需要注意的是,如果其实例方法Signal()没有达到指定的次数,则其实例方法Wait()将一直等待。所以,请确保使用CountdownEvent时,所有线程完成后都要调用Signal()方法。
    • Tips18:ReaderWriterLockSlim用以创建一个线程安全的机制,在多线程中对一个集合进行读写操作,ReaderWriterLockSlim代表了一个管理资源访问的锁,允许多个线程同时读取,以及独占写。其中,读锁允许多线程读取数据,写锁在被释放前会阻塞其它线程的所有操作。
    • Tips19:SpinWait类是一个混合同步构造,被设计为使用用户模式等待一段时间,然后切换到内核模式以节省CPU时间。

    使用线程池

    • Tips20:volatile关键字指出一个字段可能会被同时执行的多个线程修改,声明为volatile的字段不会被编译器和处理器优化为只能被单线程访问。
    • Tips21:创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。线程池的用途是执行运行时间短的操作,使用线程池可以减少并行度耗费及节省操作系统资源。在ASP.NET应用程序中使用线程池时要相当小心,ASP.NET基础切实使用自己的线程池,如果在线程池中浪费所有的工作者线程,Web服务器将不能够服务新的请求,在ASP.NET中只推荐使用I/O密集型的异步操作,因为其使用了一个不同的方式,叫做I/O线程。
    • Tips22:APM,即异步编程模型,是指使用BeginXXX/EndXXX和IAsyncResult对象等方式,其通过调用BeginInvoke方法返回IAsyncResult对象,然后通过调用EndInvoke方法返回结果,我们可通过轮询IAsyncResult对象的IsCompleted或者调用IAsyncResult对象的AsyncWaitHandle属性的WaitOne()方法来等待直到操作完成。
    • Tips23:ThreadPool.RegisterWaitForSingleObject()方法允许我们将回调函数放入线程池中的队列中,当提供的等待事件处理器收到信号或发生超时时,该回调函数将被调用,这做鱼我们为线程池中的操作实现超时功能。具体思路是:ManualResetEvent + CancellationToken,当接收到ManualResetEvent对象的信号时处理超时,或者是使用CancellationToken来处理超时。
    • Tips24:CancellationToken是.NET4.0中被引入的实现异步操作的取消操作的事实标准,我们可以使用三种方式来实现取消过程,即轮询IsCancellationRequested属性、抛出OperationCanceledException异常、为CancellationToken注册一个回调函数。
    • Tips25:Timer对象用以在线程池中创建周期性调用的异步操作。
    • Tips26:BackgroundWorker组件,是典型的基于事件的异步模式,即EAP,当通过RunWorkerAsync启动一个异步操作时,DoWork事件所订阅的事件处理器,将会运行在线程池中,如果需要需要取消异步操作,则可以调用CancelAsync()方法。

    使用任务并行库

    • Tips27:TPL即任务并行库,在.NET 4.0中被引入,目的是解决APM和EAP中获取结果和传播异常的问题,TPL在.NET4.5中进行了调整,使其在使用上更简单,它可以理解为线程池之上的又一个抽象层,对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的API。TPL的核心概念是任务,一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或者不使用独立线程运行。TPL相比之前的模式,一个关键优势是其具有用于组合任务的便利的API。
    • Tips28:Task.Run是Task.Factory.StartNew的一个快捷方式,后者有附加的选项,在无特殊需求的情况下,可以直接使用Task.Run,通过TaskScheduler,我们可以控制任务的运行方式。
    • Tips29:使用Task实例的Start方法启动任务并等待结果,该任务会被放置在线程池中并且主线程会等待,直到任务返回前一直处于阻塞状态;使用Task实例的RunSynchronously方法启动任务,该任务是运行在主线程中,这是一个非常好的优化,可以避免使用线程池来执行非常短暂的操作;我们可以通过轮询Task实例的状态信息来判断一个任务是否执行结束。
    • Tips30:通过Task实例的ContinueWith方法可以为任务设置一个后续操作,通过TaskContinuationOptions选项来指定后续任务以什么样的方式执行。
    • Tips31:通过Task实例的FromAsync可以实现APM到Task的转换
    • Tips32:通过TaskCompletionSource可以实现EAP到Task的转换
    • Tips33:TaskScheduler是一个非常重要的抽象,该组件实际上负责如何执行任务,默认的任务调度程序将任务放置在线程池的工作线程中。为了避免死锁,绝对不要通过任务调度程序在UI线程中使用同步操作,请使用ContinueWith或async/await方法。

    使用C# 6.0

    • Tips34:异步函数是C# 5.0引入的语言特性,它是基于TPL之上的更高级别抽象,真正简化了异步编程。要创建一个异步函数,首先需要使用async关键字标注一个方法,其次异步函数必须返回Task或Task类型,可以使用async void的方法,但是更推荐async Task的方法,使用async void的方法的唯一合理的地方就是在程序中使用顶层UI控制器事件处理器的时候,在使用async关键字标注的方法内部,可以使用await操作符,该操作符可与TPL任务一起工作,并获取该任务中异步操作的结果,在async方法外部不能使用await关键字,否则会有编译错误,异步函数代码中至少要拥有一个await关键字。
    • Tips35:在Windows GUI或ASP.NET等环境中不推荐使用Task.Wait和Task.Result,因为非常有可能会造成死锁。
      async可以和lambda表达式联用,在表达式体中应该至少含有一个await关键字标示,因为lambda表达式的类型无法通过自身推断,所以必须显式地向C#编译器指定类型。
    • Tips36:异步并不总是意味着并行执行
    • Tips37:单个异步操作可以使用try…catch来捕获异常,而对于一个以上的异步操作,使用try…catch仅仅可以从底层的AggregateException对象中获得第一个异常,为了获得所有的异常,可以使用AggregateException的Flatten()方法将层级异常放入一个列表,并从中提取出所有的底层异常。
    • Tips38:通过Task实例的ConfigureAwait()方法,可以设置使用await时同步上下文的行为,默认情况下,await操作符会尝试捕捉同步上下文,并在其中执行代码,即调度器会向UI线程投入成千上百个后续操作任务,这会使用它的消息循环来异步地执行这些任务,当我们不需要在UI线程中运行这些代码时,向ConfigureAwait方法传入false将会是一个更高效的方案。
    • Tips39:async void方法会导致异常处理方法,会放置到当前的同步上下文中,因此线程池中未被处理的异常会终结整个进程,使用AppDomain.UnhandledException事件可以拦截未处理的异常,但不能从拦截的地方恢复进程,async void的lambda表达式,同Action类型是兼容的,强烈建议仅仅在UI事件处理器中使用async void方法,在其他情况下,请使用返回Task或者Task的方法。

    使用并行集合

    • Tips40:ConcurrentQueue使用了原子的比较和交换(CAS),以及SpinWait来保证线程安全,它实现了一个先进先出(FIFO)的集合,这意味着元素出队列的顺序与加速队列的顺序是一致的,可以调用Enqueue方法向对接中加入元素,调用TryDequeue方法试图取出队列中第一个元素,调用TryPeek方法试图得到第一个元素但并不从队列中删除该元素。
    • Tips41:ConcurrentStack的实现同样没有使用锁,仅采用了CAS操作,它是一个后进先出(LIFO)的集合,这意味着最后添加的元素会先返回,可以调用Push和PushRange方法添加元素,使用TryPop和TryPopRange方法获取元素,使用TryPeek方法检查元素。
    • Tips42:ConcurrentBag是一个支持重复元素的无序集合,它针对以下情况进行了优化,即多个线程以这样的方式工作:每个线程产生和消费其自身的任务,极少发生线程间的交互(因为要交互就要使用锁)。可以调用Add方法添加元素,调用TryPeek方法检查元素,调用TryTake方法获取元素。
    • Tips43:ConcurrentDictionary是一个线程安全的字典集合的实现,对于读操作无需使用锁,对于写操作则需要使用锁,该并发字典使用多个锁,在字典桶之上实现了一个细粒度的锁模型(使用锁的常规字典称为粗粒度锁),参数concurrentLevel可以在构造函数中定义锁的数量。这意味着预估的线程数量将并发地更新该字典。由于并发字典使用锁,如无必要请避免使用以下操作:Count、IsEmpty、Keys、Values、CopyTo及ToArray,因为需要获取该字典中的所有锁。
    • Tips44:BlockingCollection是一个针对IProducerConsumerCollection泛型接口实现的高级封装,它有很多先进的功能来实现管道场景,即当你有一些步骤需要使用之前步骤运行的结果时。BlockingCollection类支持分块、调整内部集合容量、取消集合操作、从多个块集合中获取元素等。
    • Tips45:对BlockingCollection进行迭代时,需要注意的是,使用GetConsumingEnumerable()进行迭代,因为虽然BlockingCollection实现了IEnumerable接口,但是它默认的行为是表示集合的“快照”,这不是我们期望的行为。

    使用PLINQ

    • Tips46:将程序分割成一组任务并使用不同的线程来运行不同的任务,这种方式被称为任务并行
      将数据分割成较小的数据块,对这些数据进行并行计算,然后聚合这些计算结果,这种编程模型称为数据并行
    • Tips47:结构并行确实更易维护,应该尽可能地使用,但它并不是万能的。通常有很多情况我们是不能简单地使用结构并行,那么以非结构化的方式使用TPL任务并行也是完全可以的。
      Parallel类中的Invoke方法是最简单的实现多任务并行的方法,Invoke方法会阻塞其它线程直到所有线程都完成。
    • Tips48:Parallel类中的For和ForEach方法可以定义并行循环,通过传入一个委托来定义每个循环项的行为,并得到一个结果来说明循环是否成功完成,ParallelOptions类可以为并行循环定义最大并行数,使用CollectionToken取消任务,使用TaskScheduler类调度任务。
    • Tips49:ParallelLoopState可以用于从循环中跳出或者检查循环状态,它有两种方式:Break和Stop,Stop是指循环停止处理任何工作,而Break是指停止其之后的迭代,继续保持其之前的迭代工作。
    • Tips50:同Task类似,当使用AsParallel()方法并行查询时,我们将得到AggregateException,它将包含运行PLINQ期间发生的所有异常,我们可以使用Flatten()方法和Handle()方法来处理这些异常。
    • Tips51:ParallelEnumerable类含有PLINQ的全部逻辑,并且作为IEnumerable集合功能的一组扩展方法,默认情况下结果会被合并单个线程中,我们可以通过ForAll方法来指定处理逻辑,此时它们使用的是同一个线程,将跳过合并结果的过程,除了AsParallel()方法,我们同样可以使用AsSequential()方法,来使得PLINQ查询以顺序方式执行(相对于并行)
    • Tips52:PLINQ中提供了丰富用以PLINQ查询的选项,例如WithCancellation()方法用以取消查询,这将导致引发OperationCanceledException异常,并取消剩余的工作;例如WithDegreeOfParallelism()方法用以指定执行查询时实际并行分割数,可以决定并行执行会占用多少资源及其性能如何;例如WithExecutionMode()可以重载查询执行的模式,即我们可以决定选择以顺序执行还是并行执行的方式去执行查询;例如WithMergeOptions()方法可以用以调整对查询结果的处理,默认PLINQ会将结果合并到单个线程中,因此在查询结果返回前,会缓存一定数量的结果,当发现查询花费大量时间时,更合理的方式是关闭结果缓存从而尽可能快地得到结果;例如AsOrdered()方法,用以告诉PLINQ我们希望按照集合中的顺序进行处理(并行条件下,集合中的项有可能不是按顺序被处理的)

    使用异步I/O

    • Tips53:异步I/O,对服务器而言,可伸缩性是最高优先级,这意味着单个用户消耗的资源越少越好,如果为每个用户创建多个线程,则可伸缩性并不好,在I/O密集型的场景中需要使用异步,因为不需要CPU工作,其瓶颈在磁盘上,这种执行I/O任务的方式成为I/O线程。
      在异步文件读写中,FileOptions.Asynchronous是一个非常重要的选项,无论有无此参数都可以,以异步的方式使用该文件,区别是前者仅仅是在线程池中异步委托调用,而后者可以对FileStream垒使用异步I/O。
    • Tips54:对HttpListener类,我们可以通过GetContextasync()方法来异步地获取上下文。
    • Tips55:对数据库而言,我们可以通过OpenAsync()、ExecuteNonQueryAsync()等方法异步地执行SQL语句。

    好了,以上就是这篇读书笔记的主要内容啦,听说掌握了这55条Tips的人,都敢在简历上写”精通多线程编程“,哈哈,晚安啦,各位!

    展开全文
  • C#线程编程

    2019-06-20 16:02:12
    线程线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时...
  • .NET将关于多线程的功能定义在System.Threading名字空间中。因此,要使用多线程,必须先声明引用此名字空间(using System.Threading;)。 a.启动线程 顾名思义,“启动线程”就是新建并启动一个线程的意思,...
  • 【文章标题】: 乱涂C#线程02 【文章作者】: 有酒醉 【作者邮箱】: wuqr32@sina.com 【下载地址】: 自己搜索下载 【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教! ----------------------...
  • 一、为什么要线程同步? 多个线程同时使用共享对象会造成很多问题,同步这些线程使得对共享对象的操作能够以正确的顺序执行是非常重要的。 二、实现线程同步的方法: • 使用Mutex类 • 使用SemaphoreSlim类 • ...
  • C#线程和异步

    2018-05-24 14:48:02
    一、使用计时器在某些情况下,可能不需要使用单独的线程。如果应用程序需要定期执行简单的与 UI 有关的操作,则应该考虑使用进程计时器。有时,在智能客户端应用程序中使用进程计时器,以达到下列目:• 按计划定期...
  • C#线程

    2016-03-30 19:11:29
    高手讲解C#线程揭秘教程 www.111cn.net 编辑:edit02_lz 来源:转载 本教程是高手整理的C#线程详解,内容将通过一些实例来说明.net中如何实现多线程,主要内容有:线程概念、如何实现多线程、如何确保线程...
  • C#获得当前线程的ID号

    2019-02-27 10:31:03
    C# 获得当前 进程 或 线程的ID 如果获得当前进程的Id用: Process[] processes = Process.GetProcesses();  foreach(Process process in processes)  {  if(process.ProcessName == "进程"  {...
  • C#线程之旅(1)

    2017-05-30 18:48:22
    一、多线程介绍二、Join 和Sleep三、线程怎样工作四、线程和进程五、线程的使用和误用   原文地址:C#线程之旅(1)——介绍和基本概念 C#线程之旅目录: C#线程之旅(1)——介绍和基本概念 C#...
  • C#发起一个线程以后,经常需要给线程传递一些参数。总结了几种启动线程传递参数的方法。传递参数1、通过构造函数传递参数MyClass obj = new MyClass(a,b); Thread t = new Thread(new ThreadStart(obj.ThreadMethod)...
  • Console.WriteLine("多线程获得最小值为: {0}, 计时器3共耗时:{1}/ms!\n", MinValue, ts2.TotalMilliseconds); Console.ReadKey(); } public static void thFindMinElement...
  • C#线程防止卡死

    2016-08-19 16:29:59
    软件界面的响应特性是判断一款软件的非常重要的方面。一般来说,不管你软件功能做得有多么奇妙,如果软件有一点点死机的感觉都会让用户感到很讨厌,甚至怀疑你软件里是否藏有更大的...不过,使用多线程比使用单一线程
  • C#线程(上)

    2016-08-24 16:31:29
    本文主要从线程的基础用法,CLR线程池当中工作者线程与I/O线程的开发,并行操作PLINQ等多个方面介绍多线程的开发。 其中委托的BeginInvoke方法以及回调函数最为常用。 而 I/O线程可能容易遭到大家的忽略,其实在...
  • C#线程优先级浅析

    2019-07-13 02:04:39
    C#线程优先级的必要性:如果在应用程序中有多个线程在运行,但一些线程比另一些线程重要,该怎么办在这种情况下,可以在一个进程中为不同的线程指定不同的优先级。一般情况下,如果有优先级较高的线程在工作,就不会...
  • 入门线程小例子C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行。一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为“主线程”)自动创建的,并具有多...
  • C#线程系列

    2018-03-16 16:34:27
    个人感觉C#的编程,除了对算法和类库的使用以外,达到一定的水平以后,多线程的使用将会成为一个很大的瓶颈。所以重新花费时间读了一本书:《.net 4.0面向对象编程漫谈(应用篇)》里面关于多线程的描述。自己做了一...
1 2 3 4 5 ... 20
收藏数 61,841
精华内容 24,736