首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

C#多线程、异步相关

第一章

概念

并发:同时做多件事情。

多线程是并发的一种形式,它采用多个线程来执行程序。

并行处理是把正在执行的大量的任务分割成小块,分配给多个同时运行的线程。

为了让处理器的利用效率最大化,并行处理(或并行编程)采用多线程。

并行处理是多线程的一种,而多线程是并发的一种。在现代程序中,还有一种非常重要但很多人还不熟悉的并发类型:异步编程。

异步编程是并发的一种形式,它采用future模式或回调(callback)机制,以避免产生不必要的线程。

并发编程的另一种形式是响应式编程(reactive programming)。异步编程意味着程序启动一个操作,而该操作将会在一段时间后完成。响应式编程与异步编程非常类似,不过它是基于异步事件(asynchronous event)的,而不是异步操作(asynchronous operation)。异步事件可以没有一个实际的“开始”,可以在任何时间发生,并且可以发生多次,例如用户输入。

响应式编程是一种声明式的编程模式,程序在该模式中对事件做出响应。

最常见的并发场景包括:编写快速响应的用户界面。

可以处理同时出现的请求在服务器上,客户端的请求可能会并发到达,必须通过并行处理才能够保证程序的可伸缩性。如果使用ASP.NET、WCF或者Web Services,则.NET Framework会自动执行并行处理。然而,程序员仍然需要关注某些共享的状态(例如使用静态变量作为缓存)。

并行编程 如果可以将负载划分到多个核心上,那么多核、多处理器计算机就可以提升密集计算代码的执行速度。

要介绍并发编程,首先就要具备线程的基础知识,特别是线程的共享状态。

任务是一个中间层,它增加了学习的复杂性。因此最好能够从控制台应用程序(或者LINQPad)开始来直接创建线程,直到熟悉它们的工作方式再开始学习任务。

线程是抢占式的。它的执行和其他线程的代码是交错执行的。

在等待线程Sleep或者Join的过程中,线程是阻塞(blocked)的。如果防止队列重复消费问题。

如果一个操作的绝大部分时间都在等待事件的发生,则称为I/O密集,例如下载网页或者调用Console.ReadLine。(I/O密集操作一般都会涉及输入或者输出,但是这并非硬性要求。例如Thread.Sleep也是一种I/O密集的操作)。而相反的,如果操作的大部分时间都用于执行大量的CPU操作,则称为计算密集。

要么在当前线程同步进行等待,直至操作完成(例如Console.ReadLine、Thread.Sleep以及Thread.Join);要么异步进行操作,在操作完成的时候或者之后某个时刻触发回调函数。

共享可写状态可能引起间歇性错误,这也是多线程中经常被诟病的问题。我们将介绍如何通过锁机制来避免这种问题。然而,最好的方式是避免使用共享状态。我们稍后还将介绍如何通过异步编程的方式来解决这个问题。

当两个线程同时竞争一个锁时(它可以是任意引用类型的对象,这里是_locker),一个线程会进行等待(阻塞),直到锁被释放。这样,就保证了一次只有一个线程能够进入这个代码块。因此“Done”只会打印一次。在不确定的多线程上下文下,采用这种方式进行保护的代码称为线程安全的代码。

锁本身也存在一些问题(例如死锁)。锁的一个常见用途是访问那些存储频繁访问数据库对象的共享缓存。线程有三种基本状态:就绪、阻塞和运行。

其次,阻塞并非零开销。这是因为每一个线程在存活时会占用1MB的内存,并对CLR和操作系统带来持续性的管理开销。

CLR为每一个线程分配了独立的内存栈,从而保证了局部变量的隔离。

而静态字段提供了另一种在线程之间共享变量的方法。

21、进程

进程 可 包含 多个 应用 程序 域, 而 应用 程序 域 又可 以 加载 多个 程序 集, 相应 地, 应用 程序 域 也可以 划分 为 多个 上下文 区域。 对于 程序 集 而言, 默认 情况下 程序 集 Main 方法( 进程 入口) 执行 时 将 自动 创建 一个 主 线程, 如果 不作 其他 处理, 主 程序 中的 代码 都在 这个 主 线程 中 执行。

一般 情况下 CPU 相同 时间 只能 执行 一个 线程, 多 线程 程序 运行时, CPU 将 分配 时间 片 给 线程, 根据 时间 片 轮流 执行 多个 线程。 所以, 多 线程 带来 的 效果 即 创建 响应 更快 的 程序, 给用户 更好 的 体验( UserExperience)。

21、线程

默认 情况下, 手动 创建 的 线程 都是 前台 线程, 而 线程 池 中的 线程 只能 是 后台 线程。 只有 当前 台 线程 全部 结束, 应用 程序 域 才能 被 卸载( 程序 才能 关闭)。 当前 台 线程 全部 结束 后, 后台 线程 即使 没有 完成 工作, 都会 被 忽略, 即 自动 结束。 不过 如果 有 特定 需要 时, 手动 创建 的 线程 也可以 被 配置 为 后台 线程, 如以 下 代码 所示: //以下 为 创建 线程 的 代码, 假设 委托 类型 对象 引用 为 Ts, 新 线程 对象 引用 为 Td ThreadStart Ts = new ThreadStart (所指 向 方法 名称); Thread Td = new Thread( Ts); Td. IsBackground = true; Td. Start();

22、保证 代码 段 的 线程 安全

如果 其他 线程 继续 对 同一个 共享 数据 进行 操作, 则 会 影响 上个 线程 数据 的 有效性。 使用 lock 关键字 可以避免 类似 的 问题, lock 语句 结构 可 包含 需要 同步 执行 的 代码 段( 即 不被 其他 线程 打断、 连续 执行), 使用方法 如下 所示:

lock( this) { 需要 同步 的 代码 段; }

使用 Monitor 类 可以 达到 与 lock 语句 结构 相同 的 效果, 因为 lock 语句 结构 只是 Monitor 类 应用 子集 的 简写 方式。

Monitor. Enter( this)

try{ 需要 同步 的 代码 段; } finally { Monitor. Exit( this) }

.Net中线程同步可以有多种方式:

lock语句;监视器;同步事件和等待句柄;Mutex对象;

Lock语句用于给对象获取互斥锁,执行操作语句,然后再释放该锁;

object obj=new object();lock(obj){}相当于:System.Threading.Monitor.Enter(obj);try{}finally{System.Threading.Monitor.Exit(obj);}

概念

这在一定程度上说明了多线程的作用。例如,可以使用一个线程在后台获得数据,同时使用另一个线程显示所获得的数据。而这些数据就是所谓的共享状态(shared state)。

当线程由于特定原因暂停执行,那么它就是阻塞的。例如,调用Sleep休眠或者Join等待其他线程执行结束。阻塞的线程会立刻交出它的处理器时间片,并从此开始不再消耗处理器时间。直至阻塞条件结束。可以使用ThreadState属性测试线程的阻塞状态:

I/O密集和计算密集

如果一个操作的绝大部分时间都在等待事件的发生,则称为I/O密集,例如下载网页或者调用Console.ReadLine。(I/O密集操作一般都会涉及输入或者输出,但是这并非硬性要求。例如Thread.Sleep也是一种I/O密集的操作)。而相反的,如果操作的大部分时间都用于执行大量的CPU操作,则称为计算密集。

其次,阻塞并非零开销。这是因为每一个线程在存活时会占用1MB的内存,并对CLR和操作系统带来持续性的管理开销。因此,阻塞可能会给繁重的I/O密集型程序(例如要处理成百上千的并发操作)带来麻烦。因此,这些程序更适于使用回调的方式,在等待时完全解除这些线程。

