C#异步Thread

    技术2022-07-11  110

    笔记整理(原资源网址):https://www.bilibili.com/video/BV1Zf4y117fs

    VSCode 搭建 C# 环境

    安装 .Net Core SDK安装 C# 调试器 (C#插件)创建 C# 项目 (dotnet -h & dotnet new xxxxxx)

    线程(Thread)

    线程是一个可执行路径,可独立于其他线程执行。

    每个线程都在操作系统的**进程(Process)**内执行,而操作系统进程提供了程序运行的独立环境。

    单线程应用:在进程的独立环境里只跑一个线程,所以该线程拥有独占权。

    多线程应用:单个进程中会跑多个线程,它们会共享当前的执行环境(尤其是内存)。

    ​ 例如:一个线程在后台读取数据,另一个线程在数据到达后进行展示。(这个数据就被称作是共享的状态)

    线程被抢占:它的执行与另外一个线程上代码的执行交织的那一点,线程在这个时间就可以称为被抢占了。

    线程常用属性

    CreateThread.cs

    线程一旦开始执行,IsAlive 就为 true,线程结束就变成 false。

    ​ 线程结束的条件是:线程构造函数传入的委托结束了执行。

    ​ 线程一旦结束,就无法再重启。

    每个线程都有个 Name 属性,通常用于调试。

    ​ 线程 Name 只能设置一次,再次修改会抛出异常。

    静态的 Thread.CurrentThread 属性,会返回当前执行的线程。

    Join & Sleep

    JoinDemo1.cs
    JoinDemo2.cs
    JoinTimeout.cs

    Join:调用Join方法,就可以等待另一个线程结束。

    ​ 可以设置一个超时参数,用毫秒或者 TimeSpan。(不设置超时,无返回值;设置超时,返回 true,线程正常结束,返回 false,线程超时。)

    ​ TimeSpan waitTime = new TimeSpan(0, 0, 1);

    Sleep:线程休眠,参数为毫秒或 TimeSpan。

    ​ Thread.Sleep(0) 这样调用会导致线程立即放弃本身当前的时间片,自动将Cpu移交给其他线程。

    ​ Thread.Yield() 做同样的事情,但是它指挥把执行交给同一处理器上的其他线程。

    当等待 Sleep 或 Join 的时候,线程处于阻塞的状态。

    Tips:Sleep(0) 或 Yield 有时候在高级性能调试的生产代码中很有用。它也是一个很好的诊断工具,有助于发现线程安全问题,如果在代码中的任何地方插入 Thread.Yield() 就破坏了程序,那么你的程序几乎肯定有 bug。

    线程阻塞(Blocking)

    1、如果线程的执行由于某种原因导致暂停,那么就认为该线程被阻塞了。

    ​ 例如在 Sleep 或者通过 Join 等待其他线程结束。

    ​ 被阻塞的线程会立即将其处理器的时间片生成给其他线程,从此不再消耗处理器时间,直到满足其阻塞条件为止。

    2、可以通过 ThreadState 属性(Flags 枚举)来判断线程是否处于被阻塞的状态:

    ​ bool blocked = (someThread.ThreadState & ThreadState.WaitSleepJoin) != 0;

    ​ ThreadState 属性可用于诊断的目的,但不适合用于同步,因为线程状态可能会在测试 ThreadState 和对该信息进行操作之间发生变化。

    3、解除阻塞(Unblocking)的四种方式:

    ​ 1)阻塞条件被满足

    ​ 2)操作超时(如果设置了超时的话)

    ​ 3)通过 Thread.Interrupt() 进行打断

    ​ 4)通过 Thread.Abort() 进行中止

    4、上下文切换

    ​ 当线程阻塞或解除阻塞时,操作系统将执行上下文切换。产生少量的开销,通常为1~2微秒。

    5、I/O-bound vs. Compute-bound(CPU-bound)

    ​ 一个花费大部分时间等待某事发生的操作称为 I/O-bound,通常涉及输入或输出,但这不是硬性要求:Thread.Sleep() 也被视为 I/O-bound。

    ​ 一个花费大部分时间执行CPU密集型工作的操作称为 Compute-bound。

    6、阻塞(Blocking) vs. 忙等待(自旋 Spinning)

    CLR即公共语言运行时(Common Language Runtime,简称CRL),就是微软为.net产品构建的运行环境,与java的JVM类似,通俗的讲就是.net虚拟机。CLR上实际运行的并不是我们通常所用的编程语言(例如C#、VB等),而是一种字节码形态的“中间语言”。这意味着只要能将代码编译成这种特定的“中间语言”(MSIL),任何语言的产品都能运行在CLR上。CLR通常被运行在Windows系统上,但是也有一些非Windows的版本。这意味着.Net也很容易实现“跨平台”。CLR是.net系列产品运行的基础。

    // 将多种线程状态转换成比较简单、常用的四种线程状态 public static ThreadState SimpleThreadState (ThreadState ts) { // ts 为 ThreadState.Unstarted 或 ThreadState.WaitSleepJoin 或 ThreadState.Stopped时 // 返回响应状态,否找全部返回 ThreadState.Running = 0 return ts & (ThreadState.Unstarted | ThreadState.WaitSleepJoin | ThreadState.Stopped); }

    线程安全

    LocalAndShare.cs
    Share.cs
    Lock.cs

    本地独立(Local):CLR 为每个线程分配自己的内存栈,以便使本地变量保持独立。

    共享状态(Shared):如果多个线程都引用到同一个对象的实例,那么它们就共享了数据。

    ​ 被 Lambda 表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),所以也会被共享。

    ​ 静态字段也会在线程间共享数据。

    线程安全:尽可能的避免使用共享状态。

    C# 使用 lock语句来加锁,当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,知道锁变成可用状态。

    在多线程上下文中,以加锁的方式避免不确定性的代码就叫做线程安全。

    lock 不是线程安全的银弹,很容易忘记对字段加锁,lock也会引起一些问题(死锁)。

    向线程传递数据

    TransmitData.cs
    TransmitDataFault.cs

    往线程的启动方法里传递参数,最简单的方式是使用 lambda 表达式,在里面使用参数调用方法。

    甚至可以把整个逻辑都放在 lambda 表达式中。

    Notice:使用 lambda 表达式可以很简单的给 Thread 传递参数。但是线程开始后,可能会修改了被捕获的变量,要多加注意。

    前台线程 & 后台线程

    ForeAndBack.cs

    默认情况下,手动创建的线程就是前台线程(Foreground)。

    只要有前台线程在运行,那么应用程序就会一直处于活动状态。但是**后台线程(Background)**却不行,一旦所有的前台线程停止,那么应用程序就停止了,任何的后台线程也会突然终止。

    进程以上述形式终止(所有的前台线程停止)的时候,后台线程执行栈中的 finally 块就不会被执行了,如果想让它执行,可以在退出程序时使用 Join 来等待后台线程(如果是自己创建的线程),或者使用 signal construct,如果是线程池……

    Notice:线程的前台、后台状态与它的优先级(所分配的执行时间)无关。

    可以通过IsBackground属性判断线程是否是后台线程。

    应用程序无法正常退出的一个原因就是还有活跃的前台线程。

    线程优先级

    线程的优先级(Thread 的 ThreadPriority 属性)决定了相对于操作系统中其他活跃线程所占的执行时间。

    优先级:enum ThreadPriority {Lowest, BelowNormal, Normal, AboveNormal, Highest}

    提升线程优先级的时候需要注意,因为它可能“饿死”其他线程。

    如果想让某线程(Thread)的优先级比其他进程(Process)中的线程高,那就必须提升进程的优先级。

    ​ 使用 System.Diagnostics 下的 Process 类:

    ​ using (Process p = Process.GetCurrentProcess())

    ​ p.PriorityClass = ProcessPriorityClass.High;

    这样可以很好地用于只做少量工作且需要较低延迟的非UI进程。

    对于需要大量计算的应用程序(尤其是有 UI 的应用程序),提高进程优先级可能会使其他进程饿死,从而降低整个计算机的速度。

    发送信号(Signaling)

    Signaling.cs

    有时,你需要让某一线程一直处于等待状态,直至接收到其他线程发来的通知。这就叫做Signaling(发送信号)。

    最简单的信号结构就是 ManualResetEvent。

    ​ 调用它上面的 WaitOne 方法会阻塞当前的线程,直到另一个线程通过调用 Set 方法来开启信号。

    ​ 调用完 Set 之后,信号会处于“打开”的状态。可以通过调用 Reset 方法将其再次关闭。

    富客户端应用程序的线程

    同步上下文(Synchronization Contexts)

    ThreadWPF

    在 System.ComponentModel 下有一个抽象类:SynchronizationContext,它使得 Thread Marshaling 得到泛化。

    Marshaling is the act of taking data from the environment you are in and exporting it to another environment.

    针对移动、桌面(WPF, UWP, WinForms)等富客户端应用的 API,它们都定义和实例化了SynchronizationContext的子类。

    ​ 可以通过静态属性 SynchronizationContext.Current 来获得(当运行在UI线程时)

    ​ 捕获该属性让你可以在稍后的时候从 worker 子线程向 UI 线程发送数据

    ​ 调用 Post 就相当与调用 Dispatcher 或 Control 上面的 BeginInvoke 方法

    ​ Send 等价于 Invoke 方法

    线程池(Thread Pool)

    ThreadPool.cs

    当开始一个线程的时候,将花费几百微秒的时间来组织类似以下的内容:一个新的局部变量栈。

    线程池可以节省这个开销,通过预先创建一个可循环使用线程的池。

    线程池对于高效的并行编程和细粒度并发是必不可少的,它允许在不被线程启动的开销淹没的情况下运行短期操作。

    Notice:

    不可以设置池线程的 Name

    池线程都是后台线程

    阻塞池线程可能使性能降级

    可以自由更改池线程的优先级,当释放其回线程池的时候优先级将还原为正常状态

    可以通过 Thread.CurrentThread.IsThreadPoolThread 属性来判断是否执行在池线程

    最简单、最显示的在池线程运行代码的方式就是使用 Task.Run

    ​ // Task is in System.Threading.Tasks

    ​ Task.Run(() => Console.WriteLine(“Hello from the thread pool!”));

    线程池中的整洁:线程池提供了另一个功能,即确保临时超出 compute-bound 的工作不会导致 CPU 超额订阅。

    CPU 超额订阅:活跃的线程超过 CPU 的核数,操作系统就需要对线程进行时间切片。

    超额订阅对性能影响很大,时间切片需要昂贵的上下文切换,并且可能使 CPU 缓存失效,而 CPU 缓存对于现代处理器的性能至关重要。

    CLR 的策略:CLR通过对任务排队并对其启动进行节流限制来避免线程池中的超额订阅。先运行尽可能多的并发任务,然后通过爬山算法调整并发级别,并在特定方向上不断调整工作负荷。(如果吞吐量提高,它将继续朝同一方向调整(否则反转))。这确保它始终追随最佳性能曲线。

    如果满足以下两点,CLR的策略将发挥出最佳效果:

    ​ 工作项大多是短时间运行的(<250毫秒,或者理想情况下<100毫秒),因此CLR有很多机会进行测量和调整。

    ​ 大部分时间都被阻塞的工作项不会主宰线程池。

    Processed: 0.014, SQL: 9