C# 多线程与异步
一、先记住核心区别
多线程:解决"在哪个线程执行"的问题。
异步:解决"等待的时候要不要阻塞线程"的问题。
两者不是完全对立的概念,也不是一回事。
text
多线程 → 同时使用多个线程执行任务
异步 → 任务执行过程中遇到等待,不阻塞当前线程
二、多线程是什么?
多线程(Multithreading)是指程序同时使用多个线程执行任务。
例如:
csharp
Thread thread = new Thread(() =>
{
// 子线程执行
DoSomething();
});
thread.Start();
此时:
text
主线程
│
├── 执行主线程任务
│
子线程
│
└── 执行 DoSomething()
多线程主要解决
- CPU 密集型任务
- 多个任务并行执行
- 避免某些耗时计算阻塞主线程
例如:
csharp
// 计算大量数据
for (int i = 0; i < 100000000; i++)
{
result += i;
}
可以考虑把计算放到其他线程。
三、异步是什么?
异步(Asynchronous)主要解决的是:
任务需要等待的时候,不要一直占着当前线程。
例如网络请求:
csharp
async Task GetDataAsync()
{
string result = await DownloadDataAsync();
Console.WriteLine(result);
}
执行到:
csharp
await DownloadDataAsync();
如果数据还没有回来,当前线程可以暂时去做其他事情。
text
开始请求
↓
等待网络数据
↓
当前线程可以做其他事情
↓
数据返回
↓
继续执行后面的代码
所以:
async/await的核心价值不是"创建新线程",而是避免线程在等待期间被阻塞。
四、多线程 ≠ 异步
这是最容易混淆的地方。
多线程
csharp
Thread thread = new Thread(() =>
{
HeavyCalculation();
});
thread.Start();
重点:
text
创建/使用新的线程
异步
csharp
async Task Test()
{
await Task.Delay(1000);
Console.WriteLine("完成");
}
这里的:
csharp
Task.Delay(1000)
并不会专门创建一个线程让它"睡眠 1 秒"。
而是:
text
开始 Delay
↓
释放当前线程
↓
1 秒后恢复执行
所以:
异步代码完全可能没有创建额外线程。
五、同步与异步
先区分另一个概念:
同步
调用一个方法后,需要等待它执行完成。
csharp
DoSomething();
Console.WriteLine("下一步");
执行顺序:
text
DoSomething()
↓
等待完成
↓
Console.WriteLine()
异步
调用异步方法后,可以暂时离开当前执行流程,等待任务完成后再继续。
csharp
await DoSomethingAsync();
Console.WriteLine("下一步");
可以理解为:
text
开始任务
↓
等待
↓
线程可以去处理其他事情
↓
任务完成
↓
继续执行
六、CPU 密集型 vs IO 密集型
理解多线程和异步,最重要的是区分这两类任务。
1. CPU 密集型
主要消耗 CPU。
例如:
- 大量数学计算
- 图片处理
- 数据计算
- 路径计算
- AI 推理中的部分计算
这种情况下:
多线程 / 并行计算通常更有价值。
例如:
csharp
Task.Run(() =>
{
HeavyCalculation();
});
可以把计算任务放到线程池线程执行。
2. IO 密集型
主要时间花在等待外部资源。
例如:
- 网络请求
- 文件读写
- 数据库查询
- 等待服务器响应
这种情况下:
异步通常更加合适。
例如:
csharp
async Task LoadDataAsync()
{
var data = await httpClient.GetStringAsync(url);
Console.WriteLine(data);
}
线程不用一直等待网络返回。
七、Task 到底是什么?
Task 经常同时出现在多线程和异步代码中,所以容易产生误解。
csharp
Task task = Task.Run(() =>
{
DoSomething();
});
这里 Task 可以理解为:
对一个异步操作/未来结果的抽象。
它本身不等于线程。
例如:
csharp
Task.Delay(1000);
这里有一个 Task,但并不意味着:
text
创建一个线程
↓
线程睡眠 1 秒
而更接近:
text
创建一个等待中的 Task
↓
1 秒后完成
八、Task.Run 与 async/await 的区别
这是面试中非常容易问的问题。
Task.Run
csharp
Task.Run(() =>
{
HeavyCalculation();
});
主要作用:
把同步代码放到线程池线程执行。
适合:
text
CPU 密集型计算
async/await
csharp
await DownloadDataAsync();
主要作用:
异步等待任务完成,避免线程被阻塞。
适合:
text
IO 密集型操作
九、两者可以结合
多线程和异步并不是只能二选一。
例如:
csharp
async Task ProcessAsync()
{
int result = await Task.Run(() =>
{
return HeavyCalculation();
});
Console.WriteLine(result);
}
这里:
text
Task.Run
↓
把计算放到线程池线程
↓
HeavyCalculation()
↓
Task 完成
↓
await 继续执行
也就是说:
Task 可以使用线程,async/await 负责异步等待。
十、线程池
C# 中通常不建议频繁手动创建大量 Thread。
csharp
new Thread(...)
创建线程本身有成本。
更常见的是使用:
csharp
Task
ThreadPool
Parallel
线程池会维护一批可复用的线程。
text
ThreadPool
├── Thread 1
├── Thread 2
├── Thread 3
└── Thread 4
任务 A → Thread 1
任务 B → Thread 2
任务 C → Thread 3
任务完成后线程可以继续处理其他任务。
十一、Unity 中怎么理解?
Unity 中尤其要注意:
Unity 大量 API 只能在主线程使用。
例如:
csharp
transform.position = newPosition;
不能随便在后台线程修改 Unity 对象。
错误思路:
csharp
Task.Run(() =>
{
transform.position = Vector3.zero;
});
更合理的是:
text
主线程
↓
启动后台计算
↓
后台线程计算数据
↓
计算完成
↓
回到 Unity 主线程
↓
修改 GameObject
例如:
csharp
var result = await Task.Run(() =>
{
return HeavyCalculation();
});
// 回到 Unity 主线程后
transform.position = result;
具体是否能安全回到主线程,还取决于 Unity 的同步上下文、所使用的异步框架以及调用方式。
十二、Unity 中常见场景
适合多线程 / Task.Run
例如:
text
大量数据计算
地图寻路计算
复杂数学计算
数据解析
CPU 密集型算法
适合异步
例如:
text
网络请求
文件读取
资源加载
等待服务器响应
等待某个异步操作完成
Unity 协程又是什么?
Unity 的:
csharp
IEnumerator
yield return
经常被叫作"异步",但它和 C# 的 async/await 不是同一个机制。
例如:
csharp
IEnumerator Test()
{
yield return new WaitForSeconds(1);
Debug.Log("完成");
}
协程:
本质上是在 Unity 主线程中分帧执行代码。
它不会自动创建新的线程。
text
Unity 主线程
Frame 1
↓
执行协程
↓
yield
Frame 2
↓
继续执行
↓
yield
Frame 3
↓
继续执行
所以:
协程 ≠ 多线程
协程 ≠ C# async/await
十三、三者对比
| 技术 | 是否创建新线程 | 主要解决的问题 | 常见场景 |
|---|---|---|---|
| Thread | ✅ 创建线程 | 并发执行 | 特殊线程需求 |
| ThreadPool | ✅ 使用线程池线程 | 复用线程执行任务 | 后台任务 |
| Task.Run | ✅ 通常使用线程池 | 把计算放到后台线程 | CPU 密集型 |
| async/await | ❌ 不一定创建线程 | 避免等待时阻塞线程 | IO 操作 |
| Coroutine | ❌ 不创建线程 | Unity 分帧执行 | 游戏流程、计时、动画 |
十四、一个简单判断方法
遇到任务时可以先问:
问题 1:这个任务是不是 CPU 密集型?
text
是
↓
考虑多线程 / Task.Run / Parallel
例如:
text
大量计算
寻路
数据处理
问题 2:是不是主要在等待 IO?
text
是
↓
考虑 async/await
例如:
text
网络
文件
数据库
问题 3:是不是 Unity 中的流程控制?
text
是
↓
考虑 Coroutine
例如:
text
等待 2 秒
下一帧执行
动画播放期间等待
十五、面试一句话回答
如果面试官问:
"C# 多线程和异步有什么区别?"
可以回答:
多线程主要解决任务并发执行的问题,通过多个线程同时执行任务;异步主要解决等待期间线程被阻塞的问题,
async/await可以让线程在等待 IO 等操作时去执行其他任务。两者不是互斥的,异步操作不一定创建新线程,而Task.Run则可以把 CPU 密集型任务放到线程池线程执行。
十六、最终记忆
text
多线程
↓
多个线程执行
↓
重点:并发 / 并行
异步
↓
等待时不阻塞线程
↓
重点:提高线程利用率
Task
↓
表示一个正在进行 / 即将完成的操作
↓
不等于线程
Task.Run
↓
把同步工作交给线程池
↓
常用于 CPU 密集型任务
async/await
↓
异步等待
↓
常用于 IO 密集型任务
Coroutine
↓
Unity 主线程分帧执行
↓
不等于多线程
一句话总结
多线程关注"谁来执行",异步关注"等待时怎么办",Task 是任务抽象,Task.Run 可以使用线程池,而 async/await 本身不等于创建线程。