c#多线程总结(纯干货)

栏目:云星空知识作者:金蝶来源:金蝶云社区发布:2024-09-16浏览:1

c#多线程总结(纯干货)

c#多线程总结(纯干货)

线程基础

创建线程

 View Code

暂停线程

 View Code

工作原理

  当程序运行时,会创建一个线程,该线程会执行PrintNumbersWithDelay方法中的代码。然后会立即执行PrintNumbers方法。关键之处在于在PrintNumbersWithDelay方法中加入了Thread.Sleep方法调用。这将导致线程执行该代码时,在打印任何数字之前会等待指定的时间(本例中是2秒钟),当线程处于休眠状态时,它会占用尽可能少的CPU时间。结果我们4·会发现通常后运行的PrintNumbers方法中的代码会比独立线程中的PrintNumbersWithDelay方法中的代码先执行。

线程等待

 View Code

工作原理

  当程序运行时,启动了一个耗时较长的线程来打印数字,打印每个数字前要等待两秒。但我们在主程序中调用了t.Join方法,该方法允许我们等待直到线程t完成。当线程t完成 "时,主程序会继续运行。借助该技术可以实现在两个线程间同步执行步骤。第一个线程会等待另一个线程完成后再继续执行。第一个线程等待时是处于阻塞状态(正如暂停线程中调用 Thread.Sleep方法一样),

终止线程

 View Code

工作原理

  当主程序和单独的数字打印线程运行时,我们等待6秒后对线程调用了t.Abort方法。这给线程注入了ThreadAbortException方法,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目-标线程可以通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。因此并不推荐使用,Abort方法来关闭线程。可优先使用一些其他方法,比如提供一个CancellationToken方法来,取消线程的执行。

监测线程状态

 View Code

工作原理

  当主程序启动时定义了两个不同的线程。一个将被终止,另一个则会成功完成运行。线,.程状态位于Thread对象的ThreadState属性中。ThreadState属性是一个C#枚举对象。刚开始线程状态为ThreadState.Unstarted,然后我们启动线程,并估计在一个周期为30次迭代的,区间中,线程状态会从ThreadState.Running变为ThreadState. WaitSleepJoin。

请注意始终可以通过Thread.CurrentThread静态属性获得当前Thread对象。

  如果实际情况与以上不符,请增加迭代次数。终止第一个线程后,会看到现在该线程状态为ThreadState.Aborted,程序也有可能会打印出ThreadState.AbortRequested状态。这充分说明了同步两个线程的复杂性。请记住不要在程序中使用线程终止。我在这里使用它只是为 ,了展示相应的线程状态。

  最后可以看到第二个线程t2成功完成并且状态为ThreadState.Stopped。另外还有一些其,他的线程状态,但是要么已经被弃用,要么没有我们实验过的几种状态有用。

线程优先级

 View Code

工作原理

  当主程序启动时定义了两个不同的线程。第一个线程优先级为ThreadPriority.Highest,即具有最高优先级。第二个线程优先级为ThreadPriority.Lowest,即具有最低优先级。我们先, ,打印出主线程的优先级值,然后在所有可用的CPU核心上启动这两个线程。如果拥有一个1以上的计算核心,将在两秒钟内得到初步结果。最高优先级的线程通常会计算更多的迭代.但是两个值应该很接近。然而,如果有其他程序占用了所有的CPU核心运行负载,结果则会截然不同。

  为了模拟该情形,我们设置了ProcessorAffinity选项,让操作系统将所有的线程运,行在单个CPU核心(第一个核心)上。现在结果完全不同,并且计算耗时将超过2秒钟。 .这是因为CPU核心大部分时间在运行高优先级的线程,只留给剩下的线程很少的时间来,运行。

  请注意这是操作系统使用线程优先级的一个演示。通常你无需使用这种行为编写程序。

前台线程和后台线程

 View Code

工作原理

  当主程序启动时定义了两个不同的线程。默认情况下,显式创建的线程是前台线程。通过手动的设置threadTwo对象的IsBackground属性为ture来创建一个后台线程。通过配置来实现第一个线程会比第二个线程先完成。然后运行程序。

  第一个线程完成后,程序结束并且后台线程被终结。这是前台线程与后台线程的主要区,别:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。

  一个重要注意事项是如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。

向线程传递参数

 View Code

工作原理

  当主程序启动时,首先创建了ThreadSample类的一个对象,并提供了一个迭代次数。然后使用该对象的CountNumbers方法启动线程。该方法运行在另一个线程中,但是使用数 ,字10,该数字是通过ThreadSample对象的构造函数传入的。因此,我们只是使用相同的间接方式将该迭代次数传递给另一个线程。

  另一种传递数据的方式是使用Thread.Start方法。该方法会接收一个对象,并将该对象,传递给线程。为了应用该方法,在线程中启动的方法必须接受object类型的单个参数。在创建threadTwo线程时演示了该方式。我们将8作为一个对象传递给了Count方法,然后 Count方法被转换为整型。

  接下来的方式是使用lambda表达式。lambda表达式定义了一个不属于任何类的方法。我们创建了一个方法,该方法使用需要的参数调用了另一个方法,并在另一个线程中运行该 ,方法。当启动threadThree线程时,打印出了12个数字,这正是我们通过lambda表达式传递,的数字。

  使用lambda表达式引用另一个C#对象的方式被称为闭包。当在lambda表达式中使用任何局部变量时, C#会生成一个类,并将该变量作为该类的一个属性。所以实际上该方式与 threadOne线程中使用的一样,但是我们无须定义该类, C#编译器会自动帮我们实现。

  这可能会导致几个问题。例如,如果在多个lambda表达式中使用相同的变量,它们会共享该变量值。在前一个例子中演示了这种情况。当启动threadFour和threadFive线程时,.它们都会打印20,因为在这两个线程启动之前变量被修改为20。

使用C#中的lock关键字

 View Code

工作原理

  当主程序启动时,创建了一个Counter类的对象。该类定义了一个可以递增和递减的简,单的计数器。然后我们启动了三个线程。这三个线程共享同一个counter实例,在一个周期中进行一次递增和一次递减。这将导致不确定的结果。如果运行程序多次,则会打印出多个不同的计数器值。结果可能是0,但大多数情况下则不是0.

  这是因为Counter类并不是线程安全的。当多个线程同时访问counter对象时,第一个线程得到的counter值10并增加为11,然后第二个线程得到的值是11并增加为12,第一个线程得到counter值12,但是递减操作发生前,第二个线程得到的counter值也是12,然后 , 第一个线程将12递减为11并保存回counter中,同时第二个线程进行了同样的操作。结果,我们进行了两次递增操作但是只有一次递减操作,这显然不对。这种情形被称为竞争条件, (race condition),竞争条件是多线程环境中非常常见的导致错误的原因。

  为了确保不会发生以上情形,必须保证当有线程操作counter对象时,所有其他线程必须等待直到当前线程完成操作。我们可以使用lock关键字来实现这种行为。如果锁定了一个对象,需要访问该对象的所有其他线程则会处于阻塞状态,并等待直到该对象解除锁定。这,可能会导致严重的性能问题,在第2章中将会进一步学习该知识点。

使用Monitor类锁定资源

 View Code

工作原理

  先看看LockTooMuch方法。在该方法中我们先锁定了第一个对象,等待一秒后锁定了 ,第二个对象。然后在另一个线程中启动该方法。最后尝试在主线程中先后锁定第二个和第一个对象。

  如果像该示例的第二部分一样使用lock关键字,将会造成死锁。第一个线程保持对, lock1对象的锁定,等待直到lock2对象被释放。主线程保持对lock2对象的锁定并等待直到。lock1对象被释放,但lock1对象永远不会被释放。

  实际上lock关键字是Monitor类用例的一个语法糖。如果我们分解使用了lock关键字的代码,将会看到它如下面代码片段所示:

 View Code

  因此,我们可以直接使用Monitor类。其拥有TryEnter方法,该方法接受一个超时, "参数。如果在我们能够获取被lock保护的资源之前,超时参数过期,则该方法会返回 false.

处理异常

 View Code

工作原理

  当主程序启动时,定义了两个将会抛出异常的线程。其中一个对异常进行了处理,另一个则没有。可以看到第二个异常没有被包裹启动线程的try/catch代码块捕获到。所以如果直接使用线程,一般来说不要在线程中抛出异常,而是在线程代码中使用try/catch代码块。

  在较老版本的.NET Framework中(1.0和1.1),该行为是不一样的,未被捕获的异常不会强制应用程序关闭。可以通过添加一个包含以下代码片段的应用程序配置文件(比如app config)来使用该策略。

 View Code

 

