C# 异步回调与等待机制全解

 更新时间:2026年04月03日 09:33:41   作者:MutoKazuo  
文章介绍了ManualResetEvent和AutoResetEvent的特性、适用场景以及与TaskCompletionSource和SemaphoreSlim等现代异步方法的对比,对C# 异步回调与等待机制相关知识感兴趣的朋友跟随小编一起看看吧

TaskCompletionSource (TCS)

一个可以手动控制状态的 Task。它允许你创建一个任务,并在稍后的某个时间点手动将其标记为“已完成”。

Task.WhenAny

超时处理机制。通过将“结果任务”和“延迟任务(Task.Delay)”放在一起竞争,确保程序不会因为消息丢失而永久死锁。

private TaskCompletionSource<string> _transTaskSource;
private TaskCompletionSource<bool> _transAviResultSource;
public bool TransResult{get;set;}
public string ImageFileName{get;set;}
private void OnAppMessageReceived(AppMessageEventArgs obj)
{
    if (obj.MessageId == "xxx")
    {
        Dictionary<string, object> dic = JsonHelper.Deserialize<Dictionary<string, object>>(obj.Value);
        if (dic["FileName"].ToString().Contains("avi"))
        {
            bool result = dic["Result"].ToString() == "1";
            _transAviResultSource?.TrySetResult(result);
        }
        else
        {
            string fileName = dic["FileName"].ToString();
            _transTaskSource?.TrySetResult(fileName);
        }
    }
}
protected virtual async Task<string> OnTransStandard(string fileFormat)
{
	switch (fileFormat)
    {
		case "avi":
			_transAviResultSource = new TaskCompletionSource<bool>();
			var result = _transService.Trans2Avi();
			var timeoutTask = Task.Delay(TimeSpan.FromSeconds(15));
			var completedTask = await Task.WhenAny(_transAviResultSource.Task, timeoutTask);
			if (completedTask == _transAviResultSource.Task)
			{
				TransResult = result;
			}
		break;
		default:
			_trasnTaskSource = new TaskCompletionSource<string>();
            _transService.Trans();
            var timeoutTask = Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
            var completedTask = await Task.WhenAny(_trasnTaskSource.Task, timeoutTask);
            if (completedTask == _trasnTaskSource.Task)
            {
                ImageFileName = await _trasnTaskSource.Task;
            }
            else
            {
                // TODO
            }
		break;
	}
}

优化版本

using System.Collections.Concurrent;
// 使用字典支持并发,Key 为唯一标识(如文件名或请求ID)
private readonly ConcurrentDictionary<string, TaskCompletionSource<string>> _transTasks = new();
private readonly ConcurrentDictionary<string, TaskCompletionSource<bool>> _aviTasks = new();
private void OnAppMessageReceived(AppMessageEventArgs obj)
{
    if (obj.MessageId != "xxx") return;
    var dic = JsonHelper.Deserialize<Dictionary<string, object>>(obj.Value);
    string fileName = dic["FileName"]?.ToString() ?? string.Empty;
    if (fileName.Contains("avi"))
    {
        bool result = dic["Result"]?.ToString() == "1";
        // 尝试从字典中移除并设置结果,确保只处理一次
        if (_aviTasks.TryRemove(fileName, out var tcs))
        {
            tcs.TrySetResult(result);
        }
    }
    else
    {
        if (_transTasks.TryRemove(fileName, out var tcs))
        {
            tcs.TrySetResult(fileName);
        }
    }
}
protected virtual async Task<string> OnTransStandard(string fileFormat, string fileName)
{
    // 1. 设置超时取消令牌
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(fileFormat == "avi" ? 15 : 30));
    try
    {
        if (fileFormat == "avi")
        {
            var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
            _aviTasks[fileName] = tcs;
            _transService.Trans2Avi();
            // 使用 Register 绑定取消令牌到 TCS
            using (cts.Token.Register(() => tcs.TrySetCanceled()))
            {
                TransResult = await tcs.Task;
                return fileName;
            }
        }
        else
        {
            var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
            _transTasks[fileName] = tcs;
            _transService.Trans();
            using (cts.Token.Register(() => tcs.TrySetCanceled()))
            {
                ImageFileName = await tcs.Task;
                return ImageFileName;
            }
        }
    }
    catch (OperationCanceledException)
    {
        // 统一处理超时逻辑
        HandleTimeout(fileName, fileFormat);
        return string.Empty;
    }
    finally
    {
        // 确保清理字典,防止内存泄漏
        _aviTasks.TryRemove(fileName, out _);
        _transTasks.TryRemove(fileName, out _);
    }
}

同步阻塞式的等待机制(同步原语)

传统的线程同步对象 ManualResetEvent 来强制当前线程“停下”执行,直到收到特定的信号。

调用 OnDataTransformation 的线程会被物理挂起,不消耗 CPU 周期,但会占用一个线程资源,直到 _event.Set() 被调用。

