【问题标题】:process.WaitForExit() asynchronouslyprocess.WaitForExit() 异步
【发布时间】:2010-10-02 23:38:33
【问题描述】:

我想等待进程完成,但process.WaitForExit() 挂起我的 GUI。是否有基于事件的方式,还是我需要生成一个线程阻塞直到退出,然后自己委托事件?

【问题讨论】:

  • 这是Process完全异步 实现,您还可以重定向标准输出和标准错误流stackoverflow.com/a/39872058/1212017
  • 虽然没有异步process.WaitForExit(),但有一个异步process.StandardOutput.ReadToEnd()。如果您启动的进程在终止之前没有关闭其标准输出(长时间),您可能需要考虑将此作为替代方案(尽管您仍然必须在之后process.WaitForExit() 以防万一)。

标签: c# .net winforms user-interface asynchronous


【解决方案1】:

从 .NET 4.0/C# 5 开始,使用异步模式表示这一点会更好。

/// <summary>
/// Waits asynchronously for the process to exit.
/// </summary>
/// <param name="process">The process to wait for cancellation.</param>
/// <param name="cancellationToken">A cancellation token. If invoked, the task will return 
/// immediately as canceled.</param>
/// <returns>A Task representing waiting for the process to end.</returns>
public static Task WaitForExitAsync(this Process process, 
    CancellationToken cancellationToken = default(CancellationToken))
{
    if (process.HasExited) return Task.CompletedTask;

    var tcs = new TaskCompletionSource<object>();
    process.EnableRaisingEvents = true;
    process.Exited += (sender, args) => tcs.TrySetResult(null);
    if(cancellationToken != default(CancellationToken))
        cancellationToken.Register(() => tcs.SetCanceled());

    return process.HasExited ? Task.CompletedTask : tcs.Task;
}

用法:

public async void Test() 
{
   var process = new Process("processName");
   process.Start();
   await process.WaitForExitAsync();

   //Do some fun stuff here...
}

【讨论】:

  • 谢谢,这很有帮助。不过,我遇到的一件事是,如果任务已经取消,tcs.SetResult(null) 会抛出 InvalidOperationException,如果在任务取消后进程退出,就会发生这种情况。为了解决这个问题,我将tcs.SetResult 替换为tcs.TrySetResult
  • 如果进程在我们注册退出处理程序之前停止,我们将永远等待。注册必须在 Start 之前完成,因此编写 StartAsync 方法要容易得多。大部分代码相同,但名为 StartAsync,在返回行之前带有一个 process.Start()。
  • 注意这里的取消代码并没有杀死进程,所以如果进程挂起,它将保持运行。如果想在取消的时候停止进程,改成如下:cancellationToken.Register(() => { process.Kill(); tcs.SetCanceled(); });
  • @LonelyPixel 因为它会在进程运行的所有时间内阻塞线程?
  • @LonelyPixel IMO, WaitForExit() 将在进程运行期间阻塞线程。取决于TaskScheduler,通常不是调用者,而是来自ThreadPool 的一个线程。答案中的解决方案可能不会阻塞 ThreadPool 线程。 (这对你来说可能不是问题。)
【解决方案2】:

process.EnableRaisingEvents = true;
process.Exited += [EventHandler]

【讨论】:

  • 确保在添加事件处理程序后还测试process.HasExited,以防进程在此之前退出。
  • @ygoe,设置Exited 可能更容易设置EnableRasingEvents。还应注意,这不适用于提升的目标进程,因为当您尝试设置 EnableRasingEvents(或阅读 HasExitedHandle 属性)时,它们会抛出 Win32Exception
【解决方案3】:

这是一个稍微干净的扩展方法,因为它清除了取消令牌注册和退出事件。它还处理竞争条件边缘情况,在这种情况下,进程可以在它开始之后但在附加 Exited 事件之前结束。它使用 C# 7 中新的局部函数语法。

public static class ProcessExtensions
{
    public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default)
    {
        var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

        void Process_Exited(object sender, EventArgs e)
        {
             tcs.TrySetResult(true);
        }

        process.EnableRaisingEvents = true;
        process.Exited += Process_Exited;

        try
        {
            if (process.HasExited)
            {
                return;
            }

            using (cancellationToken.Register(() => tcs.TrySetCanceled()))
            {
                await tcs.Task.ConfigureAwait(false);
            }
        }
        finally
        {
            process.Exited -= Process_Exited;
        }
    }
}

