Thread & Process 线程和进程
启动程序时,系统会在内存中创建一个新的进程。进程是构成程序的资源的集合,这些资源包括虚地址空间、文件句柄等
在进程内部,系统创建了一个称为线程的内核对象,他代表了真正执行的程序
默认情况下一个进程只包含一个线程,在任意时刻,一个进程都可包含不同状态的多个线程,他们执行程序的不同部分,这些线程共享同一进程的内存与资源,所以常需要同步来避免数据冲突。
系统为处理器所执行的调度单元是线程而不是进程
虽然同一进程内的不同线程共享同一片虚拟地址空间,但这不等于“可以随便用彼此的资源”
- 线程安全:并发读写会造成数据竞争、撕裂和不可复现的 Bug。需要锁、原子操作或无锁结构来保证一致性,否则运行一次一个结果。
- 线程亲缘性(thread affinity):许多“资源”在底层绑定到创建它的线程或特定线程上下文:例如图形 API(OpenGL/Vulkan/Metal/DX 的上下文)、窗口系统、输入回调、音频设备、COM STA 等。
- Unity 的主循环把引擎状态(场景、Transform、Renderer、Animator、Physics 同步点等)都集中在主线程推进。在非主线程访问经常会抛错或导致未定义行为。
- 一致性与时序(帧驱动)Unity 每帧有严格的更新顺序(Input → Script Update → Physics → Animation → Rendering)。如果其他线程在错误的时刻修改引擎状态,会和物理/渲染的同步点冲突,破坏帧一致性。
什么是异步:
如果一个程序调用某个方法,并在等待方法执行所有处理后才继续执行,这样的方法我们称之为同步的
相反异步方法在完成所有工作之前就返回到调用方法,这就需要用到async关键字声明的异步方法,和await表达式
异步方法:
异步方法具有如下特点:
- 方法头中包含async方法修饰符
- 包含一个或多个await表达式,表示可以异步完成的任务
- 必须具备以下3种返回类型之一,void,Task,Task<T>, ValueTask<T>
- 形参可以为任意类型、任意数量,但不能为out / ref参数
- 按照约定,异步方法的名称应以Async为后缀
- 除了方法之外,lambda表达式和匿名方法也可以作为异步对象
Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,那么异步方法可以返回一个 Task 类型的对象。在这种情况下,如果异步方法中包含任何 return 语句,则它们不能返回任何东西。下面的代码来自于一个调用方法:
Task someTask = DoStuff.CalculateSumAsync(5, 6);
…
someTask.Wait();
Task<T>:任何返回 Task 类型的异步方法,其返回值必须为 T 类型或可以隐式转换为 T 的类型。调用方法将通过读取 Task 的 Result 属性来获取这个 T 类型的值
Task<int> value = DoStuff.CalculateSumAsync(5, 6);
Console.WriteLine($"Value: { value.Result }");
ValueTask:这是一个值类型对象,它与 Task 类似,但用于任务结果可能已经可用的情况。因为它是一个值类型,所以它可以放在栈上,而不须像 Task 对象那样在堆上分配空间。因此,它在某些情况下可以提高性能。
void:如果调用方法仅仅想执行异步方法,而不需要与它做进一步的交互(这种称为“调用并忘记”(fire and forget)),异步方法可以返回 void 类型。这时,与上一种情况类似,如果异步方法中包含任何 return 语句,则它们不能返回任何东西
异步方法的控制流
async(asynchronous异步)和 await:async该修饰符只是标识该方法包含一个或多个 await 表达式。也就是说,它本身并不能创建任何异步操作
异步方法的结构包含三个不同的区域:
- 第一个 await 表达式之前的部分:从方法开头到第一个 await 表达式之前的所有代码。这一部分应该只包含少量无须长时间处理的代码。
- await 表达式:表示将被异步执行的任务。
- 后续部分:await 表达式之后的方法中的其余代码。包括其执行环境,如所在线程信息、目前作用域内的变量值,以及当 await 表达式完成后重新执行时所需的其他信息
图21-10 阐明了一个异步方法的控制流。它从第一个 await 表达式之前的代码开始,正常(同步地)执行直到遇见第一个 await。这一区域实际上在第一个 await 表达式处结束,此时 await 的任务还没有完成(大多数情况下如此)。当 await 的任务完成时,方法将继续同步执行。如果还有其他 await,就重复上述过程
Thread Task使用案例
单线程做菜:UI进程和做菜进程共用主线程,因此在做菜的过程中,无法拖动UI页面,只有做完菜后才能拖动UI界面,这就是单线程
private void button1_Click(object sender, EventArgs e)
{
Thread.Sleep(3000);
MessageBox.Show("素菜做好了", "友情提示");
Thread.Sleep(5000);
MessageBox.Show("荤菜做好了", "友情提示");
}
线程的构造方法需要传入一个无返回值无参/无返回值有一个参数的委托(即一个无返回值无参数的方法,所以可选择传入一个lambda表达式),并可选择在第二个参数自定义线程栈大小(单位:字节)
public Thread(ParameterizedThreadStart start);
public Thread(ThreadStart start);
public Thread(ParameterizedThreadStart start, int maxStackSize);
public Thread(ThreadStart start, int maxStackSize);
Thread做菜:在做菜的过程中窗口可以移动
1 个引用
private void button2_Click(object sender, EventArgs e)
{
Thread t = new Thread(() =>
{
Thread.Sleep(3000);
MessageBox.Show("素菜做好了", "友情提示");
Thread.Sleep(5000);
MessageBox.Show("荤菜做好了", "友情提示");
});
t.Start();
}
Task做菜:但在C#中更推荐用Task,Thread 类属于较低层次的抽象,它直接映射到操作系统的线程。使用 Thread 时,你要对线程的生命周期进行直接管理,像创建、启动、暂停、终止等操作。Task是在Thread之上的封装,因此使用Task用相同的语法创建线程会更方便管理
private void button3_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(3000);
MessageBox.Show("素菜做好了", "友情提示");
Thread.Sleep(5000);
MessageBox.Show("荤菜做好了", "友情提示");
});
}
同时做了多道菜,这种多线程的优势就是能够利用CPU的多核,因此多线程编程可以大大的提高效率,
private void button4_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
Thread.Sleep(3000);
MessageBox.Show("素菜做好了", "友情提示");
});
Task.Run(() =>
{
Thread.Sleep(5000);
MessageBox.Show("荤菜做好了", "友情提示");
});
}
等待当前线程结束后再执行主线程中的后面方法,需要将方法声明为异步方法,通过在函数签名中加入async实现这一点,并在Task前加关键字await
1 个引用
private async void button5_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
Thread.Sleep(3000);
MessageBox.Show("素菜做好了", "友情提示");
});
await Task.Run(() =>
{
Thread.Sleep(5000);
MessageBox.Show("荤菜做好了", "友情提示");
});
MessageBox.Show("菜都做好了,大家快来吃饭!", "提示");
}
但如果需要等待多个线程都结束,可以使用Task包装好的方法WhenAll(这似乎和Dotween一样,也是链式调用方法,可以不断在函数后调用新的方法),其需要将所有线程添加到一个列表中
private void button5_Click(object sender, EventArgs e)
{
List<Task> ts = new List<Task>();
ts.Add(Task.Run(() =>
{
Thread.Sleep(3000);
MessageBox.Show("素菜做好了", "友情提示");
}));
ts.Add(Task.Run(() =>
{
Thread.Sleep(5000);
MessageBox.Show("荤菜做好了", "友情提示");
}));
Task.WhenAll(ts).ContinueWith(t =>
{
MessageBox.Show("菜都做好了,大家快来吃饭!", "提示");
});
}
await 表达式
await表达式指定了一个异步执行的任务,默认情况下,这个任务在当前线程下异步执行
await task
task应为awaitable类型的实例,然而实际上并不需要构建自己的awaitable,相反应该使用Task类或ValueTask类,它们是awaitable类型
尽管目前 BCL 中存在很多返回 Task 类型对象的方法,你仍然可能需要编写自己的方法,作为 await 表达式的任务。最简单的方式是在你的方法中使用 Task.Run 方法来创建一个 Task。关于 Task.Run,有一点非常重要,即它在不同的线程上运行你的方法。
Task.Run 的一个签名如下,它以 Func<TReturn> 委托为参数。如第 20 章所述,Func<TReturn> 是一个预定义的委托,它不包含任何参数,返回值的类型为 TReturn:
Task Run( Func<TReturn> func )
因此,要将你的方法传递给 Task.Run 方法,需要基于该方法创建一个委托。下面的代码展示了三种实现方式。其中,Get10 与 Func<int> 委托兼容,因为它没有参数并且返回 int。
□ 第一个实例(DoWorkAsync 方法的前两行)使用 Get10 创建名为 ten 的 Func<int> 委托。然后在下一行将该委托用于 Task.Run 方法。
□ 第二个实例在 Task.Run 方法的参数列表中创建 Func<int> 委托。
□ 第三个实例没有使用 Get10 方法,而是使用了 Get10 方法体的 return 语句,并将其用于与 Func<int> 委托兼容的 Lambda 表达式。该 Lambda 表达式将隐式转换为该委托
using System;
using System.Threading.Tasks;
class MyClass
{
public int Get10() // 与 Func<int> 兼容
{
return 10;
}
public async Task DoWorkAsync()
{
// 第一种写法:先用 Get10 创建委托,再传给 Task.Run
Func<int> ten = new Func<int>(Get10);
int a = await Task.Run(ten);
// 第二种写法:直接在 Task.Run 参数中创建委托
int b = await Task.Run(new Func<int>(Get10));
// 第三种写法:用 Lambda 表达式(与 Func<int> 兼容)
int c = await Task.Run(() => { return 10; });
Console.WriteLine($"{ a } { b } { c }");
}
}
class Program
{
static void Main()
{
Task t = (new MyClass()).DoWorkAsync();
t.Wait();
}
}
Thread常用函数
方法/属性 | 作用 | 备注 |
---|---|---|
new Thread(Action) | 创建线程 | 传入线程入口委托 |
Start() | 启动线程 | 必须先 Start 才会跑 |
Join() / Join(ms) | 等线程结束 | 主线程慎用,避免卡界面 |
IsAlive | 是否仍在运行 | 只读 |
IsBackground | 后台线程标志 | 后台线程不阻止进程退出 |
Name / ManagedThreadId | 线程名 / ID | 便于调试 |
Priority | 优先级 | 罕用 |
Sleep(ms) | 暂停当前线程 | 粗略延时,非高精度 |
Yield() | 让出时间片 | 轻量“礼让”一下 |
Thread.CurrentThread | 获取当前线程对象 | 可读各属性 |