特性ManualResetEvent (手动)AutoResetEvent (自动)
放行数量调 Set() 后,所有正在等待的线程都会被放行。调用 Set() 后,仅有一个线程会被放行。
复位方式必须手动调用 Reset() 才会再次阻塞线程。只要有一个线程通过,它会自动回到阻塞状态。
典型场景广播/通知:一个信号通知多个任务同时开始。同步/排队:确保对某个资源的访问是串行的。
public bool TransResult { get; set; }
public string Filenames { get; set; }
private static readonly ManualResetEvent _event = new ManualResetEvent(false);
private void OnAppMessageReceived(AppMessageEventArgs obj)
{
    if (obj.MessageId == "xxx")
    {
        Dictionary<string, object> dic = JsonHelper.Deserialize<Dictionary<string, object>>(obj.Value);
        if (dic["FileName"].ToString().Contains("avi"))
        {
            TransResult = dic["Result"].ToString() == "1";
            _event.Set();
        }
        else
        {
            Filenames = dic["FileName"].ToString();
            _event.Set();
        }
    }
}
public virtual void OnDataTransformation()
{
	cancellationToken.ThrowIfCancellationRequested();
	_TranService.TransImage();
    _event.Reset();
    _event.WaitOne();
	cancellationToken.ThrowIfCancellationRequested();
}

选择 ManualResetEvent 还是 AutoResetEvent 取决于你的控制目标:是想给“一群人”发信号,还是想让“一个人”过闸机。

1. ManualResetEvent(手动重置)

适用场景:状态通知 / 广播 (Broadcasting)

它通常用于表示一个“开关”或“阶段完成”的状态。一旦这个状态达成了,所有依赖它的线程都可以继续。

典型例子:系统初始化

  • 你的主程序启动时有多个后台服务(数据库连接、缓存加载、配置文件读取)。
    • 主线程调用 _initEvent.WaitOne()
    • 只有当所有初始化工作全部完成后,才调用一次 Set()
    • 此时,所有卡在 WaitOne 处的逻辑都会同时被释放。

典型例子:暂停/恢复功能

  • 在下载器或播放器中,按下“暂停”即 Reset()(关闸),所有下载线程 WaitOne;按下“开始”即 Set()(开闸),所有线程同时恢复工作。

2. AutoResetEvent(自动重置)

适用场景:独占资源 / 生产者-消费者 (Queueing)

它通常用于确保任务的串行化执行,或者作为简单的线程间信号传递。

典型例子:任务队列(单线程处理)

  • 你有一个后台线程专门处理发邮件的操作。
    • 当有新邮件进入队列时,调用一次 Set()
    • 后台处理线程 WaitOne() 收到信号,起来发一封邮件。
    • 发完后,由于是 AutoReset,它会自动回到阻塞状态,等待下一次 Set()

典型例子:由于硬件限制的互斥访问

  • 像你代码中这种“发送指令 -> 等待硬件回传”的模式。如果你想确保发一个收一个,且不希望第二个指令在第一个指令没返回前就跑掉,AutoResetEvent 更安全,因为它处理完一次会自动“关门”。

3. 对比

维度ManualResetEventAutoResetEvent
形象比喻大门的闸刀开关:拉上去,所有人都能进;拉下来,所有人都得停。地铁的旋转闸机:刷一次卡(Set),只能进去一个人,进去后闸机立刻锁死。
核心逻辑状态驱动:侧重于“某个条件是否达成”。事件驱动:侧重于“某个动作是否发生”。
重置时机你认为这个状态不再有效时(手动)。线程穿过 WaitOne 的那一刻(自动)。

4. 为什么用得少了?

在高性能开发中,这两个类正逐渐被以下方案取代:

  1. TaskCompletionSource (TCS)
    1. 理由:它是异步非阻塞的(Async/Non-blocking)。前面的 Manual/Auto 都会死死占住一个操作系统线程,非常浪费资源。
  2. SemaphoreSlim (信号量)
    1. 理由:它比 AutoResetEvent 更强大。它支持 WaitAsync(异步等待),而且可以控制允许 N 个线程同时通过,而不仅仅是一个。
  3. ManualResetEventSlim
    1. 理由:如果你非要用同步等待,请优先使用带 Slim 后缀的版本。它在等待时间很短时会先进行“自旋(Spin)”,不直接切换到昂贵的内核模式,性能更好。

异步等待 (Await) vs 同步挂起 (Wait)

维度TaskCompletionSourceManual/AutoResetEvent
编程模型异步 (Asynchronous)。基于 Task,符合现代 .NET 开发习惯。同步 (Synchronous)。基于操作系统的内核对象。
线程利用率。await 时线程会释放回线程池,去处理其他任务。。当前线程被彻底卡死(Block),什么都干不了。
超时处理灵活。通过 Task.Delay 或 CancellationToken 轻松实现。较硬。需给 WaitOne(timeout) 传参,且写法略显臃肿。
UI 响应性友好。如果在 UI 线程调用,界面不会卡死。危险。如果在 UI 线程调用,界面会直接崩溃/无响应(Deadlock)。
并发支持较好。通过 tcs 实例可以区分不同的请求。。static 的 _event 意味着全局只能同时处理一个任务。

