如何通过 CancellationToken 停止异步进程?

12

我发现了一个可以在不冻结用户界面的情况下执行某些操作的代码。这段代码将在“开始工作”按钮被按下时执行。我认为用户会通过“停止”按钮停止此工作。所以我在MSDN上找到了这篇文章:https://msdn.microsoft.com/en-us/library/jj155759.aspx。但是,在这段代码中应用CancellationToken很难。有人能帮忙解决这个问题吗?

我只使用public static async Task<int> RunProcessAsync(string fileName, string args)方法。

代码(来自https://dev59.com/WWgv5IYBdhLWcg3wMN0U#31492250):

public static async Task<int> RunProcessAsync(string fileName, string args)
{
    using (var process = new Process
    {
        StartInfo =
        {
            FileName = fileName, Arguments = args,
            UseShellExecute = false, CreateNoWindow = true,
            RedirectStandardOutput = true, RedirectStandardError = true
        },
        EnableRaisingEvents = true
    })
    {
        return await RunProcessAsync(process).ConfigureAwait(false);
    }
}

// This method is used only for internal function call.
private static Task<int> RunProcessAsync(Process process)
{
    var tcs = new TaskCompletionSource<int>();

    process.Exited += (s, ea) => tcs.SetResult(process.ExitCode);
    process.OutputDataReceived += (s, ea) => Console.WriteLine(ea.Data);
    process.ErrorDataReceived += (s, ea) => Console.WriteLine("ERR: " + ea.Data);

    bool started = process.Start();
    if (!started)
    {
        //you may allow for the process to be re-used (started = false) 
        //but I'm not sure about the guarantees of the Exited event in such a case
        throw new InvalidOperationException("Could not start process: " + process);
    }

    process.BeginOutputReadLine();
    process.BeginErrorReadLine();

    return tcs.Task;
}

使用方法:

var cancelToken = new CancellationTokenSource();
int returnCode = async RunProcessAsync("python.exe", "foo.py", cancelToken.Token);
if (cancelToken.IsCancellationRequested) { /* something */ }

当点击启动按钮时,它会启动一些Python脚本。 当脚本正在运行并且用户想要停止它时,用户按下停止按钮。 然后程序执行以下代码。

cancelToken.Cancel();
非常感谢您阅读这个问题。

我可以知道为什么我的问题没有任何回答或评论就被评为-1分吗? - youngminz
1
你希望 CancellationToken 做什么?终止进程吗? - Stephen Cleary
@StephenCleary:感谢您的评论!是的,我想要做的就是立即终止程序。 - youngminz
2
这个问题根本不清楚。你发布的代码中甚至没有 CancellationToken!更别提任何问题陈述了,除了可能是“很难”。请提供一个可靠地重现你遇到的任何问题的好的 [mcve]。同时,请提供一个精确的问题描述:解释一下代码现在的作用以及你想要它代替的内容。 - Peter Duniho
@PeterDuniho 我添加了一些简短的使用代码。感谢您的建议! - youngminz
2个回答

24

简单来说,当令牌被取消时,您只需调用process.Kill()即可:

cancellationToken.Register(() => process.Kill());

但这样做存在两个问题:

  1. 如果你试图杀死一个还不存在或已经终止的进程,你会得到一个 InvalidOperationException 异常。
  2. 如果你不调用 Dispose() 释放从 Register() 返回的 CancellationTokenRegistration,并且 CancellationTokenSource 的寿命很长,那么就会有内存泄漏,因为注册信息将与 CancellationTokenSource 一样保存在内存中。

根据你的需求和对代码整洁度(即使代价是复杂性)的需要,忽略问题 #2 并通过在 catch 中捕获异常来解决问题 #1 可能是可以接受的。


调用Register()方法应该放在process.Start()方法的前面还是后面?与Exited事件的调用有关吗?如果需要,可以将其作为单独的问题发布。我的示例类似于您在https://dev59.com/WWgv5IYBdhLWcg3wMN0U#31492250中的答案。 - squashed.bugaboo
2
你也可以少用括号来实现同样的效果:cancellationToken.Register(process.Kill); - BillHaggerty

2

现在很简单:

process.WaitForExitAsync(token);

await process.WaitForExitAsync(token); 为了完整性。 - Sean Feldman
在评论中,OP指出取消应该导致外部进程终止,这构成了@svick答案的核心。取消传递给WaitForExitAsync()的令牌 不会 终止外部进程,它只是停止当前运行的程序等待它。上面描述的CancellationTokenRegistration仍然是必需的。 - undefined

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