我们的例子展示了共享可写状态可能引起间歇性错误,这也是多线程中经常被诟病的问题。我们将介绍如何通过锁机制来避免这种问题。然而,最好的方式是避免使用共享状态。我们稍后还将介绍如何通过异步编程的方式来解决这个问题。

锁与线程安全

锁并非解决线程安全的银弹,人们很容易忘记在访问字段时加锁,而且锁本身也存在一些问题(例如死锁)。在ASP.NET应用程序中,锁的一个常见用途是访问那些存储频繁访问数据库对象的共享缓存。

CLR为每一个线程分配了独立的内存栈,从而保证了局部变量的隔离。

Task默认使用线程池中的线程,它们都是后台线程。这意味着当主线程结束时,所有的任务也会随之停止。因此,要在控制台应用程序中运行这些例子,必须在启动任务之后阻塞主线程(例如在任务对象上调用Wait,或者调用Console.ReadLine()方法):

互斥锁和信号量

这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

描述线程与进程的区别?

个应用程序实例是一个进程(如QQ、WeChat) ,一个进程内包含一个或多个线程,线程是进程的一部分:进程之间是相互独立的,他们有各自的私有内存空间和资源,进程内的线程可以共享其所属进程的所有资源

简述后台线程和前台线程的区别?

应用程序必须运行完所有的前台线程才可以退出,或者主动结束前台线程,不管后台线程是否还在运行,应用程序都会结束,而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束

简要区别:

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

通过将 Thread.IsBackqround 设置为 true,就可以将线程指定为后台线程,主线程就是一个前台线程。

说说常用的锁,lock是一种什么样的锁?

常用的如SemaphoreSlim、ManualResetEventSlim、Monitor、ReadWriteLockslim,lock是一个混合锁,其实质是Monitor['monita]。

lock为什么要锁定一个参数,可不可锁定一个值类型? 这个参数有什么要求?

lock的锁对象要求为一个引用类型。它可以锁定值类型,但值类型会被装箱,每次装箱后的对象都不一样,会导致锁定无效。

对于lock锁,锁定的这个对象参数才是关键,这个参数的同步索引块指针会指向一个真正的锁(同步块),这个锁 (同步块) 会被复用。

线程之间通信的两个基本问题是?

线程互厅和同步。

什么是线程互斥?

线程互斥是一种特殊的线程同步

线程互斥是指对于共享的操作系统资源,在各线程访问时的排他性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其他要使用该资源必须等到,直到占用资源者释放该资源。

什么是线程同步?

线程同步就是协调多个线程间的并发操作,以获得符合预期的、确定的执行结果,消除多线程应用程序执行中的不确定性,它包含两个方面:

1)保护资源(或代码),即确保资源(或代码)同时只能由一个线程(或指定个数的线程)访问,一般措施是获取锁和释放锁(后面简称锁机制)。

2)协调线程对资源(或代码)的访问顺序,即确定某一资源(或代码)只能先由线程T1访问,再由线程T2访问,一般措施是采用信号量(后面简称信号量机制)。当T2线程访问资源时,必须等待线程T1先访问,线程T1访问完后,发出信号量,通知线程T2可以访问。

线程同步方式有哪几种?

在C#中实现线程的同步有几种方法: Lock、Mutex、Monitor、Semaphore、 Interlocked和ReaderWriterlock等。同步策略也可以分为同步上下文、同步代码区、手动同步几种方式。

1、对于线程同步操作最简单的一种方式就是使用 lock 关键字,通过 lock 关键字能保证加锁的线程只有在执行完成后才能执行其他线程。

2、Monitor 类的用法虽然比 lock 关键字复杂,但其能添加等待获得锁定的超时值,这样就不会无限期等待获得对象锁。

3、C# 中 Mutex 类也是用于线程同步操作的类,例如,当多个线程同时访问一个资源时保证一次只能有一个线程访问资源。

----------------

