多线程的一点基础知识
- 单核的性能逐渐逼近工业能力上限,开始通过多核来提高性能
- 多线程逻辑很难写
- 通过
System.Diagnostics.Process
访问进程 - 被调用操作的执行和完成独立于调用它的控制流
- 要从IO受限的阻塞线程切换到就绪线程,提高处理器利用率,防止处理器闲置
- 上下文切换会把寄存器和栈上的内容从CPU保存到内存,然后加载新线程的执行环境,上下文切换可能会导致上一个线程的CPU缓存被替换掉,造成更多的cache miss
- 线程太多会导致上下文切换的开销比例显著提高,影响性能
- 并发不会提高处理器受限的任务的性能,并发主要的作用是减少阻塞
- 并行可以显著提高处理器受限的任务的性能
- += 和 -=不是原子性操作
- 竞态条件就是两个控制点以无法预测且不一致的速度竞争代码的执行
- 竞态条件会造成不确定性且难以重现
- 保证多线程代码的品质主要依赖长期的压力测试、代码分析共计和专家的code review
- 处理器同步缓存的时机也可能会造成竞态条件,两个不同的线程读取同一块内存,可能会因为缓存同步时机的问题,获取到不同的结果(?这也太逆天了吧)
- 操作系统保证同时只有一个线程可以获得锁执行代码,并保证遇到锁的时候处理器的缓存会正确同步
- 锁是有性能开销的,而且可能会出现死锁
- 大部分操作都不是原子性的,不要假设一个操作是原子性的
- 要避免所有竞态条件
- 线程池避免了创建和销毁线程的巨大开销,同时可以避免创建过多线程,防止线程上下文切换的开销过大影响性能
- 锁防止两个不同的线程同时访问数据
- 只要有需要长时间运行的方法,就可能需要多线程编程
- 每个线程在windows上的栈空间是1MB
TPL的简单使用
- TPL创建一个Task,任务调度器从线程池请求一个工作线程进行执行,线程池可能会在当前任务结束后再运行新任务(不分配新线程),也可能重用一个线程池线程去执行任务,或者创建一个全新线程
- Task代表异步工作的对象,委托是同步的,任务是异步的,委托封装的代码永远都是同步执行的,执行时当前线程的控制点立刻转移到委托内部,执行完成之后才会返回调用点,而任务启动后会立即返回到调用者,通常在另一个线程上异步执行
- 常见的获取结果的方式:轮询、阻塞等待
IsCompleted
非正常结束也会为trueCurrentId
是任务的唯一标识ID- 通过延续将多个任务合并到一起,合并成较大的任务
- 先驱任务完成时,延续任务自动开始
- 同一个先驱的两个延续任务异步执行
- 创建延续:
var taskA = Task.Run(() => { Console.WriteLine("Starting..."); })
.ContinueWith(task => { Console.WriteLine("Continuing A..."); });
var taskB = taskA.ContinueWith(task => { Console.WriteLine("Continuing B..."); });
var taskC = taskA.ContinueWith(task => { Console.WriteLine("Continuing C..."); });
Task.WaitAll(taskB, taskC);
Console.WriteLine("Finished");
- 实参task是延续task的先驱task,延续需要访问先驱的执行状态的
TaskContinuationOptions
:
- 可以组合延续和flag,为一个任务的执行设定多个执行分支
- 一个任务的几个延续互斥时,某一个延续开始执行后,任务调度器会自动取消另外几个互斥的延续,取消的任务的状态是
Canceled
,在Cancel
的任务上调用Wait
会抛出一个异常,因为被取消的任务永远不会执行,永远也而等不到 - 可以用Task.WaitAny() 结合
AggregateException
(Aggregate是聚合的意思,这个异常可以理解为一个异常的集合)来处理所有分支延续 - 不能用try包装Start来捕捉异常,因为Start会立即返回
- 任何线程上的未处理异常都被视为严重错误,造成应用程序panic,所有线程上的异常都必须被捕捉
- 任务调度器会有一个处理程序捕获所有的异常,防止CLR自动终止进程