任务取消未能停止任务执行

4
我有一个任务,它查询活动目录并用结果填充列表。我已经设置了任务可以被取消,但是当取消被调用时,任务仍然继续执行。我知道任务已经被取消,因为它返回并运行在任务返回上意图执行的操作,但是查询仍在后台运行,使用内存和处理能力。任务可以重复启动和“取消”,每个任务迭代都会运行并使用资源。如何使取消实际上取消?
视图模型
private async Task RunQuery(QueryType queryType,
    string selectedItemDistinguishedName = null)
{
    StartTask();
    try
    {
        _activeDirectoryQuery = new ActiveDirectoryQuery(queryType,
            CurrentScope, selectedItemDistinguishedName);
        await _activeDirectoryQuery.Execute();
        Data = _activeDirectoryQuery.Data.ToDataTable().AsDataView();
        CurrentQueryType = queryType;
    }
    catch (ArgumentNullException)
    {
        ShowMessage(
            "No results of desired type found in selected context.");
    }
    catch (OutOfMemoryException)
    {
        ShowMessage("The selected query is too large to run.");
    }
    FinishTask();
}

private void CancelCommandExecute()
{
    _activeDirectoryQuery?.Cancel();
}

ActiveDirectoryQuery

public async Task Execute()
{
    _cancellationTokenSource = new CancellationTokenSource();
    var taskCompletionSource = new TaskCompletionSource<object>();
    _cancellationTokenSource.Token.Register(
        () => taskCompletionSource.TrySetCanceled());
    DataPreparer dataPreparer = null;
    var task = Task.Run(() =>
    {
        if (QueryTypeIsOu())
        {
            dataPreparer = SetUpOuDataPreparer();
        }
        else if (QueryTypeIsContextComputer())
        {
            dataPreparer = SetUpComputerDataPreparer();
        }
        else if (QueryTypeIsContextDirectReportOrUser())
        {
            dataPreparer = SetUpDirectReportOrUserDataPreparer();
        }
        else if (QueryTypeIsContextGroup())
        {
            dataPreparer = SetUpGroupDataPreparer();
        }
        Data = GetData(dataPreparer);
    },
    _cancellationTokenSource.Token);
    await Task.WhenAny(task, taskCompletionSource.Task);
}

public void Cancel()
{
    _cancellationTokenSource?.Cancel();
}

Cancel()被一个绑定在Button上的Command调用。该任务可能需要几分钟才能执行完,并且可能会消耗数百兆字节的RAM。如果有帮助的话,我可以提供任何引用方法或其他信息。


4
在任务中,您需要查询令牌的 IsCancellationRequested 属性,并相应地从任务中返回。Cancel 不只是“杀死”任务,它只是将该属性的值设置为 true。 - KDecker
@KDecker 我猜你的意思是我需要重复查询它?考虑到实际工作是在几个方法深度下完成的,这是不切实际的,但我的设计可能是真正的问题所在。 - Michael Brandon Morris
1
@MichaelBrandonMorris 是的,你需要对其进行测试。你也可以使用ThrowIfCancellationRequested()。另请参阅如何:取消任务及其子级 - Conrad Frix
1
@MichaelBrandonMorris 取消令牌与其他变量一样。使该变量可用于静态方法的方法也是相同的。通常意味着将令牌传递到每个方法中。 - Conrad Frix
@ConradFrix 在我发完上一条评论后,我意识到它是多么不明智。我试图将所有的方法重写为异步任务... - Michael Brandon Morris
显示剩余2条评论
1个回答

5

取消操作是协作的,如果你想要取消操作,你需要编辑你的函数以进行取消。因此,Execute将变为:

public async Task Execute()
{
    _cancellationTokenSource = new CancellationTokenSource();
    var taskCompletionSource = new TaskCompletionSource<object>();

    //Token registrations need to be disposed when done.
    using(_cancellationTokenSource.Token.Register(
        () => taskCompletionSource.TrySetCanceled()))
    {
        DataPreparer dataPreparer = null;
        var task = Task.Run(() =>
        {
            if (QueryTypeIsOu())
            {
                dataPreparer = SetUpOuDataPreparer(_cancellationTokenSource.Token);
            }
            else if (QueryTypeIsContextComputer())
            {
                dataPreparer = SetUpComputerDataPreparer(_cancellationTokenSource.Token);
            }
            else if (QueryTypeIsContextDirectReportOrUser())
            {
                dataPreparer = SetUpDirectReportOrUserDataPreparer(_cancellationTokenSource.Token);
            }
            else if (QueryTypeIsContextGroup())
            {
                dataPreparer = SetUpGroupDataPreparer(_cancellationTokenSource.Token);
            }
            Data = GetData(dataPreparer, _cancellationTokenSource.Token);
        },
        _cancellationTokenSource.Token);
        await Task.WhenAny(task, taskCompletionSource.Task);
   }
}

然后在这些方法中。如果您在这些函数中有循环,需要从循环内部调用 token.ThrowIfCancellationRequested() 来取消请求。如果您没有循环并且调用了某个外部API,则需要使用该API的取消方法,希望该API接受CancellationToken,如果它不接受并且您需要调用.Cancel()方法,请像在Execute中一样使用Register方法。
如果API没有公开取消查询的方法,唯一安全停止查询的方法是将其移动到单独的exe。当执行查询时,您可以使用var proc = Process.Start(...)启动exe。要与其通信,请使用某种IPC,例如命名管道上的WCF,您可以在进程启动之前生成一个Guid并将其作为参数传递,然后使用该guid作为命名管道的名称。如果需要提前结束查询,请使用proc.Kill()来结束外部进程。

谢谢。这段代码是我自己写的,所以我已经修改了其中的每个方法,使其接受一个“CancellationToken”。 - Michael Brandon Morris
这就是我做的方式,我认为这是唯一的方法。不得不查询属性/传递令牌似乎相当混乱。我一直想问是否有更简洁的方法,但我怀疑它是否有效,因为它可以工作。// 这种方法也很好,因为您可以取消“深入”计算,而不必等待长时间运行的方法完成并在“任务级别”上取消。 - KDecker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接