线程同步

  正如前面所看到的一样,多个线程同时使用共享对象会造成很多问题。同步这些线程使得对共享对象的操作能够以正确的顺序执行是非常重要的。在使用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是一种原始的同步方式,其只对一个线程授予对共享资源的独占访问。

 View Code

工作原理

  当主程序启动时,定义了一个指定名称的互斥量,设置initialOwner标志为false。这意.味着如果互斥量已经被创建,则允许程序获取该互斥量。如果没有获取到互斥量,程序则简单地显示Running,等待直到按下了任何键,然后释放该互斥量并退出。

  如果再运行同样一个程序,则会在5秒钟内尝试获取互斥量。如果此时在第一个程序中,按下了任何键,第二个程序则会开始执行。然而,如果保持等待5秒钟,第二个程序将无法,获取到该瓦斥量。

使用SemaphoreSlim类

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

 View Code

工作原理

  当主程序启动时,创建了SemaphoreSlim的一个实例,并在其构造函数中指定允许的并发线程数量。然后启动了6个不同名称和不同初始运行时间的线程。

  每个线程都尝试获取数据库的访问,但是我们借助于信号系统限制了访问数据库的并发,数为4个线程。当有4个线程获取了数据库的访问后,其他两个线程需要等待,直到之前线,程中的某一个完成工作并调用semaphore.Release方法来发出信号。

  这里我们使用了混合模式,其允许我们在等待时间很短的情况下无需使用上下文切换。然而,有一个叫作Semaphore的SemaphoreSlim类的老版本。该版本使用纯粹的内核时间 ( kernel-time)方式。一般没必要使用它,除非是非常重要的场景。我们可以创建一个具名的semaphore,就像一个具名的mutex一样,从而在不同的程序中同步线程。SemaphoreSlim并不使用Windows内核信号量,而且也不支持进程间同步。所以在跨程序同步的场景下可以使用Semaphore.

使用AutoResetEvent类

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

 View Code

工作原理

  当主程序启动时,定义了两个AutoResetEvent实例。其中一个是从子线程向主线程发信号,另一个实例是从主线程向子线程发信号。我们向AutoResetEvent构造方法传人false,定义了这两个实例的初始状态为unsignaled。这意味着任何线程调用这两个对象中的任何一个的WaitOne方法将会被阻塞,直到我们调用了Set方法。如果初始事件状态为true,那么 AutoResetEvent实例的状态为signaled,如果线程调用WaitOne方法则会被立即处理。然后事件状态自动变为unsignaled,所以需要再对该实例调用一次Set方法,以便让其他的线程对,该实例调用WaitOne方法从而继续执行。

  然后我们创建了第二个线程,其会执行第一个操作10秒钟,然后等待从第二个线程发,出的信号。该信号意味着第一个操作已经完成。现在第二个线程在等待主线程的信号。我们对主线程做了一些附加工作,并通过调用mainEvent.Set方法发送了一个信号。然后等待从第二个线程发出的另一个信号。

  AutoResetEvent类采用的是内核时间模式,所以等待时间不能太长。使用ManualResetEventslim类更好,因为它使用的是混合模式。

使用ManualResetEventSlim类

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

 View Code

工作原理

  当主程序启动时,首先创建了ManualResetEventSlim类的一个实例。然后启动了三个线程,等待事件信号通知它们继续执行。

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

使用CountdownEvent类

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

 View Code

工作原理

  当主程序启动时,创建了一个CountdownEvent实例,在其构造函数中指定了当两个操,作完成时会发出信号。然后我们启动了两个线程,当它们执行完成后会发出信号。一旦第二个线程完成,主线程会从等待CountdownEvent的状态中返回并继续执行。针对需要等待多,个异步操作完成的情形,使用该方式是非常便利的。

  然而这有一个重大的缺点。如果调用countdown.Signal()没达到指定的次数,那么-countdown. Wait()将一直等待。请确保使用CountdownEvent时,所有线程完成后都要调用,Signal方法

使用Barrier类

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

 View Code

工作原理

  我们创建了Barrier类,指定了我们想要同步两个线程。在两个线程中的任何一个调用了-barrier.SignalAndWait方法后,会执行一个回调函数来打印出阶段。

  每个线程将向Barrier发送两次信号,所以会有两个阶段。每次这两个线程调用Signal AndWait方法时, Barrier将执行回调函数。这在多线程迭代运算中非常有用,可以在每个迭代,结束前执行一些计算。当最后一个线程调用SignalAndWait方法时可以在迭代结束时进行交互。

使用ReaderWriterLockSlim类

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

 View Code

工作原理

  当主程序启动时,同时运行了三个线程来从字典中读取数据,还有另外两个线程向该字典中写入数据。我们使用ReaderWriterLockSlim类来实现线程安全,该类专为这样的场景而设计。

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

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

  主线程在所有后台线程完成后会等待30秒。

使用SpinWait类

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

 View Code

工作原理

  当主程序启动时,定义了一个线程,将执行一个无止境的循环,直到20毫秒后主线程,设置_isCompleted变量为true,我们可以试验运行该周期为20-30秒,通过Windows任务管理器测量CPU的负载情况。取决于CPU内核数量,任务管理器将显示一个显著的处理时间。

  我们使用volatile关键字来声明isCompleted静态字段。Volatile关键字指出一个字段可能会被同时执行的多个线程修改。声明为volatile的字段不会被编译器和处理器优化为只能被单个线程访问。这确保了该字段总是最新的值。

  然后我们使用了SpinWait版本,用于在每个迭代打印一个特殊标志位来显示线程是否切换为阻塞状态。运行该线程5毫秒来查看结果。刚开始, SpinWait尝试使用用户模式,在9 个迭代后,开始切换线程为阻塞状态。如果尝试测量该版本的CPU负载,在Windows任务管理器将不会看到任何CPU的使用。

使用线程池

简介

  在之前的章节中我们讨论了创建线程和线程协作的几种方式。现在考虑另一种情况,即只花费极少的时间来完成创建很多异步操作。创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。

  为了解决该问题,有一个常用的方式叫做池( pooling),线程池可以成功地适应于任何需要大量短暂的开销大的资源的情形。我们事先分配一定的资源,将这些资源放入到资源池。每次需要新的资源,只需从池中获取一个,而不用创建一个新的。当该资源不再被使用,时,就将其返回到池中。

  .NET线程池是该概念的一种实现。通过System.Threading.ThreadPool类型可以使用线程池。线程池是受,NET通用语言运行时( Common Language Runtime,简称CLR)管理的。这意味着每个CLR都有一个线程池实例。ThreadPool类型拥有一个QueueUserWorkItem静态方法。该静态方法接受一个委托,代表用户自定义的一个异步操作。在该方法被调用后,委,托会进入到内部队列中。如果池中没有任何线程,将创建一个新的工作线程( worker thread) 并将队列中第一个委托放入到该工作线程中。如果想线程池中放入新的操作,当之前的所有操作完成后,很可能只需重用一个线程来执行这些新的操作。然而,如果放置新的操作过快,线程池将创建更多的线程来执行这些操,作。创建太多的线程是有限制的,在这种情况下新的操作将在队列中等待直到线程池中的工作线程有能力来执行它们。

  当停止向线程池中放置新操作时,线程池最终会删除一定时间后过期的不再使用的线程。这将释放所有那些不再需要的系统资源。我想再次强调线程池的用途是执行运行时间短的操作。使用线程池可以减少并行度耗费,及节省操作系统资源。

  我们只使用较少的线程,但是以比平常更慢的速度来执行异步操作, ,使用一定数量的可用的工作线程批量处理这些操作。如果操作能快速地完成则比较适用线程!池,但是执行长时间运行的计算密集型操作则会降低性能。

  另一个重要事情是在ASPNET应用程序中使用线程池时要相当小心。ASPNET基础设施使用自己的线程池,如果在线程池中浪费所有的工作线程, Web服务器将不能够服务新的请求。在ASPNET中只推荐使用输入/输出密集型的异步操作,因为其使用了一个不同的方式,叫做IO线程。

  在本章中,我们将学习使用线程池来执行异步操作。本章将覆盖将操作放入线程池的不,,同方式,以及如何取消一个操作,并防止其长时间运行。

  保持线程中的操作都是短暂的是非常重要的。不要在线程池中放入长时间运行的操作,或者阻塞工作线程。这将导致所有工作线程变得繁忙,从而无法服务用户操作。这会导致性能问题和非常难以调试的错误。

  请注意线程池中的工作线程都是后台线程。这意味着当所有的前台线程(包括主程序线程)完成后,所有的后台线程将停止工作。

在线程池中调用委托

  本节将展示在线程池中如何异步的执行委托。另外,我们将讨论一个叫做异步编程模型(Asynchronous Programming Model,简称APM)的方式,这是NET历史中第一个异步编程模式

 View Code

工作原理

  当程序运行时,使用旧的方式创建了一个线程,然后启动它并等待完成。由于线程的构造函数只接受一个无任何返回结果的方法,我们使用了lambda表达式来将对Test方法的调用包起来。我们通过打印出Thread. CurrentThread.IsThreadPoolThread属性值来确,保该线程不是来自线程池。我们也打印出了受管理的线程ID来识别代码是被哪个线程执行的。

  然后定义了一个委托并调用Beginlnvoke方法来运行该委托。BeginInvoke方法接受一个回调函数。该回调函数会在异步操作完成后会被调用,并且一个用户自定义的状态会传给该回调函数。该状态通常用于区分异步调用。结果,我们得到了一个实现了IAsyncResult接口的result对象。BeginInvoke立即返回了结果,当线程池中的工作线程在执行异步操作时,仍允许我们继续其他工作。当需要异步操作的结果时,可以使用BeginInvoke方法调用返回的result对象。我们可以使用result对象的IsCompleted属性轮询结果。但是在本例子中,使用的是AsyncWaitHandle属性来等待直到操作完成。当操作完成后,会得到一个结果,可以通过委托调用EndInvoke方法,将IAsyncResult对象传递给委托参数。

  事实上使用AsyncWaitHandle并不是必要的。如果注释掉r.AsyncWaitHandle.WaitOne,代码照样可以成功运行, 因为EndInvoke方法事实上会等待异步操作完成。调用 "EndInvoke方法(或者针对其他异步API的EndOperationName方法)是非常重要的, '因为该方法会将任何未处理的异常抛回到调用线程中。当使用这种异步API时,请确保始终调用了Begin和End方法。

  当操作完成后,传递给BeginInvoke方法的回调函数将被放置到线程池中,确切地说是,一个工作线程中。如果在Main方法定义的结尾注释掉Thread.Sleep方法调用,回调函数将不,会被执行。这是因为当主线程完成后,所有的后台线程会被停止,包括该回调函数。对委托和回调函数的异步调用很可能会被同一个工作线程执行。通过工作线程ID可以容易地看出。使用BeginOperationName/EndOperationName方法和.NET中的IAsyncResult对象等方 ,式被称为异步编程模型(或APM模式),这样的方法对被称为异步方法。该模式也被应用于多个,NET类库的API中,但在现代编程中,更推荐使用任务并行库( Task Parallel Library,简称TPL)来组织异步API

向线程池中放入异步操作

 View Code

工作原理

  首先定义了AsyncOperation方法,其接受单个object类型的参数。然后使用QueueUser WorkItem方法将该方法放到线程池中。接着再次放入该方法,但是这次给方法调用传入了一个状态对象。该对象将作为状态参数传递给AsynchronousOperation方法。

  在操作完成后让线程睡眠一秒钟,从而让线程池拥有为新操作重用线程的可能性。如果注释掉所有的Thread.Sleep调用,那么所有打印出的线程ID多半是不一样的。如果ID是一样的,那很可能是前两个线程被重用来运行接下来的两个操作。

  首先将一个lambda表达式放置到线程池中。这里没什么特别的。我们使用了labmbda表达式语法,从而无须定义一个单独的方法。

  然后,我们使用闭包机制,从而无须传递lambda表达式的状态。闭包更灵活,允许我,们向异步操作传递一个以上的对象而且这些对象具有静态类型。所以之前介绍的传递对象给,方法回调的机制既冗余又过时。在C#中有了闭包后就不再需要使用它了。

线程池与并行度

  本节将展示线程池如何工作于大量的异步操作,以及它与创建大量单独的线程的方式有何不同。

 View Code

