如何在后台长任务循环中等待条件/事件触发以继续迭代?
用信号量实现事件触发式预缓存(替代轮询方案)
核心思路
要避免后台任务的轮询浪费,关键是让后台任务在缓存满时进入等待状态,只有当主循环处理完缓存项(空出位置)时,才主动通知后台任务继续填充。这里用SemaphoreSlim就能实现这种异步的等待/通知机制,完美适配你的场景。
修改后的代码示例
public class SomeClass { private readonly ConcurrentQueue<int> _sharedData = []; private Task? _backgroundTask; // 信号量初始计数设为缓存阈值5,最多允许同时填充5个缓存项 private readonly SemaphoreSlim _cacheSemaphore = new(5); private const int CacheThreshold = 5; private async Task Start() { using CancellationTokenSource cts = new(); _backgroundTask = Task.Run(() => BackgroundWork(cts.Token), cts.Token); for (int i = 0; i < 100; i++) { if (_sharedData.TryDequeue(out int data)) { // 取出缓存项后,释放一个信号量名额,通知后台可以加新的 _cacheSemaphore.Release(); Status = string.Join(", ", _sharedData.Select(a => a.ToString())); } await Task.Delay(300); } cts.Cancel(); try { await _backgroundTask; } catch (OperationCanceledException) { // 取消异常是预期情况,不用处理 } } private async Task BackgroundWork(CancellationToken ct) { try { while (!ct.IsCancellationRequested) { // 异步等待信号量,直到有空位或者任务被取消 await _cacheSemaphore.WaitAsync(ct); // 检查是否已取消,避免取消后还执行添加操作 if (ct.IsCancellationRequested) break; _sharedData.Enqueue(Random.Shared.Next()); Status = string.Join(", ", _sharedData.Select(a => a.ToString())); } } catch (OperationCanceledException) { // 捕获取消异常,正常退出 } finally { Status = "bg task done."; _cacheSemaphore.Dispose(); } } }
代码说明
- 信号量初始化:初始给5个名额,对应你的缓存阈值,后台任务可以先填满5个缓存项,之后每取出一个,就释放一个名额,后台才能再加一个。
- 主循环操作:每次成功取出队列里的元素后,调用
Release()释放信号量,相当于告诉后台“有空位了,可以加新的”。 - 后台任务逻辑:不再循环检查队列数量,而是等着信号量的通知,只有当有空闲名额或者任务被取消时,才会继续执行添加操作,完全避免了无效轮询。
- 取消处理:增加了对取消令牌的检查,确保任务能及时响应取消请求,不会做无用功。
适配实际文件缓存场景的调整
如果是实际的文件预加载场景,你可以这么改:
- 把
ConcurrentQueue<int>换成存储文件信息或缓存内容的队列,比如ConcurrentQueue<FileCacheItem>。 - 把
Enqueue(Random.Shared.Next())替换成异步的文件下载逻辑,比如await DownloadFileFromServerAsync(nextFileUrl, ct),这样不会阻塞线程。 - 根据文件大小、网络速度调整缓存阈值,比如设置成3-5个文件,平衡预加载速度和内存占用。
内容的提问来源于stack exchange,提问作者Roland Deschain
相关产品推荐
相关产品推荐

