C#如何异步向WhenAny添加任务实现Playwright请求监听超时
实现逻辑
核心用TaskCompletionSource<T>实现,这类原语就是专门为「任务触发时机不确定、需要在任意回调中手动控制任务完成状态」的异步场景设计的,完美适配动态事件监听+超时等待的需求,不需要提前预注册所有待等待的任务。
之前尝试的方案不适用的原因很明确:
Task.WhenAny要求传入所有已知的Task实例,但请求是运行时动态生成的,触发前根本拿不到对应的Task实例Monitor.TryEnter、信号量都属于线程同步原语,适配的是多线程阻塞等待场景,和异步事件回调的编程模型不匹配,硬写很容易出现死锁、异常丢失的问题
完整实现代码
string? result = null; // 初始化30秒超时的取消源 using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // 初始化TCS,指定异步执行延续任务,避免阻塞事件触发线程 var tcs = new TaskCompletionSource<string?>(TaskCreationOptions.RunContinuationsAsynchronously); // 新页面事件处理器 void OnNewPage(object? _, IPage newPage) { // 页面请求事件处理器,因为Playwright事件是无返回值的EventHandler签名,这里用async void是合规写法 async void OnPageRequest(object? _, IRequest request) { try { // 不匹配目标特征直接跳过 if (!request.Url.Contains("some_identifier")) return; // TCS已经完成(已经拿到结果/超时/报错)就不再处理 if (tcs.Task.IsCompleted) return; await newPage.GotoAsync(request.Url); var targetContent = await newPage.InnerTextAsync("body"); // 标记任务完成,传入结果 tcs.TrySetResult(targetContent); } catch (Exception ex) { // 捕获执行过程中的异常,传递到等待线程 tcs.TrySetException(ex); } } newPage.Request += OnPageRequest; // 任务结束后自动解绑请求事件,避免内存泄漏 _ = tcs.Task.ContinueWith(_ => newPage.Request -= OnPageRequest, TaskScheduler.Default); } context.Page += OnNewPage; // 任务结束后自动解绑新页面事件 _ = tcs.Task.ContinueWith(_ => context.Page -= OnNewPage, TaskScheduler.Default); // 超时触发时自动标记TCS完成,返回null timeoutCts.Token.Register(() => tcs.TrySetResult(null)); try { await mainPage.Locator("a#somelink").ClickAsync(); // 等待拿到结果或者超时 result = await tcs.Task; } finally { // 释放超时资源 timeoutCts.Dispose(); } return result;
注意事项
- 所有设置TCS状态的操作都用
TrySet*系列方法,避免多个请求同时匹配特征时重复设置状态抛出异常 - async void的事件处理器必须包裹try/catch,否则异常会直接抛到线程池未被捕获,导致程序崩溃
- 如果需要单独区分超时场景,可以把超时回调里的逻辑改成
tcs.TrySetCanceled(),在外层捕获OperationCanceledException处理超时逻辑即可 - 必须在任务结束后解绑注册的事件监听器,否则会造成内存泄漏,后续其他操作触发对应事件时也会误执行这段逻辑
内容的提问来源于stack exchange,提问作者Uxonith
相关产品推荐
相关产品推荐