编辑:

我已将TaskCreationOptions.RunContinuationsAsynchronously 添加到TaskCompletionSource 构造函数中,该构造函数应该修复TrySetResult() 尝试同步运行延续时可能发生的死锁。

这种死锁条件实际上很难追踪。原来TaskCompletionSource.SetResult()默认是同步运行continuation,这会导致await下的所有代码都在SetResult()里面运行。这通常不是问题,因为Process.Exited 在线程池线程上运行。但是,整个Process.Exited 回调this 上的锁定中运行,在新线程池线程中,其中thisProcess 实例。这个可以看here

process.Exited -= Process_Exited; 也会锁定 this,这是由于 C# 语言规范实现事件处理程序的方式。最终结果是两个单独的线程池线程最终阻塞了Process 实例上的锁。疯狂的!更疯狂的是,如果你没有同步上下文,紧接在await WaitForExitAsync() 下方的代码也可能会同步运行,因此你最终会在lock 内部Process.Exited 内运行大量代码打回来。如果您不知道这种行为,TaskCompletionSource 是非常危险的!

解决方案是将TaskCreationOptions.RunContinuationsAsynchronously 添加到TaskCompletionSource 构造函数中。这允许TaskCompletionSource.SetResult() 立即返回,一切都会“按预期”工作。

【讨论】:

【解决方案4】:

如果您选择@MgSam 答案,请注意,如果您通过WaitForExitAsync 一些CancellationToken,将在指定延迟后自动取消,您可以获得InvalidOperationException。要解决这个问题,你需要改变

cancellationToken.Register(tcs.SetCanceled);

cancellationToken.Register( () => { tcs.TrySetCanceled(); } );

P.S.:不要忘记及时处理你的CancellationTokenSource

【讨论】:

    【解决方案5】:

    According to this linkWaitForExit() 方法用于使当前线程等待,直到关联进程终止。但是,Process 确实有一个您可以挂钩的 Exited 事件。

    【讨论】:

    • 我猜之前的评论者已经在这个答案中想象了“不”这个词?没有。
    【解决方案6】:

    Ryan 解决方案在 Windows 上运行良好。在 OSX 上发生了奇怪的事情,它可能是 tcs.TrySetResult() 的死锁!有两种解决方案:

    第一个:

    tcs.TrySetResult() 包装到Task.Run():

        public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default)
        {
            var tcs = new TaskCompletionSource<bool>();
    
            void Process_Exited(object sender, EventArgs e)
            {
                Task.Run(() => tcs.TrySetResult(true));
            }
    
            process.EnableRaisingEvents = true;
            process.Exited += Process_Exited;
    
            try
            {
                if (process.HasExited)
                {
                    return;
                }
    
                using (cancellationToken.Register(() => Task.Run(() => tcs.TrySetCanceled())))
                {
                    await tcs.Task;
                }
            }
            finally
            {
                process.Exited -= Process_Exited;
            }
        }
    

    关于这个和更多细节的对话: Calling TaskCompletionSource.SetResult in a non blocking manner

    第二个:

        public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken)
        {
            while (!process.HasExited)
            {
                await Task.Delay(100, cancellationToken);
            }
        }
    

    您可以将轮询间隔从 100 毫秒增加到更多,具体取决于您的应用程序。

    【讨论】:

      【解决方案7】:

      截至.NET5,现在Process class 提供了一个WaitForExitAsync

      await process.WaitForExitAsync( token );
      

      【讨论】:

      • 对于.NET Framework,我推荐CliWrap
      【解决方案8】:

      快速简单的解决方案:

      async Task RunProcessWait(string Path)
      {
          Process MyProcess = Process.Start(Path);
          while (!MyProcess.HasExited)
              await Task.Delay(100);
      }
      
      await RunProcessWait("C:\...")
      

      【讨论】:

      • 这就是我所需要的。我还在循环中添加了一个检查中止按钮 - 效果很好。
      【解决方案9】:

      使用System.Diagnostics.Process.Exited

      【讨论】:

        猜你喜欢
        • 2016-08-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-27
        • 1970-01-01
        • 2011-09-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多