并发和并行——从线程,线程池到任务

并发和并行

  通常我们认为,并发指的是多个事件同时发生,并行指的是多个任务一起执行。我更喜欢另一种描述,即:被调用操作的开始和结束都独立于调用它的控制流,该工作与当前的控制流同时执行,就实现了并发性。并行是指将一个问题分解为较小的部分,并异步的发起对每个部分的处理,使它们能并发的得到处理。

  另外,并发不仅仅指多线程编程,实际上多线程仅仅是并发编程的一种形式。在C#中,异步编程,并行编程,TPL数据流,响应式编程等都属于并发编程技术。

  并发编程也并非大型服务器的专利。在许多场合,我们的程序都需要保证及时的响应用户操作,尤其是读写数据,服务通信等场景。这正是并发编程的目的之一。

线程——Thread

  线程是使应用并行化和分发异步操作的最基本手段。是用户模式程序中最低级别的抽象,线程几乎没有提供结构和控制方面的支持,直接对线程进行编程,是一种较为古老的方式。

  在C#中,我们通常采用为Thread类的构造函数传入方法委托的方式创建线程,并调用Start()方法来启动线程运行。另外,Start方法还可以为线程的委托方法传递参数,我们也可以使用lambda表达式的闭包机制来传递参数。

static void Print()
{
    Console.WriteLine("Hello World");
}

Thread t = new Thread(Print);
t.Start();

线程对象的一些其它方法:

  • Sleep():告知操作系统在指定时间内不要为该线程分配时间片,线程的睡眠时间通常是不精确的。它常被用来模拟高延迟操纵,并且让处理器不必进行无意义的运算。当指定的时间为0时,表示将该线程放弃当前时间片内的剩余时间。在基于任务的异步编程中,通常使用await Task.Delay()来引入延迟。
  • Join():告知当前线程需要等待本线程结束后才能继续往下执行,参数表示最多等待多久。
  • Abort():尝试摧毁线程。它会为该线程注入ThreadAbortException异常,即使该异常被捕获并忽略,它仍会继续重新引发异常来保证线程被摧毁。但大家仍不推荐使用Abort()函数,原因有以下几种:
  1. 如果线程的控制点在finally代码块中,不会试图触发该异常,因为此时可能在执行关键的清理操作,不应被打断。在非托管代码中也不会,因为这会破坏CLR本身。CLR会等到控制点脱离上述情况后再尝试引发异常,但效果是无法保证的。
  2. 控制点可能处于lock代码块中,lock无法阻止异常发生,但是同步机制会被破坏,这会影响线程安全,带来不可预知的结果。
  3. 可能会损坏进程的数据结构或者程序的基础类库数据,引发错误。

  线程操作需要考虑的问题的不便远不止这些。比如:我们需要找出1-1000000内的所有质数,我们很容易对这个问题进行并行化。假如我们的电脑是8核,那么我们只需要开8个线程,每个线程计算125000个数字,找出哪些是质数并汇集在一起就好了。然而真的是这么简单吗?

  1. 8核计算机就应该开8个线程吗?如果是计算20以内的质数还有必要吗?
  2. 如何保证线程的工作量是相等的?计算1-125000内的质数所需的运算量和计算875001-1000000是相等吗?
  3. 如果某一个线程产生了错误,意外终止了怎么办?
  4. 各个线程运行结果汇总时的同步问题如何解决?

  这样的程序毫无伸缩性可言。要解决这些问题并不容易,任务并行库(TPL)正是这样一个框架。它能很好的控制线程数量,线程的负载,能生成可靠的结果和错误报告,并很方便的与其他线程进行协作。

  要进行手动线程管理,最自然的方式是借助线程池。

线程池——ThreadPool

  线程池是一个组件,它可以管理大量的用于执行工作项的线程。它不会为了某项任务而创建新的线程,而是将任务放置于线程池中排队,由池中的空闲线程来执行。以此分发任务,可以减少线程创建销毁带来的成本,减少线程过载等。

  例如:在求1-1000000以内的质数这个例子中,我们可以采用线程池的方式,将数据源划分成更小的块,将每一块都作为一个任务添加到线程池的运行队列中去。这样程序将具有更好的伸缩性以及性能提升。

        static IEnumerable<int> PrimesInRange(int start,int end)
        {
            List<int> primes = new List<int>();
            const int chunkSize = 100;
            int complete = 0;
            ManualResetEvent alldone = new ManualResetEvent(false);
            int chunks = (end - start) / chunkSize;
            for(int i = 0; i < chunks; i++)
            {
                int chunkStart = start + i * chunkSize;
                int chunkEnd = chunkStart + chunkSize;
                ThreadPool.QueueUserWorkItem(_ =>
                {
                    for (int number = chunkStart; number < chunkEnd; number++)
                    {
                        if (IsPrime(number))
                        {
                            lock (primes)
                            {
                                primes.Add(number);
                            }
                        }
                    }
                    if(Interlocked.Increment(ref complete) == chunks)
                    {
                        alldone.Set();
                    }
                });             
            }
            alldone.WaitOne();
            return primes;
        }

线程池的缺点

  线程池假定所有的工作运行时间都较短,因此可以尽量保证处理器全力以赴完成任务,而非无效率的通过时间片来进行多个任务,工作时间短还可以保证线程池及时的收回线程。

扫描二维码关注公众号,回复: 14708131 查看本文章

  有别于Thread和Task,线程池不提供正在执行给定工作的线程引用,这样我们无法管理线程以及与线程同步等。微软的C#团队认为,开发人员真正要做的是构建高级别的抽象,将线程和线程池作为实现细节使用。这种抽象由任务并行库来实现。

任务并行库(TPL)

什么是任务?

  TPL提供一个能够代表异步工作的对象,工作可以理解为某些需要异步执行的代码块,它被抽象为Task。它为我们提供了便利的API来与工作进行交互。这看起来与委托十分类似,委托不也是封装了一段代码的对象吗?任务其实就是将委托同步执行的方式转化成了异步。

创建一个Task对象十分简单。

  1. 使用Task构造函数创建,这在写法上类似于Thread,但它们背后的含义和实现是完全不同的。
    Task t1 = new Task(Print);
    t1.Start();
  1. 使用Task.Factory.StartNew工厂方法创建:
    Task t2 = Task.Factory.StartNew(Print);
  1. 使用Task.Run方法来创建,这种写法是Task.Factory.StartNew的快捷方式。
    Task t3 = Task.Run(Print);

等待所有任务结束:

    Task.WaitAll(new Task[] { t1, t2, t3 });

  任务并行库的出现被视为线程池上的又一个抽象,它隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的API。Task具有以下功能:

  1. 由任务调度器决定给定的任务由哪个线程来执行,默认将任务压入CLR线程池队列,但如UI线程一类的可以发送给指定线程。
  2. 可以等待任务完成并获取结果。
  3. 提供一个后续操作,通常称之为回调。不单单是任务正常完成才可以执行回调函数,我们可以为一个先驱任务指定成功时,失败时的回调任务,也可以指定该回调任务在先驱任务线程上执行还是在其他线程上执行等。
  4. 处理异常。可以处理包括单个任务,有层级的任务,以及其他相关任务的异常。
  5. 取消任务。

  另外,还有一个非常有用的辅助方法称为Parallel.Invoke,可以执行一组任务,并且等待所有任务返回。

            Parallel.Invoke
                (
                    ()=>Print(),
                    ()=>Print()
                );

猜你喜欢

转载自blog.csdn.net/qq_40404477/article/details/102165017