第一种 (TaskCompletionSource) —— 手机短信提醒: 你该干嘛干嘛(去洗澡、打游戏),线程被释放回去了。快递到了,手机响了(Task 完成),你再回到门口处理快递。这种是非阻塞的。

第二种 (ManualResetEvent) —— 站在门口死等: 你推掉了一切活动,就站在门口盯着路口(线程被挂起)。快递员没来,你哪也不去,也不说话。快递员一招手(Set),你立刻动起来。这种是同步阻塞的。

同步方式 (EventWaitHandle)

// 这是一条死胡同,除非有人开门,否则车(线程)就停死在这里
_event.WaitOne();
// 只有开门后,才能跑这一行
DoNextStep();
  • 后果:如果你在 UI 线程(如点击按钮)里这么写,你的软件界面会直接“未响应”,因为 UI 线程被阻塞了,没法处理鼠标点击和界面刷新。

异步方式 (TaskCompletionSource)

// 这是一条路口,车(线程)发现红灯,就先掉头去干别的活了
await _tcs.Task;
// 绿灯亮了(SetResult),会有另一辆车(或原车)回来继续跑
DoNextStep();
  • 后果:UI 依然丝滑。await 释放了当前线程,让它回消息循环里去处理界面绘制,等结果到了再回来。

.NET中的位置

在 .NET 专门的同步分类中,它们属于内核模式同步对象(Kernel-mode objects)

类别代表组件特点
内核模式 (同步)ManualResetEvent, AutoResetEvent, Mutex重型。涉及操作系统内核切换,跨进程可用,但性能开销大。
混合模式 (同步)ManualResetEventSlim, SemaphoreSlim轻量。先自旋再挂起,性能极高,是现代同步的首选。
异步模式TaskCompletionSource, Task.WhenAny现代。完全不阻塞线程,支撑高并发的核心。

到此这篇关于C# 异步回调与等待机制全解的文章就介绍到这了,更多相关C# 异步回调与等待机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • datagridview实现手动添加行数据

    datagridview实现手动添加行数据

    这篇文章主要介绍了datagridview实现手动添加行数据,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • C#中的协变与逆变深入讲解

    C#中的协变与逆变深入讲解

    这篇文章主要给大家介绍了关于C#中协变与逆变的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-12-12
  • C# swagger ui增加访问限制方式

    C# swagger ui增加访问限制方式

    本文介绍了如何在C#中使用Swagger UI并增加访问限制,通过创建`SwaggerBasicAuthMiddleware`类和`MiddlewareExtension`类,并在`Startup.cs`的`Configure`方法中注入`app.UseSwaggerBasicAuth()`,从而实现对Swagger页面的访问控制
    2025-02-02
  • 基于C#实现获取本地磁盘目录

    基于C#实现获取本地磁盘目录

    这篇文章主要为大家详细介绍了如何利用C#实现获取本地磁盘目录的功能,文中的示例代码讲解详细,对我们学习C#有一定的帮助,感兴趣的小伙伴可以跟随小编一起了解一下
    2022-12-12
  • C#实现窗体间传值实例分析

    C#实现窗体间传值实例分析

    这篇文章主要介绍了C#实现窗体间传值的方法,结合实例形式较为详细的分析了C#针对窗体间传值的处理技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11
  • c# 如何使用 My 命名空间

    c# 如何使用 My 命名空间

    这篇文章主要介绍了c# 如何使用 My 命名空间,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下
    2020-10-10
  • C#关联自定义文件类型到应用程序并实现自动导入功能

    C#关联自定义文件类型到应用程序并实现自动导入功能

    今天通过本文给大家分享C#关联自定义文件类型到应用程序并实现自动导入功能,代码中写入了两个注册表,实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2021-09-09
  • c# Parallel类的使用

    c# Parallel类的使用

    这篇文章主要介绍了c# Parallel类的使用,帮助大家实现数据与任务的并行,感兴趣的朋友可以了解下
    2020-11-11
  • C#实现集合转换成json格式数据的方法

    C#实现集合转换成json格式数据的方法

    这篇文章主要介绍了C#实现集合转换成json格式数据的方法,涉及C#针对dataTable、Enumerable及Json格式数据的遍历及转换操作相关技巧,需要的朋友可以参考下
    2016-07-07
  • C#使用Spire.PDF for .NET实现为PDF添加X/Y页码

    C#使用Spire.PDF for .NET实现为PDF添加X/Y页码

    本文将引导您使用 C# 编程语言,结合强大的第三方库 Spire.PDF for .NET,轻松实现在 PDF 文档的每一页底部添加“第 X 页 / 共 Y 页”格式的页码,从而优化您的文档处理流程
    2025-12-12

最新评论