CodeGym /课程 /C# SELF /阻塞调用的问题

阻塞调用的问题

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

1. 介绍

阻塞调用 是任何在操作完成之前“阻塞”线程执行的调用。通常包括:

  • 读取或写入数据(例如,从磁盘)。
  • 耗时的数据库或网络请求。
  • 调用运行“慢”的第三方库。

在这些时刻线程只是站着,原地踏步并等待:“怎么样,什么时候有回应?”

生活中的例子


// 您的主要逻辑
Console.WriteLine("等待来自服务器的响应...");
string response = CallServer(); // 这里全都卡住了!
Console.WriteLine("服务器响应: " + response);

在服务器没有回应之前,您的线程“卡住”了——什么也做不了!

在应用中是什么样子

在不同类型的应用中表现不同。在控制台程序里看起来就像程序“冻结”了——光标停止闪烁,没有任何反应。在图形界面(比如 WinForms 或 WPF)中,窗口突然不响应,会出现大家熟悉的“not responding”,用户可能会咒骂你的软件和你。服务器应用中情况更糟:一个卡住的线程就意味着少了一个可服务的用户。

2. C# 中的阻塞调用实践

我们动手感受一下这个情况,使用练习项目。假设我们有一个魔法球应用。里面可能有这样的片段:


Console.WriteLine("请输入你要问魔法球的问题:");
string question = Console.ReadLine(); // 阻塞调用:等待输入!
Console.WriteLine("在想答案...");
Thread.Sleep(5000); // 模拟耗时工作
Console.WriteLine("答案:明天再试试!");
  • Console.ReadLine() — 会等待用户输入,直到输入完成(阻塞线程)。
  • Thread.Sleep(5000) — 人为地暂停,冻结线程。

问题: 如果我们只是处理控制台应用,这有什么不好?

回答: 在控制台应用中这不是很严重——用户自己会等。但如果用户不止一个呢?如果是一个每秒收到 1000 请求的服务器?或者是 GUI 应用,用户期待界面响应,而不是你的哲学思考?

示例:阻塞 UI


// 在 WinForms 中的按钮处理器:
private void button1_Click(object sender, EventArgs e)
{
    label1.Text = "加载中...";
    DoHeavyWork(); // 重操作(例如,数据库请求)
    label1.Text = "完成!";
}

问题: 当 DoHeavyWork 在运行时,窗口不会更新界面,不响应键盘和鼠标,不重绘。如果用户尝试关闭窗口——Windows 会显示“不响应”并建议结束任务。

3. 多线程世界的主要痛点

阻塞调用的问题

  • 资源浪费。 在 .NET 中每个线程(Thread)都是相当“重”的对象。操作系统会为其分配栈内存(通常 1MB)、句柄、同步资源等。如果线程“闲置”(等待文件或网络),它就在浪费这些资源(“什么也不做但占着内存”)。
  • 线程有限。 在服务器应用中通常存在线程池。如果所有线程都被阻塞——新的请求就无法处理。例如在 ASP.NET 中:如果所有可用线程都在等数据库响应,网站对新用户就“卡住”了。
  • 界面响应差。 在 GUI 应用中,如果 UI 线程被阻塞,窗口甚至不能响应“关闭”操作。
  • 性能下降。 被阻塞的线程越多,上下文切换越多,对操作系统的负载越大,整个机器运行越慢。

典型的阻塞调用来源

看看哪些调用会“无缘无故”阻塞线程:

  • 网络操作:调用 API、上传/下载文件、检查更新。
  • 磁盘/文件系统:读取或写入大文件。
  • 数据库:耗时的 SQL Server 查询或本地数据库查询。
  • 用户输入:耗时的 ReadLine——虽然是小事,但也会阻塞。
  • Thread.Sleep, Task.Delay:人为“冻结”线程。
示例(从网站加载数据)

using System.Net.Http;

HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // 阻塞的同步调用!
Console.WriteLine(result);

这里发生了什么?方法的 .Result 会阻塞线程直到收到响应!

