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 现代。完全不阻塞线程,支撑高并发的核心。
相关推荐
百变梦仔10 小时前
Codex 写前端任务时,我用这张 GitHub Skill 地图先选工具
前端·github
Java后端的Ai之路10 小时前
03、Python - 抽象工厂模式
开发语言·python·设计模式·抽象工厂模式·通用智慧
2601_9621229711 小时前
nginx 部署前端vue项目
前端·vue.js·nginx
雪芽蓝域zzs11 小时前
第三节:目录结构重构(仿若依,优化层级,layout 与 components 同级)
前端·vue.js·重构
格林威12 小时前
C# 相机Burst模式图像采集:使用相机内存配合OpenCvSharp和Halcon实现短时间的高速采集的方法
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·工业相机
子非鱼a12 小时前
【WEB】[NewStarCTF 公开赛赛道]UnserializeOne
前端·javascript·html
南雨北斗13 小时前
Vue3 v-model中的自动事件绑定
前端
J总裁的小芒果13 小时前
参数过多-分批传参
c#
典典分享指南14 小时前
短视频分镜提示词:让 AI 出片的产品口播
java·c#·bash·composer·symfony
雪芽蓝域zzs14 小时前
第八节:Element Plus 基础、封装通用组件 + 路由守卫
前端·vue.js