C# 异步多线程开发实战:从入门到返回值处理
线程是操作系统运算调度的最小单位,进程则是资源分配的基本单元。在C#开发中,将耗时的业务逻辑由同步转为异步多线程执行,不仅能显著提升响应速度,还能有效防止界面卡顿。本文将深入解析基于委托(Action/Func)和 Task 的多种异步实现方案及其结果处理。
同步方式的局限性
在传统的同步模式下,程序按顺序依次执行。当点击按钮触发耗时操作时,业务逻辑与UI运行在同一主线程上。由于主线程被占用,前端UI会处于无响应状态。
以下代码演示了同步执行的典型结构:
private void btnSync_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
Console.WriteLine("************btnSync_Click同步方法 开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 5; i++)
{
string name = string.Format("{0}_{1}", "btnSync_Click", i);
this.DoSomethingLong(name);
}
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
同步方式的核心特征如下:
- 严格遵循顺序执行,前一个任务完成后才开启下一个。
- 全部在主线程中进行,UI无法响应用户请求。
- 执行时间累积较长。例如,本示例中的CPU密集型累加操作耗时约9.32秒,其控制台输出流程可参考下图:
异步多线程的基础实现
要实现非阻塞的异步执行,可以利用委托的 BeginInvoke 方法将任务派发至线程池。这使得主线程能够立即返回,从而保持UI界面的流畅。
第1步:使用 BeginInvoke 发起调用
- 目的:将耗时操作剥离出主线程,提升系统吞吐量。
- 入口:实例化委托并调用
BeginInvoke。 - 动作:为每个循环任务创建独立的异步请求。
private void btnAsync_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
Action action = new Action(DoSomethingLong);
for (int i = 0; i < 5; i++)
{
string name = string.Format("{0}_{1}", "btnAsync_Click", i);
// 异步发起调用,第三个参数为null表示不传递状态对象
action.BeginInvoke(name, null, null);
}
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
这种方式改变了任务的调度模式,不再受限于线性序列,同时也实现了UI与业务的解耦。其快速完成的反馈与控制台执行情况如下:
虽然这种“以资源换性能”的方式缩短了感知等待时间,但由于多个核心同时参与计算,系统的整体CPU利用率会有所上升。两者的资源消耗对比可见:
async/await 优化与执行流控制
在使用 BeginInvoke 时,由于任务并未强制挂起当前方法直到结束,后续统计总耗时的代码会立即执行,导致测得的“总耗时”往往不符合预期。为了实现既异步又能在逻辑末端正确捕获结果的优化方案,推荐使用 async 与 await 结合 Task.Run 的形式。
private async void btnAsync2_Click(object sender, EventArgs e)
{
Stopwatch watch = Stopwatch.StartNew();
Console.WriteLine("************开始,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
await DoAsync();
Console.WriteLine("************结束,线程ID= {0}************", Thread.CurrentThread.ManagedThreadId);
watch.Stop();
Console.WriteLine("************总耗时= {0}************", watch.Elapsed.TotalSeconds.ToString("0.00"));
}
private async Task DoAsync() {
Action action = new Action(DoSomethingLong);
List results = new List();
// 1. 提交所有异步任务
for (int i = 0; i < 5; i++) {
string name = string.Format("task_{0}", i);
IAsyncResult result = action.BeginInvoke(name, null, null);
results.Add(result);
}
// 2. 轮询等待所有任务完成
await Task.Run(() => {
while (results.Count > 0) {
for (int i = 0; i < results.Count; i++) {
if (results[i].IsCompleted) {
results.RemoveAt(i); // 移除已完成项
break;
}
}
if (results.Count > 0) Thread.Sleep(200); // 适度休眠降低CPU空转
}
});
}
通过这种优化的执行方式,不仅保证了UI不被锁死,还准确地测量了整个批处理任务的真实运行时间(如示例所示约为3.26秒),相比同步方式有显著的性能提升:
其他异步处理机制与返回值获取
C# 提供了多种机制来处理异步调用后的反馈与返回值获取。开发者需要根据是否需要返回值以及是否要在后台进行复杂判断来选择合适的模式。
异步回调模式
利用 AsyncCallback 可以在异步任务完成后自动触发特定的函数块。这种方法适合在后台计算完成后需要自动更新某些全局变量或状态的场景。
// 在BeginInvoke时传入回调委托
Action action = DoComplexWork;
action.BeginInvoke((ar) => {
if (ar.IsCompleted)
{
Console.WriteLine("回调触发了异步结束逻辑");
}
}, null);
采用这种回调模式执行的结果时序及内部状态监控如下:
异步信号量模式 (WaitOne)
若需在启动异步任务后继续执行一段主逻辑,随后再同步等待任务彻底完成,可以使用 IAsyncResult.AsyncWaitHandle.WaitOne()。但需注意,如果在主线程直接调用此方法,会造成严重的界面卡顿。
带有返回值的处理 (Func & EndInvoke)
对于需要接收计算结果的业务(如数据库查询、文件读取等),应当使用带有泛型返回值的 Func<T> 代替 Action。要获取结果,必须在 BeginInvoke 之后调用对应的 EndInvoke 方法。
Func<string, int> func = GetResultValue;
// 注意:BeginInvoke没有参数,必须通过引用拿到委托实例来调用EndInvoke
IAsyncResult result = func.BeginInvoke("param1", null, null);
int finalAnswer = func.EndInvoke(result);
若在主线程直接阻塞等待返回值,同样会导致界面无响应,因此更推荐采用异步回调的方式来安全地接收返回值数据:
最终通过完整的事件流闭环,可以安全地完成带参输入与带返回值输出的整个生命周期管理:
总结而言,合理使用 BeginInvoke 配合适当的监听手段,是实现C#高性能服务端或桌面端应用的关键技术之一。
以上就是C# 异步多线程开发实战的详细内容,更多关于C# 多线程的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