lock语句;监视器;同步事件和等待句柄;Mutex对象;

Lock语句用于给对象获取互斥锁,执行操作语句,然后再释放该锁;

lock语句就是简化monitor类使用的快捷语法,编译器优化了,monitor类提供了同步访问对象的机制。

1、Monitor:

使用MonitorSystem.Threading.Monitor对资源进行保护的思路很简单,即使用排他锁(Exclusive Lock)。

当线程A需要访问某一资源(对象、方法、类型成员、代码段)时,对其进行加锁,线程A获取到锁以后,任何其他线程如果再次对资源进行访问,则将其放到等待队列中,直到线程A释放锁之后,再将线程从队列中取出。Monitor的Enter()静态方法用于获取锁,Exit()静态方法用于释放锁。

2、使用System.Object作为锁对象

Monitor有一个限制,就是只能对引用类型加锁。

解决的办法是将锁加在其他的引用类型上,比如System.Object,此时,线程并不会访问该引用类型的任何属性和方法,该对象的作用仅仅是协调各个线程。换言之,之前的操作流程是占有A,操作A,释放A。现在的流程是占有B,操作A,释放B。当这样做时,要保证所有线程在加锁和释放锁的时候都是针对同一个对象――对象B。

object obj=new object();lock(obj){}相当于:System.Threading.Monitor.Enter(obj);try{}finally{    System.Threading.Monitor.Exit(obj);}

死锁

使用Monitor进行线程同步,可能会出现的一种比较棘手的问题就是死锁,简单来说死锁就是两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果没有外力进行干预(例如调用Abort()方法),则它们都将无法推进下去。

死锁,就是两个线程相互等待对方释放锁定的对象。

Mutex和lock有何不同? 一般用哪一个作为锁使用更好?

Mutex是一个基于内核模式的互斥锁,支持锁的递归调用,而Lock是一个混合锁,一般建议使用Lock更好,因为lock的性能更好

下面的代码,调用方法DeadLockTest (20) ,是否会引起死锁? 并说明理由

不会的,因为lock是一个混合锁,支持锁的递归调用,如果你使用一个ManualResetEvent或AutoResetEvent可

能就会发生死锁。

用双检锁实现一个单例模式Singleton:

多线程并行 (Parallelism) 和并发 (Concurrency) 的区别

并行:同一时刻有多条指令在多个处理器上同时执行,无论从宏观还是微观上都是同时发生的。

并发:是指在同一时间段内,宏观上看多个指令看起来是同时执行,微观上看是多个指令进程在快速的切换执行,同一时刻可能只有一条指令被执行。

线程池的优点有哪些?又有哪些不足?

优点: 减小线程创建和销毁的开销,可以复用线程;也从而减少了线程上下文切换的性能损失:在GC回收时较少的线程更有利于GC的回收效率。

缺点:线程池无法对一个线程有更多的精确的控制,如了解其运行状态等,不能设置线程的优先级:加入到线程池的任务 (方法)不能有返回值:对于需要长期运行的任务就不适合线程池。

同步与异步的区别?

·同步,是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。异步,不用等所有操作都做完,就相应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较9

好。

同步

所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉)。这种情况下,用户不能关闭界

面,如果关闭了,即迁移程序就中断了

异步

将用户请求放入消息队列,并反馈给用户,系统迁移程序已经启动,你可以关闭浏览器了。然后程序再慢慢地去写入数据库去。这就是异步。但是用户没有卡死的感觉,会告诉你,你的请求系统已经响应了。你可以关闭界面了。

使用异步代码的优势

Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 /0 完成。 使用异步代码时,当进程正在等待 /0 完成,服务器可以将其线程释放用于处理其他请求。 因此,使用异步代码可以更有效地利用服务器资源,并且服务器可以无延迟地处理更多流量。

异步的使用场景

1、不涉及共享资源,或对共享资源只读,即非互斥操作

2、没有时序上的严格关系

3、不需要原子操作,或可以通过其他方式控制原子性4、常用于IO操作等耗时操作,因为比较影响客户体验和使用性能

5、不影响主线程逻辑

第二章

创建线程

其实有六种方式,线程,线程池,任务,异步委托,并行,定时器(非winfrom,wpf的定时器),但本质就两种,线程和线程池。

1、定义一个委托,并异步调用它。 委托是方法的类型安全的引用。

2、线程池ThreadPool.QueueUserWorkItem

线程池中的所有线程都是后台线程。如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线程。

不能给入池的线程设置优先级或名称。

入池的线程只能用于时间较短的任务。如果线程要一直运行(如 Word的拼写检查器线程),就应使用 thread类创建一个线程。

3、new Thread

private static void Test()  {      var waits = new List();      for (int i = 0; i < 10; i++)    {          var handler = new ManualResetEvent(false);          waits.Add(handler);          new Thread(new ParameterizedThreadStart(Print))//把任务合成10份,开10个线程去跑        {              Name = "thread" + i.ToString()          }.Start(new Tuple(i, handler));//新建线程,传入参数    }      WaitHandle.WaitAll(waits.ToArray());//等待所有子线程完成再继续执行    Console.WriteLine("All Completed!");      Console.Read();  }  private static void Print(object param)  {      var p = (Tuple)param;//线程中解析传入的数据,传入的参数和目标类型要一致    Console.WriteLine(Thread.CurrentThread.Name + ": Begin!");        if (p.Item1 == 2) Thread.Sleep(1200);      else if (p.Item1 ==1 ) Thread.Sleep(2000);      else Thread.Sleep(1000);      Console.WriteLine(Thread.CurrentThread.Name + ": Print" + p.Item1);      Console.WriteLine(Thread.CurrentThread.Name + ": End!");      p.Item2.Set();//发送本线程已经执行完毕的信号}

4、Task.Run 和 TaskFactory.StartNew

static void test()    {        Task mainTask = Task.Factory.StartNew(() =>        {            for (int i = 0; i < 10; i++)            {                Task.Factory.StartNew(iobj =>                {                    Console.WriteLine("", Thread.CurrentThread.ManagedThreadId);                    try                    {                        Console.WriteLine("ManagedThreadId:{0},任务:{1}", Thread.CurrentThread.ManagedThreadId, (int)iobj);                        Thread.Sleep(1000 - i * 100);                    }                    catch { }                }, i, TaskCreationOptions.AttachedToParent);            }        });        mainTask.Wait();        Console.WriteLine("Task finished.");    }

5、Parallel.For

并行LINQ (PLINQ) 是 LINQ 模式的并行实现。 PLINQ 查询在许多方面类似于非并行 LINQ to Objects 查询。 PLINQ 尝试充分利用系统中的所有处理器, 它利用所有处理器的方法是,将数据源分成片段,然后在多个处理器上对单独工作线程上的每个片段并行执行查询。 在许多情况下,并行执行意味着查询运行速度显著提高。

Parallel适用于数据量大的情况(>10),如果数据少的话,还是不要用并行操作,因为使用硬件线程时,内部也是要用资源的。

要保证线程安全,一般使用对象:ConcurrentBag来定义List类型集合。

private static void Test3()  {      var list=new List();      for(int i=0;i<100;i++)      {          list.Add(i);      }      Console.WriteLine("test3 start\n" + DateTime.Now);      //并行循环      Parallel.ForEach(list, (i) =>      {          Thread.Sleep(100);//遍历每个数字,休眠0.1秒          //Console.WriteLine(i);      });      Console.WriteLine("test3  end\n" + DateTime.Now);      Console.ReadKey();  }  private static void Test4()  {      var list = new List();      for (int i = 0; i < 100; i++)      {          list.Add(i);      }      Console.WriteLine("test4 start\n" + DateTime.Now);      //顺序循环      list.ForEach((i) =>      {          Thread.Sleep(100);//遍历每个数字,休眠0.1秒          //Console.WriteLine(i);      });      Console.WriteLine("test4  end\n" + DateTime.Now);      Console.ReadKey();  }

6、Timer

在多线程的基础上,可以考虑使用“线程池”或“连接池”,“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统。

System.Threading命名空间下的Thread类用于管理单独的线程,包括创建线程、启动线程、终止线程、合并线程以及让线程休眠等。利用Thread或ThreadPool可在一个进程中实现多线程并行执行。但是,在新的开发中,不建议直接用Thread或ThreadPool编写多线程应用程序,这是因为其实现细节控制复杂,本章介绍它的目的只是为了让读者了解传统的编程技术是如何实现多线程编程的,并阐释涉及的相关概念,为后续章节的学习打下基础。

线程安全集合

线程安全集合是可同时被多个线程修改的可变集合。线程安全集合混合使用了细粒度锁定和无锁技术,以确保线程被阻塞的时间最短(通常情况下是根本不阻塞)。对很多线程安全集合进行枚举操作时,内部创建了该集合的一个快照(snapshot),并对这个快照进行枚举操作。线程安全集合的主要优点是多个线程可以安全地对其进行访问,而代码只会被阻塞很短的时间,或根本不阻塞。生产者/消费者集合是一种可变集合。

不可变栈和队列是最简单的不可变集合。

var stack = ImmutableStack.Empty;

.NET中的ConcurrentDictionary类是数据结构中的精品。它是线程安全的,混合使用了细粒度锁定和无锁技术,以确保绝大多数情况下能进行快速访问。

如果多个线程读写一个共享集合,使用ConcurrentDictionary是最合适的。如果不会频繁修改(很少修改),那更适合使用ImmutableDictionary。

ConcurrentDictionary最适合用在需要共享数据的场合,即多个线程共享同一个集合。如果一些线程只添加元素,另一些线程只移除元素,那最好使用生产者/消费者集合。

多线程使用注意事项

主要原因还是多线程会引发的问题,比如线程同步问题、多线程中有某个线程局部报错怎么处理的问题、数据库操作中多线程会造成事务出错的问题、多线程中数据库上下文对象不一致需要每个线程建立一个数据库对象的问题;

还有一个比较常见的问题就是造成CPU使用率过高导致系统卡顿。像我们后台web、api、task等项目,每一次请求一般都只使用一个线程,即使这样,请求多了cpu占用也会高,试想如果每次请求都使用多个线程的话,cpu高的情况就会更加严重了。

什么是线程上下文

当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存的线程上下文。个人理解就是线程需要保存的数据和资源。一般英文文档中的xxxContext都会被翻译为“xxx上下文”,个人认为是挺隔路。隔路的点在于,英文文档中的xxxContext都是表示该对象的内容,但汉语语境中,“xxx上下文”,通常会理解为除该对象以外的内容

前台线程与后台线程的区别

这个根据要表达的重点不同会有很多表述。其核心功能可狭义理解为前台线程不受外在因素影响,启动后必须执行完才停止。而后台线程受其他因素控制,执行过程中也可立即停止。一个显著的例子就是若应用程序启动了一个前台线程,退出应用程序后,前台线程还会继续执行(也就是应用程序其实并没有真正“退出”,资源也没有释放)。若应用程序启动的是后台线程,退出应用程序后,后台线程也会停止执行并释放。所以使用前台线程时要注意避免遗留为停止的前台线程,会导致应用程序无法停止。

低优先级的线程会等待高优先级的线程执行完再执行吗?

不会,低优先级的线程不会被阻塞。低优先级的线程相比于高优先级的线程,只是在相同时间间隔内,被CPU调度的次数相对少而已。

线程池出现的原因

创建和销毁线程是十分消耗CPU资源的操作,也就是十分耗时的操作。频繁创建、销毁线程会影响应用程序性能。所以引入缓存来解决这个问题。创建一些线程后不销毁,而是保存在一些地方,需要使用线程时,调用这些已有线程就可以。节省了创建、销毁线程的时间。

系统,程序中的池是什么

我们编程过程中或多或少都接触过各种“池”,比如,数据库连接池,线程池,socket连接池等等。这些池的主要用途都是一个:把系统需要频繁使用的对象保存起来,供系统调用,节省对象重复创建与销毁多耗费的时间。是一种“空间换时间”的处理机制。当然把对象保存起来并不能解决问题,我们还需要解决缓存的大小问题、排队执行任务、调度空闲线程、按需创建新线程及销毁多余空闲线程……等等问题。而微软的团队已经都为我们解决好了这些问题,也就是ThreadPool类,我们只需要调用类中的方法就可以了。这样我就就可以专注于程序业务功能而不是线程管理。

并行与并发的区别

并行:多个处理核心同一时刻同时处理多个不同的任务。并发:一个处理核心在同一时间段处理多个不同任务,各个任务快速交替执行。即同一时刻,其实只有一个任务在执行。

什么是任务的全局队列与局部队列

在主线程或其他并没有分配给某个特定任务的线程的上下文中创建并启动的任务,这些任务将会在全局队列中竞争工作线程。这些任务被称为顶层任务。如果是在其他任务的上下文中创建的任务(子任务或嵌套任务),这些任务将被分配在线程的局部队列中。全局队列的调用顺序是FIFO局部队列的调用顺序通常是LIFO

为什么会出现任务的局部队列这种机制

线程的全局队列是共享资源,所以内部会实现一个锁机制。当一个任务内部会创建很多子任务时,并且这些子任务完成得非常快,就会造成频繁的进入全局队列和移出全局队列,从而降低应用程序的性能。为了避免这种情况,线程池引擎为每个线程引入了局部队列。局部队列有2个性能优势:任务内联化和工作窃取

什么是任务内联化

仅当线程等待时出现是线程的局部队列带来的性能优化方法。是利用阻塞的顶层任务的线程去执行局部队列中的任务,减少了额外线程的开销。如一个顶层任务需要等待3个嵌套任务执行完毕再执行,其中一个嵌套任务就可以运行在正在等待的顶层任务的线程中,这样就减少了一个额外线程的开销。

什么是工作窃取

就是让空闲的工作线程,来进入局部队列执行局部队列中正在等待的任务。

async会创建新线程还是await会创建新线程

都不会,async/await可以理解为一种异步的结构同步化语法糖,具体的新线程还是通过Task.Run()等代码创建。

在await的代码中不返回Task,返回void不行吗

不行,await后面跟着的必须是一个等待表达式,如Task,Task。返回void,或其他参数会报错。"CS4008:无法等待void"或“CS1061:bool未包含GetAwaiter的定义,并且找不到可接受第一个bool类型参数的可访问扩展方法GetAwaiter(是否缺少 using 指令或程序集引用?)”

以前的异步编程怎么实现顺序执行

在异步代码内连续委托,回调。

异步编程模式的逐步发展主要为了什么

除去基础设施的完善。异步编程的发展主要为了编码人员能够更加简单的编写出异步程序。由最初的Thread发展至目前常用的async\await关键字。逐步解决了线程频繁创建的问题,线程管理的问题,APM或EAP模式需要手写大量代码,又因为委托、回调导致代码可读性很差,控制流混乱的问题。最终可以让我们以一种类似于同步的结构来编写异步代码,极大的减少了编写难度,增强了可读性。

异步编程本质是为了什么

这个一定是有很多的用处,但目前就我个人来说,最大的用处就是使用异步处理一些耗时操作,保证UI线程的线程能力,提高用户体验。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OUYQzGAN4JGvuIcaWDQBsZUw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券