工作原理

  当主程序启动时,创建了很多不同的线程,每个线程都运行一个操作。该操作打印出线,程ID并阻塞线程100毫秒。结果我们创建了500个线程,全部并行运行这些操作。虽然在,我的机器上的总耗时是300毫秒,但是所有线程消耗了大量的操作系统资源。

  然后我们使用了执行同样的任务,只不过不为每个操作创建一个线程,而将它们放入到线程池中。然后线程池开始执行这些操作。线程池在快结束时创建更多的线程,但是仍然花,费了更多的时间,在我机器上是12秒。我们为操作系统节省了内存和线程数,但是为此付,出了更长的执行时间。

实现一个取消选项

  .本节将通过一个示例来展示如何在线程池中取消异步操作。

 View Code

工作原理

  本节中介绍了CancellationTokenSource和CancellationToken两个新类。它们在.NET4.0被引人, 目前是实现异步操作的取消操作的事实标准。由于线程池已经存在了很长时间,并,没有特殊的API来实现取消标记功能,但是仍然可以对线程池使用上述API。

  在本程序中使用了三种方式来实现取消过程。第一个是轮询来检查CancellationToken.IsCancellationRequested属性。如果该属性为true,则说明操作需要被取消,我们必须放弃该操作。

  第二种方式是抛出一个OperationCancelledException异常。这允许在操作之外控制取消过程,即需要取消操作时,通过操作之外的代码来处理。

  最后一种方式是注册一个回调函数。当操作被取消时,在线程池将调用该回调函数。这允许链式传递一个取消逻辑到另一个异步操作中。

在线程池中使用等待事件处理器及超时

  本节将描述如何在线程池中对操作实现超时,以及如何在线程池中正确地等待。

 View Code

工作原理

  线程池还有一个有用的方法: ThreadPool.RegisterWaitForSingleObject,该方法允许我们将回调函数放入线程池中的队列中。当提供的等待事件处理器收到信号或发生超时时,该回调函数将被调用。这允许我们为线程池中的操作实现超时功能。

  首先按顺序向线程池中放入一个耗时长的操作。它运行6秒钟然后一旦成功完成,会设置一个ManualResetEvent信号类。其他的情况下,比如需要取消操作,则该操作会被丢弃。 .

  然后我们注册了第二个异步操作。当从ManualResetEvent对象接受到一个信号后,该异步操作会被调用。如果第一个操作顺利完成,会设置该信号量。另一种情况是第一个操作还未完成就已经超时。如果发生了该情况,我们会使用CancellationToken来取消第一个操作。

  最后,为操作提供5秒的超时时间是不够的。这是因为操作会花费6秒来完成,只能取消该操作。所以如果提供7秒的超时时间是可行的,该操作会顺利完成。

  当有大量的线程必须处于阻塞状态中等待一些多线程事件发信号时,以上方式非常有,用。借助于线程池的基础设施,我们无需阻塞所有这样的线程。可以释放这些线程直到信号事件被设置。在服务器端应用程序中这是个非常重要的应用场景,因为服务器端应用程序要求高伸缩性及高性能。

使用计时器

  本节将描述如何使用System.Threading. Timer对象来在线程池中创建周期性调用的异步

 View Code

工作原理

  我们首先创建了一个Timer实例。第一个参数是一个1ambda表达式,将会在线程池中被执行。我们调用TimerOperation方法并给其提供一个起始时间。由于无须使用用户状态对象,所以第二个参数为null,然后指定了什么时候会第一次运行TimerOperation,以及之后 "再次调用的间隔时间。所以第一个值实际上说明一秒后会启动第一次操作,然后每隔两秒再,次运行。

  之后等待6秒后修改计时器。在调用timer.Change方法一秒后启动TimerOperation,然后每隔4秒再次运行。

  计时器还可以更复杂:可以以更复杂的方式使用计时器。比如,可以通过Timeout.Infinet值提供给计时器个间隔参数来只允许计时器操作一次。然后在计时器异步操作内,能够设置下一次计,时器操作将被执行的时间。具体时间取决于自定义业务逻辑。

使用BackgroundWorker组件

 View Code

工作原理

  当程序启动时,创建了一个BackgroundWorker组件的实例。显式地指出该后台工作线,程支持取消操作及该操作进度的通知。

  接下来是最有意思的部分。我们没有使用线程池和委托,而是使用了另一个C#语法,称为事件。事件表示了一些通知的源或当通知到达时会有所响应的一系列订阅者。在本例中,我们将订阅三个事件,当这些事件发生时,将调用相应的事件处理器。当事件通知其订,阅者时,具有特殊的定义签名的方法将被调用。

  因此,除了将异步API组织为Begin/End方法对,还可以只启动一个异步操作然后订阅给不同的事件。这些事件在该操作执行时会被触发。这种方式被称为基于事件的异步模式, ( Event-based Asynchronous Pattern,简称EAP)。这是历史上第二种用来构造异步程序的方,式,现在更推荐使用TPL

  我们共定义了三个事件。第一个是oWork事件。当一个后台工作对象通过RunWorkerAsync方法启动一个异步操作时,该事件处理器将被调用。该事件处理器将会运行在线程池中。如果需要取消操作,则这里是主要的操作点来取消执行。同时也可以提供该操作的运行进程信,息。最后,得到结果后,将结果设置给事件参数,然后RunWorkerCompleted事件处理器将,被调用。在该方法中,可以知道操作是成功完成,还是发生错误,抑或被取消。

  基于此, BackgroundWorker组件实际上被使用于Windows窗体应用程序(Windows Forms Applications,简称WPF)中。该实现通过后台工作事件处理器的代码可以直接与UI控制器交互。与线程池中的线程与UI控制器交互的方式相比较,使用BackgroundWorker组件的方式更加自然和好用。

使用任务并行库

简介

  我们在之前的章节中学习了什么是线程,如何使用线程,以及为什么需要线程池。使用线程池可以使我们在减少并行度花销时节省操作系统资源。我们可以认为线程池是一个抽象层,其向程序员隐藏了使用线程的细节,使我们专心处理程序逻辑,而不是各种线程,问题。

  然而使用线程池也相当复杂。从线程池的工作线程中获取结果并不容易。我们需要实现,自定义方式来获取结果,而且万一有异常发生,还需将异常正确地传播到初始线程中。除此,以外,创建一组相关的异步操作,以及实现当前操作执行完成后下一操作才会执行的逻辑也不容易。在尝试解决这些问题的过程中,创建了异步编程模型及基于事件的异步模式。在第3章中提到过基于事件的异步模式。这些模式使得获取结果更容易,传播异常也更轻松,但是组,合多个异步操作仍需大量工作,需要编写大量的代码。

  为了解决所有的问题, Net Framework4.0引入了一个新的关于异步操作的API,它叫做.任务并行库( Task Parallel Library,简称TPL), .Net Framework 4.5版对该API进行了轻微的改进,使用更简单。在本书的项目中将使用最新版的TPL,即.Net Framework 4.5版中的 API, TPL可被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的APL, TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或不使用独立线程运行。在本章中将探究任务的所有使用细节。

  默认情况下,程序员无须知道任务实际上是如何执行的。TPL通过向用户隐藏任务的实现细节从而创建一个抽象层。遗憾的是,有些情况下这会导致诡秘的错误,比如试图获取任务的结果时程序被挂起。本章有助于理解TPL底层的原理,以及如何避免不恰当的使用方式。

  一个任务可以通过多种方式和其他任务组合起来。例如,可以同时启动多个任务,等待所有任务完成,然后运行一个任务对之前所有任务的结果进行一些计算。TPL与之前的模式相比,其中一个关键优势是其具有用于组合任务的便利的API,

  处理任务中的异常结果有多种方式。由于一个任务可能会由多个其他任务组成,这些任,务也可能依次拥有各自的子任务,所以有一个AggregateException的概念。这种异常可以捕获底层任务内部的所有异常,并允许单独处理这些异常。

  而且,最后但并不是最不重要的, C# 5.0已经内置了对TPL的支持,允许我们使用新的 await和async关键字以平滑的、舒服的方式操作任务。

  在本章中我们将学习使用TPL来执行异步操作。我们将学习什么是任务,如何用不同的,方式创建任务,以及如何将任务组合在一起。我们会讨论如何将遗留的APM和EAP模式转换为使用任务,还有如何正确地处理异常,如何取消任务,以及如何使多个任务同时执行。另外,还将讲述如何在Windows GUI应用程序中正确地使用任务。

创建任务

 View Code