4. 如何判断你的调用会阻塞线程?

很简单:如果方法在完成工作之前不会把控制权返回(等待、输入、网络、磁盘)——它就是阻塞调用。

  • 包含 Thread.Sleep, Task.Wait, .Result, .Wait() 的方法——会阻塞。
  • 那些在没有异步支持的情况下进行读/写数据的方法——也是阻塞的。
  • “挂起”的窗口或卡住的服务器——常见的迹象。

可视化元素:阻塞调用的流程图


+-------------------+
| 启动方法          |
+-------------------+
         |
         v
+-------------------+
| 调用阻塞方法      |
| (例如,读文件)  |
+-------------------+
         |
         v
+-------------------+
| 等待操作完成      |
+-------------------+
         |
         v
+-------------------+
| 返回方法并继续    |
| 执行               |
+-------------------+

5. 为什么在服务器应用中阻塞调用更糟糕

大多数现代应用都是网络或服务器类型。即使你写游戏,也有从服务器加载的部分。如果你做网站,肯定会和网络与磁盘打交道。

想象一下你的服务器每秒处理 1000 请求。每个请求都需要和数据库交互。你写了:


// Web 处理器
string data = db.ReadDataSync(); // 同步阻塞!
return new Response(data);

在数据库请求期间线程只是无聊地等待。一个请求还好,但当很多同时到来时,所有线程都会被占满。新的请求进不来,只能在队列里等。最后服务器不再像机器,而像一条长队,大家站着、打哈欠、等着有人挪动一小步。

示意:"同步请求处理"


请求 1: 占用线程 -> 等数据库 -> 释放线程
请求 2: 占用线程 -> 等数据库 -> 释放线程
...
请求 100: 没有空余线程,排队等待...

6. 异步 — 对抗阻塞调用的解药

为了解决阻塞问题,使用 异步调用。在 C# 中关键词是:async 和 await。

它们可以:

  • 不阻塞线程(尤其是宝贵的 UI 或服务器线程)。
  • 避免无意义地占用资源。
  • 改善 GUI 的响应性。
  • 在慢操作进行时不“占着”线程。

注意: 我们不会一开始就盲目使用异步,因为它需要理解线程和阻塞。现在你理解了阻塞调用的痛苦后,异步方式会对你和用户都是大大的提升。

表格:什么会阻塞,什么不会?

方法/调用 会阻塞线程吗 适合 UI/Server 吗
Console.ReadLine()
是 否
Thread.Sleep(1000)
是 否
File.ReadAllText()
是 否
Task.Delay(1000).Wait()
是 否
HttpClient.GetStringAsync().Result
是 否
await File.ReadAllTextAsync()
否 是
await Task.Delay(1000)
否 是
await HttpClient.GetStringAsync()
否 是

注意: await 只能在异步函数中使用(如 async Task ...),我们将在后面的讲座中详细讲解。

7. 与阻塞调用相关的经典错误

“阻塞 UI — 用户诅咒一切”


private void btnLoad_Click(object sender, EventArgs e)
{
    var data = BigFileReader.Read(@"C:\huge.dat"); // 阻塞调用!
    textBox1.Text = data;
}

在巨大文件读完之前,窗口会“卡住”。

“服务器挂起 — 客户跑了”


public IActionResult Download()
{
    var content = File.ReadAllBytes("bigfile.zip"); // 同步、耗时,阻塞 ASP.NET 线程!
    return File(content, "application/zip");
}

小型服务器(比如免费主机)可能会因为这种做法直接“崩溃”。

“异步方法 + .Wait()/.Result = 同步之死”


public void LoadData()
{
    var result = DoAsyncWork().Result; // 阻塞线程!
}

.Result 和 .Wait() 会把即便是优雅的异步代码变成普通的阻塞调用。

2
任务
C# SELF, 第 59 级, 课程 0
已锁定
通过暂停模拟阻塞调用
通过暂停模拟阻塞调用
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION