异步进程的启动和等待其完成

38

我对.NET中的线程模型还不熟悉。您需要使用什么来完成以下操作:

  1. 启动一个处理文件的进程 (process.StartInfo.FileName = fileName;)
  2. 等待用户关闭进程或在一段时间后放弃线程。
  3. 如果用户关闭了进程,则删除文件。

启动进程并等待应该在不同于主线程的线程上进行,因为此操作不应影响应用程序。

示例:

我的应用程序生成HTML报告。用户可以右键单击某处并选择“查看报告” - 现在我在临时文件中检索报告内容,并启动处理HTML文件即默认浏览器的进程。问题是我无法清除,即删除临时文件。


控制台或WinForms。我启动的进程是本地机器上的。 - Bogdan Gavril MSFT
我有一个使用async/await的示例发布在这里--http://www.allampersandall.com/2013/03/net-process-async-await - Micah
你可能会对这篇文章感兴趣,它解释了如何使用async/await和超时处理进程。 - ChaseMedallion
1
可能是Is there any async equivalent of Process.Start?的重复问题。 - Chris Moschini
标记为重复的较新问题具有更多当前(因此可能更相关)的答案。 - Chris Moschini
显示剩余2条评论
6个回答

60

"等待必须是异步的" - 我并不是在开玩笑,但这不是一种自相矛盾的说法吗?然而,由于您正在启动一个 Process,因此Exited事件可能会有所帮助:

ProcessStartInfo startInfo = null;
Process process = Process.Start(startInfo);
process.EnableRaisingEvents = true;
process.Exited += delegate {/* clean up*/};

如果你想要真正等待(包括超时等待),那么:

if(process.WaitForExit(timeout)) {
    // user exited
} else {
    // timeout (perhaps process.Kill();)
} 

等待异步操作完成,也许可以使用不同的线程?

ThreadPool.QueueUserWorkItem(delegate {
    Process process = Process.Start(startInfo);
    if(process.WaitForExit(timeout)) {
        // user exited
    } else {
        // timeout
    }
});

@Grace仅凭这个很难说...但是除非您等待工作线程,否则您不正是期望看到的吗? - Marc Gravell
那么我应该在主方法中等待线程结束吗? - Grace
@Grace 这取决于你正在做什么;如果你的唯一目的是等待进程结束,那么为什么要在工作线程上这样做呢?需要更多背景信息。否则我无法给出有用的回答。 - Marc Gravell
@Grace,如果你不想阻塞原始请求,你需要使用某种类型的ajax/polling来处理这种情况;当旧请求完成后,你不能只是向旧请求写入。 - Marc Gravell
嗨@MarcGravell,我有一个问题。我做了同样的事情,当我打开一个.ini文件然后关闭它时,一切都很好,文件被清理了。但是当我打开一个.jpg文件时,情况就变得不同了,看起来进程根本没有退出,因此文件没有被收集。如果有帮助的话,当我打开一个.jpg文件时,进程为null,顺便说一句,我正在使用(string filename)参数的重载。有什么解决办法吗? - Mahdi Tahsildari
显示剩余3条评论

23

在这个老问题上添加一种高级选项。如果你想等待进程退出而不阻塞任何线程,同时仍然支持超时,请尝试以下操作:

    public static Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
    {
        ManualResetEvent processWaitObject = new ManualResetEvent(false);
        processWaitObject.SafeWaitHandle = new SafeWaitHandle(process.Handle, false);

        TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

        RegisteredWaitHandle registeredProcessWaitHandle = null;
        registeredProcessWaitHandle = ThreadPool.RegisterWaitForSingleObject(
            processWaitObject,
            delegate(object state, bool timedOut)
            {
                if (!timedOut)
                {
                    registeredProcessWaitHandle.Unregister(null);
                }

                processWaitObject.Dispose();
                tcs.SetResult(!timedOut);
            },
            null /* state */,
            timeout,
            true /* executeOnlyOnce */);

        return tcs.Task;
    }

与已接受的答案相比,这种方法的优势在于不会阻塞任何线程,从而减少应用程序的开销。


4
.NET 5引入了新的API Process.WaitForExitAsync,它允许异步等待进程完成。 它具有与现有Process.WaitForExit相同的功能,唯一的区别是等待是异步的,因此不会阻塞调用线程。

使用示例:

private async void button1_Click(object sender, EventArgs e)
{
    string filePath = Path.Combine
    (
        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
        Guid.NewGuid().ToString() + ".txt"
    );
    File.WriteAllText(filePath, "Hello World!");
    try
    {
        using Process process = new();
        process.StartInfo.FileName = "Notepad.exe";
        process.StartInfo.Arguments = filePath;
        process.Start();
        await process.WaitForExitAsync();
    }
    finally
    {
        File.Delete(filePath);
    }
    MessageBox.Show("Done!");
}

在上面的例子中,当用户与打开的文件交互时,UI保持响应。如果使用了WaitForExit,则会阻塞UI线程。

1
如果我需要在较低版本的.NET上使用它,我可能会将源代码复制到扩展方法中。https://source.dot.net/#System.Diagnostics.Process/System/Diagnostics/Process.cs,b6a5b00714a61f06 - Bogdan Gavril MSFT
如果仅涉及启动一个进程,我可能更喜欢使用不太正规但安全的 await Task.Run(() => process.WaitForExit()); - Theodor Zoulias
@TheodorZoulias 不行。这与对WaitForExit()的阻塞调用相比没有任何优势,而且还增加了await的额外开销。这是不安全的。不要这样做。 - Marc L.
@MarcL 我不确定你在比较什么。你是说异步的 Process.WaitForExitAsync 和阻塞的 Process.WaitForExit 没有任何优势,并且也不安全吗? - Theodor Zoulias
我是说await Task.Run(<同步方法>)不是一个好的选择,因为它会将工作排队到线程池中。更多详情请参考Stephen Cleary - Marc L.
显示剩余4条评论

3
尝试以下代码。
public void KickOffProcess(string filePath) {
  var proc = Process.Start(filePath);
  ThreadPool.QueueUserWorkItem(new WaitCallBack(WaitForProc), proc);
}

private void WaitForProc(object obj) {
  var proc = (Process)obj;
  proc.WaitForExit();
  // Do the file deletion here
}

1
不确定它是否处理了“或者在一定时间后放弃线程”的情况。 - Marc Gravell

0

我可能不会使用单独的进程来打开文件。相反,如果我认为该操作将花费很长时间并可能阻塞UI线程,我可能会利用后台线程。

private delegate void FileOpenDelegate(string filename);

public void OpenFile(string filename)
{
   FileOpenDelegate fileOpenDelegate = OpenFileAsync;
   AsyncCallback callback = AsyncCompleteMethod;
   fileOpenDelegate.BeginInvoke(filename, callback, state);
}

private void OpenFileAsync(string filename)
{
   // file opening code here, and then do whatever with the file
}

当然,这不是一个好的工作示例(它不返回任何内容),我也没有展示UI如何更新(你必须在UI级别使用BeginInvoke,因为后台线程不能更新UI线程)。但这种方法通常是我在.Net中处理异步操作的方式。

抱歉,实际上我的意思是该进程应该处理文件,而不是打开它。我重新写了问题。 - Bogdan Gavril MSFT

0
您可以在 Process 类中使用 Exited 事件。
ProcessStartInfo info = new ProcessStartInfo();

info.FileName = "notepad.exe";
Process process = Process.Start(info);

process.Exited += new EventHandler(process_Exited);
Console.Read();

如果发生这种情况,您可以处理您提到的操作


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