工作原理

  当程序运行时,我们使用Task的构造函数创建了两个任务。我们传入一个lambda表达式作为Action委托。这可以使我们给TaskMethod提供一个string参数。然后使用Start方法运行这些任务。

  请注意只有调用了这些任务的Start方法,才会执行任务。很容易忘记真正启动任务。

  然后使用Task.Run和Task.Factory.StartNew方法来运行了另外两个任务。与使用Task构造函数的不同之处在于这两个被创建的任务会立即开始工作,所以无需显式地调用这些任务的Start方法。从Task 1到Task 4的所有任务都被放置在线程池的工作线程中并以未指定,的顺序运行。如果多次运行该程序,就会发现任务的执行顺序是不确定的。

  Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项。通!常如果无特殊需求,则可使用前一个方法,如Task 5所示。我们标记该任务为长时间运行,结果该任务将不会使用线程池,而在单独的线程中运行。然而,根据运行该任务的当前的任务调度程序( task scheduler)运行方式有可能不同。

使用任务执行基本的操作

  本节将描述如何从任务中获取结果值。我们将通过几个场景来了解在线程池中和主线程中运行任务的不同之处。

 View Code

工作原理

  首先直接运行TaskMethod方法,这里并没有把它封装到一个任务中。结果根据它提供给我们的主线程的信息可以得知该方法是被同步执行的。很显然它不是线程池中的线程。

  然后我们运行了Task 1,使用Start方法启动该任务并等待结果。该任务会被放置在线程池中,并且主线程会等待,直到任务返回前一直处于阻塞状态。

  Task 2和Task 1类似,除了Task 2是通过RunSynchronously()方法运行的。该任务会运行在主线程中,该任务的输出与第一个例子中直接同步调用TaskMethod的输出完全一样。这是个非常好的优化,可以避免使用线程池来执行非常短暂的操作。

  我们用以运行Task 1相同的方式来运行Task 3,但这次没有阻塞主线程,只是在该任务完成前循环打印出任务状态。结果展示了多种任务状态,分别是Creatd, Running和 RanToCompletion.

组合任务

  本节将展示如何设置相互依赖的任务。我们将学习如何创建一个任务,使其在父任务完成后才会被运行。另外,将探寻为非常短暂的任务节省线程开销的可能性。

 View Code

工作原理

  当主程序启动时,我们创建了两个任务,并为第一个任务设置了一个后续操作( continuation,一个代码块,会在当前任务完成后运行),然后启动这两个任务并等待4秒,这个时间足够两个任务完成。然后给第二个任务运行另一个后续操作,并通过指定TaskContinuationOptions."ExecuteSynchronously选项来尝试同步执行该后续操作。如果后续操作耗时非常短暂,使用以上方式是非常有用的,因为放置在主线程中运行比放置在线程池中运行要快。可以实现这一点是因为第二个任务恰好在那刻完成。如果注释掉4秒的Thread.Sleep方法,将会看到该代码被放置到线程池中,这是因为还未从之前的任务中得到结果。

  最后我们为之前的后续操作也定义了一个后续操作,但这里使用了一个稍微不同的方式,即使用了新的GetAwaiter和OnCompleted方法。这些方法是C# 5.0语言中异步机制中的方法。

  本节示例的最后部分与父子线程有关。我们创建了一个新任务,当运行该任务时,通过提供一个TaskCreationOptions.AttachedToParent选项来运行一个所谓的子任务。

  子任务必须在父任务运行时创建,并正确的附加给父任务!

  这意味着只有所有子任务结束工作,父任务才会完成。通过提供一个TaskContinuation Options选项也可以给在子任务上运行后续操作。该后续操作也会影响父任务,并且直到最后一个子任务结束它才会运行完成。

将APM模式转换成任务

  本节将说明如何将过时的APM API转换为任务。多个示例覆盖了转换过程中可能发生的不同情况。

 View Code

工作原理

  这里我们定义了两种委托。其中一个使用了out参数,因此在将APM模式转换为任务,时,与标准的TPLAPI是不兼容的。这样的转换有三个示例。

  将APM转换为TPL的关键点是Task<T>.Fa

c#多线程总结(纯干货)

c#多线程总结(纯干货)线程基础创建线程 View Code暂停线程 View Code工作原理  当程序运行时,会创建一个线程,该线程会执行PrintNumbe...
点击下载文档
确认删除?