关于多线程:在Powershell中等待C#事件时阻塞会导致死锁

Blocking While Waiting For C# Event In Powershell Causes Deadlock

在PowerShell中,我试图启动一个进程,然后等待直到该进程退出或脚本从命名管道中获取信号。虽然命名管道组件可以正常工作,但是当脚本被阻止时,代码无法处理事件。这只是事件相关代码的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ErrorActionPreference ="Stop"

# launch process with the appropriate args.
$p = [System.Diagnostics.Process]::new()
$p.StartInfo.FileName ="notepad.exe"
$p.StartInfo.Arguments = $null
$p.EnableRaisingEvents = $true $p.Start()

# create a task completion source
$tcs = [System.Threading.Tasks.TaskCompletionSource[Boolean]]::new()

Register-ObjectEvent -InputObject $p -EventName Exited `
    -MessageData $tcs `
    -Action {
        $Event.MessageData.SetResult($true)
    }

# wait for the program to exit
$processTask = $tcs.Task

# this deadlocks
$processTask.Wait()

是否存在通过PowerShell的作业系统解决该问题的正确方法,还是通过手动旋转另一个等待该过程退出的C#任务来解决该问题,从而使脚本能够在发生任何一个事件后恢复运行?

作为参考,这是更完整的脚本(包括对流程事件或命名管道的等待):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
$ErrorActionPreference ="Stop"

#inbound pipe
$pipeListener = [System.IO.Pipes.NamedPipeServerStream]::new("testPipe", 1)

# launch process with the appropriate args.
$p = [System.Diagnostics.Process]::new()
$p.StartInfo.FileName ="notepad.exe"
$p.StartInfo.Arguments = $null
$p.EnableRaisingEvents = $true
$p.Start()

# I would normally solve this using a TaskCompletionSource
# in C#, and have it fire off the process's exited event.

# create a task completion source
$tcs = [System.Threading.Tasks.TaskCompletionSource[Boolean]]::new()

Register-ObjectEvent -InputObject $p -EventName Exited `
    -MessageData $tcs `
    -Action {
        $Event.MessageData.SetResult($true)
    }

# wait for the program to exit, or for pipe signal
try {
    $pipeTask = $pipeListener.WaitForConnectionAsync()
    $processTask = $tcs.Task

    while ($true)
    {
        echo"Launched Process, waiting for sequence to complete..."

        # this works, but not instantly - the timeout appears to allow the
        # event to process.
        #$res = [System.Threading.Tasks.Task]::WaitAny(@($processTask, $pipeTask), 10000)

        # this does not.
        $res = [System.Threading.Tasks.Task]::WaitAny(@($processTask, $pipeTask))


        echo"Got wait result: $res"

        if ($pipeTask.IsCompleted) {
            echo"Got pipe connection"
            $sr = [System.IO.StreamReader]::new($pipeListener)
            $msg = $sr.ReadToEnd()

            echo"Pipe sent: $msg"
            break
        }

        if ($processTask.IsCompleted) {
            echo"Process completed."
            break
        }
    }
}
finally {
    $pipeListener.Dispose()
}


经过更多研究,这似乎不可能-PowerShell仅在主线程中处理C#事件,即使事件的源是后台线程也是如此。

此外,PowerShell作业系统为每个作业使用进程而不是线程,因此您不能从后台作业设置"任务完成源"。

但是,从PowerShell调用的C#代码可以正常工作,并且可以生成线程。因此,为解决此问题,我编写了一个C#函数,该函数在进程终止时将消息发送到命名管道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public static void NotifyPipeOnProcessExit(
        Process processToMonitor,
        String pipeName
    )
    {
        // Create a pipe client.
        var client = new NamedPipeClientStream(".", pipeName, PipeDirection.Out);

        // create a process task
        var processTask = Task.Run(() => {
            processToMonitor.WaitForExit();

            using (client)
            {
                client.Connect(0);
                var sw = new StreamWriter(client);
                sw.Write("Process exitted.");
                sw.Close();
            }

        });

        return;
    }

然后我在PowerShell中简单地调用了此函数以将过程对象附加到管道:

1
2
3
[ThreadTool.ThreadHelper]::NotifyPipeOnProcessExit($p, $pipeName)

$pipeListener.WaitForConnection()

这也简化了代码,因为PowerShell现在只需要监视一个事件。

也就是说,C#代码可以像设置命名管道一样轻松地设置TaskCompletionSource而不是发送消息。