在 WPF 的异步刷新场景中(尤其是 .NET Framework 4.0),"防重入" 和**"时序错乱"**是两个必须分开处理的"致命陷阱":
加上这套逻辑后,无论用户怎么狂点按钮,或者定时器如何触发,你的 TreeView 永远显示最新且正确的数据,绝不会出现"刷新后选不中"或"数据变回旧版本"的诡异问题。 😊
-
防重入(防止堆叠) :用户狂点按钮,或者 10 秒定时器触发时,上一次刷新还没结束。如果不加锁,会导致多个线程同时修改
TreeRoot,界面卡死或报错。 -
时序错乱(防止覆盖) :这是更隐蔽的坑。 比如第 1 次刷新 (耗时 3 秒)先发起,第 2 次刷新 (耗时 1 秒)后发起。第 2 次先完成并显示了最新数据 ,但随后第 1 次才完成,把界面强行回退成了旧数据------用户看到的数据是错的
-
针对你的 .NET 4.0 + WPF 环境,我为你设计了一套 "三层防护网",直接照着抄就能完美解决这两个问题。
🛡️ 第一层:防重入锁(拒绝堆叠)
-
这是最基础的,防止多个刷新任务同时执行。
改进点 :将
bool IsRefreshing升级为int类型,使用Interlocked.CompareExchange实现无锁线程安全,比 bool 更可靠。
cs
private int _isRefreshing = 0; // 0=空闲, 1=刷新中
private bool TryEnterRefresh()
{
// 原子操作:如果 _isRefreshing 为 0,则设为 1 并返回 true
return Interlocked.CompareExchange(ref _isRefreshing, 1, 0) == 0;
}
private void ExitRefresh()
{
Interlocked.Exchange(ref _isRefreshing, 0);
}
⏱️ 第二层:取消令牌(解决"后发先至")
- 利用
CancellationTokenSource取消过时的任务。当发起新刷新时,强制终止上一个还在后台运行的旧任务(前提是你的耗时任务支持取消检测)。
cs
private CancellationTokenSource _cts;
private void RefreshTreeAsync()
{
// 1. 取消上一个未完成的任务
if (_cts != null)
{
_cts.Cancel();
_cts.Dispose();
}
_cts = new CancellationTokenSource();
var token = _cts.Token;
// 防重入判断
if (!TryEnterRefresh()) return;
// ... 保存状态 ...
Task.Factory.StartNew(() =>
{
// 在耗时循环中检测是否被取消
if (token.IsCancellationRequested) return null;
Thread.Sleep(1500); // 模拟耗时
if (token.IsCancellationRequested) return null;
return GenerateMockData(DateTime.Now.Second % 5 + 1);
}, token)
.ContinueWith(task =>
{
// ... 更新 UI ...
}, TaskScheduler.Default);
}
🏆 第三层:版本号标记(解决"慢请求覆盖快请求"------终极奥义)
- 即使你取消了旧任务,但 .NET 4.0 的
Task取消是协作式 的(需要代码配合),如果旧任务正在Thread.Sleep或读取数据库卡住,无法立即停止,它依然可能在你更新完 UI 后强行覆盖。 - 解决方案 :给每次刷新分配一个递增的"版本号",只有最新版本号的任务才能更新 UI。
cs
private int _refreshVersion = 0;
private void RefreshTreeAsync()
{
if (!TryEnterRefresh()) return;
// 1. 递增版本号(获取当前操作序号)
int currentVersion = Interlocked.Increment(ref _refreshVersion);
// 保存状态(UI线程)...
Task.Factory.StartNew(() =>
{
// 耗时操作...
var data = GenerateMockData(DateTime.Now.Second % 5 + 1);
return data;
})
.ContinueWith(task =>
{
// 回到 UI 线程更新
Application.Current.Dispatcher.Invoke((Action)(() =>
{
try
{
// 2. 【核心校验】如果不是最新版本,直接丢弃,绝不更新界面!
if (currentVersion != _refreshVersion)
{
Console.WriteLine($"丢弃过时版本 {currentVersion},当前最新为 {_refreshVersion}");
return;
}
if (task.IsFaulted) { /* 处理异常 */ return; }
// 3. 执行真正的 UI 替换
TreeRoot = new ObservableCollection<NodeViewModel>(BuildTree(task.Result));
RestoreState(TreeRoot, selectedId, expandedIds);
}
finally
{
ExitRefresh();
}
}));
}, TaskScheduler.Default);
}
💡 不同场景下的组合策略
| 你的需求场景 | 推荐组合方案 |
|---|---|
| 操作频率低(用户点按钮) | 第一层(防重入锁) 即可。 |
| 定时器刷新(10秒)+ 手动按钮 | 第一层(防重入锁) + 第三层(版本号)。用户点击时,旧定时任务的数据来了直接丢弃,保证永远显示最新点击的数据。 |
| 耗时任务可中断(如支持取消的HttpClient) | 第一层 + 第二层(取消令牌),能节省后台资源(CPU/网络)。 |
| 耗时任务不可中断(如底层数据库驱动不支持Cancel) | 第一层 + 第三层(版本号) 。这是最稳妥的终极保命方案,强烈推荐你直接上这个组合。 |
- 结合上面三层防护的完整最终版模板
cs
private int _isRefreshing = 0;
private int _refreshVersion = 0;
private void RefreshTreeAsync()
{
if (Interlocked.CompareExchange(ref _isRefreshing, 1, 0) != 0) return;
int currentVersion = Interlocked.Increment(ref _refreshVersion);
int selectedId = -1;
var expandedIds = new HashSet<int>();
Application.Current.Dispatcher.Invoke((Action)(() =>
{
var selected = FindSelectedNode(TreeRoot);
if (selected != null) selectedId = selected.Id;
CollectExpandedIds(TreeRoot, expandedIds);
}));
Task.Factory.StartNew(() =>
{
Thread.Sleep(1500);
return GenerateMockData(DateTime.Now.Second % 5 + 1);
})
.ContinueWith(task =>
{
Application.Current.Dispatcher.Invoke((Action)(() =>
{
try
{
// 【灵魂逻辑】丢弃过时的刷新结果
if (currentVersion != _refreshVersion) return;
if (task.IsFaulted) throw task.Exception;
TreeRoot = new ObservableCollection<NodeViewModel>(BuildTree(task.Result));
RestoreState(TreeRoot, selectedId, expandedIds);
}
catch (Exception ex)
{
MessageBox.Show("刷新出错:" + ex.Message);
}
finally
{
Interlocked.Exchange(ref _isRefreshing, 0);
}
}));
}, TaskScheduler.Default);
}
🎯 总结
-
防重入 靠
Interlocked.CompareExchange,保证同一时间只有一个任务在跑。 -
时序错乱 靠
currentVersion != _refreshVersion,保证只有最新的数据能画到界面上。
cs
/// <summary>
/// 异步请求时序错乱防护器(版本号方案,.NET4.0 可用,无依赖)
/// 适用场景:选中项切换、Tab切换、分页、树节点加载明细
/// </summary>
public class AsyncRaceGuard
{
private long _version;
/// <summary>
/// 开启一次新请求,获取本次请求版本标记
/// </summary>
public long BeginNewRequest()
{
return Interlocked.Increment(ref _version);
}
/// <summary>
/// 校验本次请求是否已经过期
/// </summary>
public bool IsExpired(long requestToken)
{
// 当前全局版本 > 请求发起时的token → 已有新请求发起,旧结果丢弃
return _version > requestToken;
}
/// <summary>
/// 重置(清空所有历史请求,适合切换大页面)
/// </summary>
public void Reset()
{
Interlocked.Exchange(ref _version, 0);
}
}