CodeGym /课程 /C# SELF /异步 vs. 多线程 ( async/await

异步 vs. 多线程 ( async/awaitThread)

C# SELF
第 59 级 , 课程 1
可用

1. 介绍

在编程中,初学者(甚至有经验的开发者)常常把两个看起来相似但实际上不同的概念混淆:多线程异步。面试官喜欢问这个问题,目的是看你是否理解它们的区别,因为这直接影响到如何写出快速且响应良好的代码。

我们来弄清楚到底有什么区别。

多线程:很多手一起干活的时候

多线程 是用多个线程来组织程序工作。线程是执行流,一条“轨道”,处理器沿着它执行程序指令。一个进程(比如我们的 .NET 应用)可以启动多个线程,让不同的任务同时执行。

生活中的例子: 你是项目经理,把不同任务分配给不同同事,大家同时干活。比如一个写报告,一个给客户打电话,第三个做演示。

多线程的核心思想: 任务是真正并行发生的(或者在单核上通过快速上下文切换实现伪并行)。

异步:学会在等待时不闲着

异步 是组织代码的方式,让程序在等待长时间操作(比如网络响应或文件读取)时能去做别的事。异步代码不一定使用多个线程!它只是不会在等待某个操作时阻塞程序执行。

生活中的例子: 与其站在咖啡机前盯着滴咖啡,不如把做咖啡的事交给咖啡机,自己去做别的事(回邮件、看新闻),等咖啡好了再去取。

异步的核心思想: 在等待“长任务”时不要闲着,而是去做有用的事。

2. 到底有什么区别?

很多时候异步和多线程会一起用,这反而增加了混淆。但实际上这两种技术回答的是不同的问题:

  • 多线程 用于真正并行化工作,利用可用的处理器/核心。
  • 异步 用于智能地组织等待资源(网络 I/O、磁盘 I/O 等),避免阻塞线程。

它们可以结合使用,但并不必然相关。

简要可视化

多线程 (Threads) 异步 (Async)
何时使用? 当任务“吃掉”CPU(CPU-bound:计算、渲染、数组处理) 当任务在等待事件(I/O-bound:网络、磁盘、数据库)
代码做了什么? 占用处理器,启动多个线程,真正并行 在等待数据时释放线程—线程可以去做别的事
管理什么? 同时执行的线程数量 谁现在忙/空闲,操作完成时做什么
典型例子 视频压缩、图像渲染 文件下载、向服务器发起 HTTP 请求

示例 1:多线程 — 快速计算

假设我们有一个费时的任务:计算大范围数的和。


void ComputeSum(long start, long end)
{
    long sum = 0;
    for (long i = start; i <= end; i++)
    {
        sum += i;
    }
    Console.WriteLine($"从 {start} 到 {end} 的和 = {sum}");
}

// 同时启动三项任务 — 每个计算自己的一部分
Thread t1 = new Thread(() => ComputeSum(1, 1000_000_000));
Thread t2 = new Thread(() => ComputeSum(1000_000_001, 2000_000_000));
Thread t3 = new Thread(() => ComputeSum(2000_000_001, 3000_000_000));

t1.Start();
t2.Start();
t3.Start();

// 等待所有线程完成
t1.Join();
t2.Join();
t3.Join();

为什么这里需要线程?
因为这是 CPU 密集型工作。线程真实地占用处理器,如果你有多核机器,任务会更快完成。

示例 2:异步 — 等待服务器响应

网络是慢的,等待服务器响应时,线程可以是“空闲”的。


// 异步下载网页,线程不会被阻塞
async Task DownloadPageAsync()
{
    using HttpClient client = new HttpClient();
    string html = await client.GetStringAsync("https://dotnet.microsoft.com/");
    Console.WriteLine(html.Length);
}

为什么这里用异步?
我们让系统“开始下载”,同时不阻塞线程,等待数据到来时再继续执行。

3. 异步没有多线程:这是神话还是现实?

问题: 所有异步代码都会启动新线程吗?
回答: 不是!很多时候异步根本不需要额外线程。

例如,当你做 await file.ReadAsync(...) 时,.NET 在操作系统层面启动异步操作,调用该方法的线程立即变为“空闲”并返回到线程池。当操作完成时,线程池中的任意空闲线程会继续执行你的任务。

  • 如果你用同步方法(比如 file.Read(...))—线程就会傻等操作结束,什么也不做。
  • 异步代码会告诉处理器:“在等待时去做别的事吧!”

重要说明:


// 线程不会被阻塞,只是在等待操作准备好
await Task.Delay(1000); // 只是等一秒 — 不占用 CPU!

没有异步的多线程

有时候并行处理工作只能靠线程:重计算、大量数据循环等。在这种情况下,异步对加速计算本身无用,因为 CPU 本身会被完全占满。

经典场景:处理大文件


// 这段代码真实占用 CPU — 异步不会帮忙。
void CalculateHash(string file)
{
    byte[] data = File.ReadAllBytes(file); // 同步读取!
    // 计算哈希...
}

想加速?那就开多个线程,每个线程处理自己的文件。

4. 在你的应用中这看起来像什么?

异步操作 (await)

在我们的教学应用中可以加入异步加载数据。例如,请求汇率或天气信息时——最好用异步。


async Task GetWeatherAsync(string city)
{
    using HttpClient client = new HttpClient();
    string json = await client.GetStringAsync($"https://api.weather.com/{city}");
    // 在收到响应后继续处理
    Console.WriteLine($"城市 {city} 的天气: {json}");
}

底层发生了什么?

await 的调用会把你的方法“切成两段”:

  • 发起异步操作 — 操作发起后线程被“释放”,可以去做别的事。
  • 当数据到达 — 方法的其余部分会在线程池中某个空闲线程上继续执行。

用于复杂计算的多线程

如果我们的计算器(假设它开始处理大数组)需要并行化——这时候值得在单独线程中运行计算。


// 将大任务分成小块,每个线程计算自己的部分
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 4; i++)
{
    int rangeStart = i * 1000000;
    int rangeEnd = (i + 1) * 1000000 - 1;
    Thread t = new Thread(() => ComputeSum(rangeStart, rangeEnd));
    threads.Add(t);
    t.Start();
}
// 等待所有线程完成
foreach (Thread t in threads) t.Join();

5. 常见错误和异步使用注意事项

错误 №1:把 async 当作“提速”手段。
一个很常见的误解是以为 async 会让代码跑得更快。不是这样的。异步是关于响应性,不是关于速度。

如果任务是 CPU-bound(占用 CPU),异步不会让它更快。
如果任务是 I/O-bound(网络、磁盘操作),异步有用,因为线程不会白白等待,可以去做别的工作。

错误 №2:通过 Wait()Result 阻塞线程。
在异步代码中绝对不要调用 Wait() 或访问任务的 Result。这几乎总会导致线程阻塞和死锁。


// 不好的做法!会阻塞线程并导致问题
var result = GetDataAsync().Result;

async Task<string> GetDataAsync() { /* ... */ return "data"; }

正确做法是使用 await,不要阻塞线程。

错误 №3:异步和 UI。
在图形界面应用(WPF、WinForms)中,最大的问题是不要让 UI 线程卡住。如果在主线程运行耗时或阻塞操作,整个应用会“卡死”。异步能解决这个问题:把重活放后台做,界面保持响应。

错误 №4:没有统一的异步命名约定。
如果不在异步方法名后加上 Async 后缀,很容易搞错哪个方法是同步的,哪个是异步的。这会导致意外的阻塞和调用错误。务必给异步方法加上 Async 后缀。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION