我正在尝试构建一个函数,该函数在目录中搜索文件并将它们添加到ObservableCollection
中。
此任务应该异步运行,以便UI保持响应,并在再次执行该方法时随时停止搜索。使得CCD_ 2被清除并且搜索再次开始。
我的问题是,当任务仍在运行时,我不知道如何以及何时以相同的方法取消CancellationTokenSource
此外,我不知道如何在函数上使用Async和Await,因为它分为三个方法。
现在看起来是这样的:
搜索功能:
CancellationTokenSource _tokenSource;
public async void SearchAsync()
{
Files.Clear();
FileCount = 0;
// Cancels Task if its running
if(_tokenSource != null)
{
_tokenSource.Cancel(); (throws Error right here. Task already disposed)
}
// Create new Cancellation Token
_tokenSource = new CancellationTokenSource();
CancellationToken token = _tokenSource.Token;
try
{
//Starts Task
await Task.Run(() => FindFiles(token));
}
catch (OperationCanceledException ex)
{
//When Canceled
MessageBox.Show("Test", "Test");
}
finally
{
//When finished
_tokenSource.Dispose();
}
}
查找文件:
private async Task FindFiles(CancellationToken token)
{
foreach (string filePath in Directory.EnumerateFiles(MainPath, "*.*", searchOption))
{
await AddFileAsync(filePath);
FileCount++;
if (token.IsCancellationRequested)
{
Files.Clear();
FileCount = 0;
token.ThrowIfCancellationRequested();
}
}
}
将文件添加到ObservableCollection:
我将其放入自己的方法中,因为它在LoadFiles()
中被多次调用。我只是简化了它。
public ObservableCollection<FileModel> Files { get; set; }
private async Task AddFileAsync(string filePath)
{
await Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
{
FileModel file = new()
{
FileName = Path.GetFileName(filePath),
FilePath = filePath
};
Files.Add(file);
}));
}
您的代码有几个问题:
- 您将处理
CancellationTokenSource
,但在下一次调用SearchAsync()
时,您将在已处理的CTS上调用.Cancel()
。这个呼叫当然失败了 - 在
FindFiles()
中使用async void
。你的await Task.Run(() => FindFiles(token));
会立即退出。除非绝对必要,否则永远不要使用async void
- 您可能有一个竞争条件,这取决于您的任务调度程序是否是多线程的
看看这个实现:
public sealed class Searcher
{
// Initialized here to avoid null checks.
private CancellationTokenSource _tokenSource = new CancellationTokenSource();
public async Task SearchAsync()
{
var newTokenSource = new CancellationTokenSource();
var oldTokenSource = Interlocked.Exchange(ref _tokenSource, newTokenSource);
if (!oldTokenSource.IsCancellationRequested)
{
oldTokenSource.Cancel();
}
try
{
//Starts Task
await Task.Run(() => FindFiles(newTokenSource.Token));
}
catch (OperationCanceledException)
{
//When Canceled
}
}
private async Task FindFiles(CancellationToken token)
{
await Task.Delay(1000, token);
}
}
因此,现在对SearchAsync()
的每次调用都会跟踪两个CTS:当前CTS和前一个CTS。这解决了问题1。
我把async void FindFiles
变成了async Task FindFiles
解决了问题2。我还将SearchAsync()
的签名更改为async Task
,以便调用者可以等待它。尽管这取决于函数的使用方式,并且不是必需的。
最后,我使用Interlocked.Exchange来解决问题3。此调用是原子调用。另一种选择是使用锁。
我已经删除了CTS处理代码,因为这并不容易。主要问题是,当您完成CTS时,应该调用.Dispose()
。因此,无论是在任务被取消时,还是在任务完成时。问题是,您无法轻松验证CTS是否已被处理(之后它处于不可用状态(。或者我不知道该怎么做。处理CTS既奇怪又困难,请阅读以下内容:何时处理CancellationTokenSource?那里的大多数答案都声称,如果CTS没有链接,那么GC会在某个时候正确地收集和处理它。
一种方法是用一个自定义类包装CTS,该类跟踪该CTS是否已被释放。并且对.Cancel()
和.Dispose()
的调用可能是用锁包装的。但后来一切都变得复杂起来。另一种选择是在访问ObjectDisposedException
时捕获它,但我不确定这是否正确。
另一方面,对于这个简单的用例,我认为你不必担心处理它。让GC收集它并处理它。CTS和令牌都不会泄漏到外部,如果你记得保持等待,那应该没问题。我会根据这个注释来做这件事:
注意
在发布对CancellationTokenSource的最后一个引用之前,请始终调用
ObservableCollection
0。否则,在垃圾收集器调用CancellationTokenSource对象的Finalize
方法之前,它正在使用的资源将不会被释放。
在我看来,如果不是绝对需要立即处理,那么不处理它是可以的。事实并非如此