C# 异步回调与等待机制

文章目录

TaskCompletionSource (TCS)

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

Task.WhenAny

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

csharp 复制代码
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;
	}
}

优化版本

csharp 复制代码
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() 才会再次阻塞线程。 只要有一个线程通过,它会自动回到阻塞状态。
典型场景 广播/通知:一个信号通知多个任务同时开始。 同步/排队:确保对某个资源的访问是串行的。
csharp 复制代码
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. 对比

维度 ManualResetEvent AutoResetEvent
形象比喻 大门的闸刀开关:拉上去,所有人都能进;拉下来,所有人都得停。 地铁的旋转闸机:刷一次卡(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)

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

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

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

同步方式 (EventWaitHandle)

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

异步方式 (TaskCompletionSource)

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

.NET中的位置

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

类别 代表组件 特点
内核模式 (同步) ManualResetEvent, AutoResetEvent, Mutex 重型。涉及操作系统内核切换,跨进程可用,但性能开销大。
混合模式 (同步) ManualResetEventSlim, SemaphoreSlim 轻量。先自旋再挂起,性能极高,是现代同步的首选。
异步模式 TaskCompletionSource, Task.WhenAny 现代。完全不阻塞线程,支撑高并发的核心。
相关推荐
啥都不懂的小小白2 小时前
前端CSS入门详解
前端·css
林恒smileZAZ2 小时前
前端大屏适配方案:rem、vw/vh、scale 到底选哪个?
开发语言·前端·css·css3
QQ5110082852 小时前
基于区块链的个人医疗咨询挂号信息系统vue
前端·vue.js·区块链
he___H3 小时前
Spring中的设计模式
java·spring·设计模式
程序员小寒4 小时前
JavaScript设计模式(八):命令模式实现与应用
前端·javascript·设计模式·ecmascript·命令模式
wgod4 小时前
new AbortController()
前端
UXbot4 小时前
UXbot 是什么?一句指令生成完整应用的 AI 工具
前端·ai·交互·个人开发·ai编程·原型模式·ux
棒棒的唐4 小时前
WSL2用npm安装的openclaw,无法正常使用openclaw gateway start启动服务的问题
前端·npm·gateway
哔哩哔哩技术5 小时前
使用Compose Navigation3进行屏幕适配
前端