【学习笔记】异步编程-C#(2)

112 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情 >>

有哪些用了线程池

  • WCF、Asp.Net Core、ASMX等应用服务器
  • System.Timers.Timer、System.Threading.Timer
  • 并行编程结构
  • 。。。

线程池中的整洁

  • 如果项充分利用CPU,那么保持线程池的“整洁”是非常重要的。

超额订阅

  • CPU的超额订阅:活跃的线程超过CPU的核数,操作系统就需要对线程进行时间切片。
  • 线程池提供了另一个功能,即确保临时超出的Compute-Bound工作不会导致CPU的超额订阅。由于超额订阅需要增加时间切片,时间切片又需要昂贵的上下文切换,并且可能使CPU缓存失效(至关重要)。

CLR的策略

  • C#中使用CLR通过对任务排队并对其启动进行节流限制来避免线程池中的超额订阅。它首先运行尽可能对的并发任务,然后通过一些算法调整并发线程的级别,并在特定的方向上不断调整工作负载,具体就是确保并发线程的运行始终追随最佳的性能曲线。
  • 如果下面两点能够满足,则CLR的策略将发挥出最佳的效果:
    1. 工作项大多是短时间(<250毫秒,理想情况下<100毫秒)运行的,因此CLR有更多的机会进行测量和调整。
    2. 大部分时间都被阻塞的工作项不会主宰线程池。

Task

Thread的问题

  • Thread是用来创建concurrency(并发)的一种低级别工具,他有一些限制:
    1. Thread在进行Join的时候,很难从线程获得返回值,解决这个问题可能需要设置一些共享变量。
    2. 如果操作抛出异常,捕获和传播该异常都会很麻烦。
    3. 无法告诉线程在结束时开始做另外的工作,你必须使用Join来完成这个操作,这将会导致其他线程不能执行。
    4. 很难使用较小的并发来组建大型的并发,导致了对手动同步的更大依赖。

Task class

  • Task类是一个相对高级的并发工具,它是在System.Threading.Tasks命名空间下的,它代表了一个并发操作,可以很好地解决上述问题,它有一些操作可能由Thread支持,有可能不由Thread支持。
  • Task是可以组合的,可以使用Continuation把它们串成链。
  • Tasks可以使用线程池来减少启动延迟,它也可以利用回调的方式,在等待IO绑定操作时完全避免线程。

开始一个Task

  • 开始一个Task的最简单方法是使用Task.Run这个静态方法,只需要传入一个委托即可。
class Program
{
    static void Main(string[] args)
    {
        /**
             * 创建了一个任务,在创建任务时会自动使用线程池,线程池里面有一个线程(也就是委托实例)
             * 主线程执行完后Task中的线程还没有执行结束就被kill了,这时并不会输出线程中的那句话
             */
        Task.Run(() => { Console.WriteLine("Hello Task!"); });
        //这时要阻塞一下主线程才可以执行线程中的语句
        // Console.ReadLine();
        Thread.Sleep(1000);
    }
}
  • Task默认使用线程池,也就是后台线程:当主线程结束时,你创建的所有tasks都会结束。
  • Task.Run返回了一个Task对象,可以使用它来监视线程的执行过程
  • 使用Task.Run来启动线程的任务是“热”任务,它并不需要调用Start,而使用Task的构造器创建的任务为“冷”任务,需要使用Start来启动线程池中的线程,但很少使用冷任务。
  • 可以通过Task的Status属性来跟踪task的执行状态;

Wait

  • 调用task的Wait方法会进行阻塞直到Task中的并发操作完成,这就相当于调用了thread上的Join方法。
class Program
    {
        static void Main(string[] args)
        {
            var task = Task.Run(() =>
            {
                Thread.Sleep(3000);
                Console.WriteLine("Hello Task!");
            });
            //task的状态:WaitingToRun
            Console.WriteLine(task.Status);
            //是否完成操作:false
            Console.WriteLine(task.IsCompleted);
            //task的状态:Running
            Console.WriteLine(task.Status);
            //阻塞直至task完成操作
            task.Wait();
            //是否完成操作:true
            Console.WriteLine(task.IsCompleted);
            //task的状态:RanToCompletion
            Console.WriteLine(task.Status);
        }
    }
  • Wait也可以传入一个超时时间和一个取消指令来提前结束等待。

长时间运行的任务

  • Task比较适合短时间运行的Compute-Bound类的工作,而那种长时间运行的任务可以使用下面方式运行:
class Program
{
    static void Main(string[] args)
    {
        var task = Task.Factory.StartNew(() =>
                                         {
                                             Thread.Sleep(3000);
                                             Console.WriteLine("Hello Task!");
                                         }, TaskCreationOptions.LongRunning);
        task.Wait();
    }
}

Task的返回值

  • Task有一个泛型子类叫做Task,它指定Task的返回值类型,使用Func委托或兼容的Lambda表达式来调用Task.Run就可以得到Task,随后可以通过Result属性来获得返回的结果。
  • 如果task还没有完成操作,访问Result属性就会阻塞该线程直到task完成操作。
class Program
{
    static void Main(string[] args)
    {
        Task<int> task = Task.Run(() =>
                                  {
                                      Console.WriteLine("Foo");
                                      return 3;
                                  });
        //若task还没执行完成,访问Result将会阻塞主线程,直至task执行结束。
        var taskResult = task.Result;
        Console.WriteLine(taskResult);
    }
}
class Program
{
    static void Main(string[] args)
    {
        //2到3000000中的质数的个数
        var task = Task.Run(() =>
                            Enumerable.Range(2, 3000000).Count(n =>
                                                               Enumerable.Range(2, (int) Math.Sqrt(n) - 1).All(i => n % i > 0)));
        Console.WriteLine("Task running...");
        Console.WriteLine("The answer is " + task.Result);
    }
}
  • Task可以看作是一种所谓的future、promise(未来/许诺),早它里面包裹着一个Result,可以在Task.Run后依旧可以使用。