不使用TaskCompletionSource实现循环Task的暂停与恢复
问题解答
你当前的暂停逻辑确实是通过取消Task实现的
你的判断完全正确,现有代码的暂停本质是终止正在运行的计数任务:
- 点击暂停时直接调用
tokenSource.Cancel()触发取消信号,countNumber循环内的ThrowIfCancellationRequested()会抛出OperationCanceledException,直接终止整个Task的执行 - 点击恢复时是新建了一个CancellationTokenSource,从保存的
_nCurrentCount值启动全新的计数Task,并不是唤醒之前被暂停的旧Task - 这套逻辑虽然能正常运行,但有潜在bug:取消后立刻调用
tokenSource.Dispose(),如果此时Task.Delay还在监听该token,有概率触发对象已释放的异常。
无需取消Task的暂停实现方法
不需要销毁重建Task,用异步等待信号的方式就能实现Task的挂起和恢复,全程保持同一个Task运行,核心是用适配异步场景的SemaphoreSlim做暂停状态开关,CancellationToken仅用于真正需要终止计数的场景(比如用户主动取消、关闭窗口),不再承担暂停功能。
核心实现逻辑
- 新增类级别的
SemaphoreSlim实例作为暂停信号,初始计数设为1,代表非暂停放行状态 - 计数循环每次迭代前先异步等待信号量:如果处于暂停状态,代码会停在等待处不往下执行,直到收到恢复信号,整个Task不会退出
- 点击暂停/恢复按钮时,只需要切换信号量的计数状态即可,不需要触发Task取消
改造后代码参考
首先定义类级别字段:
// 原有字段保留 private int _nCurrentCount = 0; private CancellationTokenSource _countCts; // 新增暂停信号量,初始为放行状态 private readonly SemaphoreSlim _pauseSignal = new SemaphoreSlim(1, 1); private bool _isPaused = false;
按钮点击事件改造:
private void btnStart_Click(object sender, EventArgs e) { btnCancel.Enabled = true; if (_isPaused) { // 恢复逻辑:释放信号量,放行等待中的计数逻辑 _pauseSignal.Release(); _isPaused = false; btnStart.Text = "PAUSE"; } else if (_countCts != null) { // 暂停逻辑:将信号量计数置0,循环走到等待处就会挂起 _pauseSignal.Wait(0); _isPaused = true; btnStart.Text = "RESUME"; } else { // 首次启动计数 _countCts = new CancellationTokenSource(); btnStart.Text = "PAUSE"; _nCurrentCount = 0; countNumber(_nCurrentCount, _countCts.Token).ContinueWith((t) => { // 计数结束后重置状态 if (_isPaused) { _pauseSignal.Release(); _isPaused = false; } _countCts.Dispose(); _countCts = null; if (InvokeRequired) { Invoke((MethodInvoker)(() => { txtCount.Text = _nCurrentCount.ToString(); btnStart.Text = "START"; btnStart.Enabled = true; })); } }, TaskContinuationOptions.ExecuteSynchronously); } }
计数方法改造:
private async Task countNumber(int pnStart, CancellationToken pCancellationToken) { if (pnStart < 0) pnStart = 0; int targetCount = Convert.ToInt32(txtInput.Text); for (int i = pnStart; i <= targetCount; i++) { pCancellationToken.ThrowIfCancellationRequested(); // 等待暂停信号,暂停状态下会在此异步等待直到恢复,Task不会终止 await _pauseSignal.WaitAsync(pCancellationToken); _pauseSignal.Release(); // 放行后立刻释放信号,保证下次循环能正常判断状态 _nCurrentCount = i; // 跨线程更新UI必须切回UI线程 if (InvokeRequired) { Invoke((MethodInvoker)(() => txtCount.Text = _nCurrentCount.ToString())); } await Task.Delay(500, pCancellationToken); } }
补充:原代码直接在异步Task中访问UI控件
txtInput.Text、txtCount.Text属于线程不安全操作,改造后统一通过Invoke切回UI线程更新,避免随机抛出跨线程异常。
方案优势
- 全程只有一个计数Task运行,暂停时只是异步等待信号,没有销毁、重建Task的额外开销
- 职责划分清晰:CancellationToken只负责真正的终止操作,暂停/恢复完全由信号量控制
- 规避了原实现中取消后立刻释放TokenSource导致的潜在异常
内容的提问来源于stack exchange,提问作者QuangGiap
相关产品推荐
相关产品推荐

