C# 多线程与异步

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 本身不等于创建线程。

相关推荐
鱼听禅4 小时前
C# 学习笔记-使用 类型化客户端搭建HTTP客户端服务
学习·http·c#·工厂方法模式
geovindu6 小时前
CSharp:Condition Variable Pattern
后端·设计模式·c#·.net·.netcore·条件变量模式·同步型模式
淡海水7 小时前
11-03-Unity-List-T-和Dictionary-TKey-TValue-的性能调优实战
算法·unity·c#·list·dictionary
fogota8 小时前
C# 如何调用系统图标
c#·.net
△曉風殘月〆8 小时前
.NET是什么?为什么要选择.NET?
c#·.net·.net core·clr
fogota8 小时前
C# WPF 按钮加载字体图标 / 动物头像
c#·.net·wpf
fogota8 小时前
C# WPF 按钮加载系统图标 / 动物头像
c#·.net·wpf
军训猫猫头8 小时前
C# 封装 MySQL 自动写入与查询工具类(基于 MySqlConnector)
mysql·c#·.net·wpf
zxy284722530119 小时前
C# 音频倍速播放
c#·音频·naudio·soundtouch·倍速