停止 PowerShell 时,.Net 方法的分配是原子的吗?

问题描述 投票:0回答:1

考虑以下代码,它打开一个文件以在作业中写入,停止该作业,然后再次打开同一文件以进行写入:

$filePath = [system.io.path]::GetTempFileName()
$job =
    Start-ThreadJob             `
        -ArgumentList $filePath `
        -ScriptBlock {
            param($filePath)
            end {
                $fs = [System.IO.StreamWriter]::new($filePath)
            }
            clean {
                ${fs}?.Dispose()
            }
        }

$job | Stop-Job
$fs = [System.IO.StreamWriter]::new($filePath)

clean{}
块似乎在
Stop-Job
完成之前运行完成,这意味着
$fs
如果被分配就会被释放。

但是,如果作业在

new()
已经开始时停止,但对
$fs
的分配尚未完成,会发生什么情况? 作业完成了吗? 停止 PowerShell 时以下行是原子的吗?

$fs = [System.IO.StreamWriter]::new($filePath)

背景

非原子赋值表达式

构造一个发生构造但赋值未完成的赋值语句是相当简单的。 (看起来有点荒谬)的说法

$fs =
    @([System.IO.StreamWriter]::new($filePath)
      Start-Sleep 1)

就是这样一个例子。 在这种情况下,可以停止作业,而无需处理第一个

StreamWriter
,然后第二个
new()
失败并出现错误

Exception calling ".ctor" with "1" argument(s): "The process cannot access the file 'C:\Users\User\AppData\Local\Temp\tmpzc3tf0.tmp' because it is being used by another process."

但是这种引人注目的

Start-Sleep
可以被任何形式的合法功利性工作所取代。 另一种似乎容易受到此问题影响的情况是构造函数被包装在像
New-StreamWriter
这样的高级函数中。 高级函数的引入是否会阻止它调用的 .Net 方法的原子赋值?

测试结果

我准备了一个测试,用于测试不同的表达式,并在检测到分配不是原子时报告。 但该测试可以在幸运的时机通过,即使所测试的分配实际上不是原子的。 所以并不能证明没有问题,只能证明有问题。

测试输出如下:

result assignment                                         use_again_error
------ ----------                                         ---------------
-      $fs = [System.IO.StreamWriter]::new($filePath)
        $c.Set()
-      $fs = New-Object System.IO.StreamWriter $filePath
        $c.Set()
❌      $fs =                                              Exception calling ".ctor" with "1" argument(s): "The
           @([System.IO.StreamWriter]::new($filePath)     process cannot access the file
             $c.Set()                                     'C:\Users\User1\AppData\Local\Temp\tmpgm0d0q.tmp' because
             Start-Sleep 1)                               it is being used by another process."
-      $fs = New-StreamWriter $filePath

测试代码

{$fs = [System.IO.StreamWriter]::new($filePath)
 $c.Set()                                         } ,
{$fs = New-Object System.IO.StreamWriter $filePath
 $c.Set()                                         } ,
{$fs =
    @([System.IO.StreamWriter]::new($filePath)
      $c.Set()
      Start-Sleep 1)                              } ,
{$fs = New-StreamWriter $filePath} |
ForEach-Object {
    $assignment = $_
    $filePath = [system.io.path]::GetTempFileName()
    $c = [System.Threading.ManualResetEventSlim]::new() # indicates constructor has completed
    $job =
        Start-ThreadJob `
            -ArgumentList $filePath,$assignment,$c `
            -ScriptBlock {
                param($filePath,$assignment,$c)
                begin {
                    function New-StreamWriter {
                        param($filePath)
                        [System.IO.StreamWriter]::new($filePath)
                        $c.Set()
                    }
                }
                end {
                    . ($assignment.Ast.GetScriptBlock())
                }
                clean {
                    ${fs}?.Dispose()
                }
            }
    $c.Wait() | Out-Null
    $assignment_error =
        @(
            $job.Error
            try {
                $job | Receive-Job -ea Stop
            }
            catch {
                $_
            }
        ) |
            Select-Object -Unique

    $use_again_error  = $(try { $fs2 = [System.IO.StreamWriter]::new($filePath)} catch {$_})
    [pscustomobject]@{
        result           = $(if($assignment_error) {'⚠️'} else {if($use_again_error) {'❌'} else {''}})
        assignment       = $assignment
        assignment_error = $assignment_error
        use_again_error  = $use_again_error
    }
    ${fs2}?.Dispose()
} |
Format-Table `
    -Wrap `
    -Property @(
        @{ Name = 'result';Expression = 'result'; Width = 'result'.Length }
        @{ Name = 'assignment';Expression = 'assignment'; Width = 50 }
        @{ Name = 'use_again_error';Expression = 'use_again_error'; Width = 60 }
    )
``
powershell variable-assignment atomic race-condition dispose
1个回答
0
投票

TL;博士

停止 PowerShell 时以下行是原子的吗?

$fs = [System.IO.StreamWriter]::new($filePath)

是的。

停止 PowerShell 时,.Net 方法的分配是否具有原子性?

这至少取决于以下几点:

  • 如何进行分配(即
    =
    Set-Variable
  • =
    右侧的表达式是如何组成的

这些答案基于以下实证结果。 我还没有找到这些问题的分析答案,但我认为该测试充分确定了将 .Net 方法生成的值分配给 PowerShell 变量的原子性。


实证检验

问题归结为

$fs = [System.IO.StreamWriter]::new($filePath)

...

...如果作业在

new()
已经开始时停止,但对
$fs
的分配尚未完成,会发生什么情况?作业完成了吗?

为了测试在这种情况下会发生什么,我们需要设计以下内容:

  • PowerShell 作业一直进行到构造函数调用中途,然后等待作业停止后再继续
  • 用于创建该作业的脚本,并在该作业进入该构造函数后停止该作业

通过这样的设置,只需检查

clean{}
块中的分配是否完成即可。

下面的代码实现了此设置。

结果

运行测试代码会在下面的“数据”部分中产生输出。 有一些结果令我惊讶。 结果非常微妙,以至于我不愿意在没有像下面这样的原子性单元测试来确认的情况下依赖该行为。

涉及 PowerShell 命令的分配都不是原子的

所测试的涉及 PowerShell 命令的赋值语句都不是原子的。 也就是说,对于以下所有内容,构造函数完成并输入

clean{}
,并将变量
$pl
分配给:

  • Set-Variable pl ([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
  • $pl = New-Object PreemptionLogger
  • $pl = New-PreemptionLoggerCmdlet
    :这是一个二进制命令,它构造一个对象并使用
    WriteObject()
    方法输出它。
  • $pl = New-PreemptionLoggerFunction
    :这是一个PowerShell函数,它构造一个对象并将其输出到管道。

直接从 .Net 方法进行的所有
=
变量赋值都是原子的

所有测试的将 .Net 方法的返回值直接分配给变量的赋值语句都是原子的。 即使使用

()
@()
$()
也是如此:

  • $pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)
  • $pl = ([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
  • $pl = @([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
  • $pl = $([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))

涉及
()
的“Clang”风格赋值是原子的,但
$()
@()
不是

以下对

$pl
的赋值是原子的:

  • $pl = ($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))

但是,以下对

$pl
的赋值是不是原子的:

  • $pl = $($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
  • $pl = @($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))

来自
.
点源或
&
调用运算符脚本块输出的赋值都是原子的

来自脚本块输出的以下分配都不是原子的:

  • $pl = . {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}
  • $pl = & {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}

数据

assigned scriptblock
-------- -----------
✅        $pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)
✅        $pl = @([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
✅        $pl = ([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
✅        $pl = $([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
✅        $pl = ($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
❌        $pl = $($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
❌        $pl = @($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
✅        $x = ($pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
✅        $x = $($pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
✅        $x = @($pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
❌        $pl = . {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}
❌        $pl = & {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}
❌        $pl = % {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}
❌        $pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion),
                    [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)
❌        Set-Variable pl ([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
❌        $pl = New-Object PreemptionLogger $log,$constructorEntered,$allowCompletion$pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)
✅        $pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)
✅        $pl = @([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
✅        $pl = ([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
✅        $pl = $([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
✅        $pl = ($x = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
❌        $pl = $($x = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
❌        $pl = @($x = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
✅        $x = ($pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
✅        $x = $($pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
✅        $x = @($pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
❌        $pl = . {[PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}
❌        $pl = & {[PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}
❌        $pl = % {[PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}
❌        $pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion),
                    [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)
❌        Set-Variable pl ([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))
❌        $pl =
                 New-PreemptionLoggerCmdlet               `
                     -Log             $log                `
                     -Entered         $constructorEntered `
                     -AllowCompletion $allowCompletion
❌        $pl =
                 New-PreemptionLoggerFunction             `
                     -Log             $log                `
                     -Entered         $constructorEntered `
                     -AllowCompletion $allowCompletion


Name                           Value
----                           -----
PSVersion                      7.4.6
PSEdition                      Core
GitCommitId                    7.4.6
OS                             Microsoft Windows 10.0.22631
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

测试代码

测试.ps1

[CmdletBinding()]
param()

@(
    {$pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}
    {$pl = @([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$pl = ([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$pl = $([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$pl = ($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$pl = $($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$pl = @($x = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$x = ($pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$x = $($pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$x = @($pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))}
    {$pl = . {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}}
    {$pl = & {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}}
    {$pl = % {[PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)}}
    {$pl = [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion),
           [PreemptionLogger]::new($log,$constructorEntered,$allowCompletion) }
    {Set-Variable pl ([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion)) }
    {$pl = New-Object PreemptionLogger $log,$constructorEntered,$allowCompletion }

    {$pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}
    {$pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}
    {$pl = @([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$pl = ([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$pl = $([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$pl = ($x = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$pl = $($x = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$pl = @($x = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$x = ($pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$x = $($pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$x = @($pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion))}
    {$pl = . {[PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}}
    {$pl = & {[PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}}
    {$pl = % {[PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)}}
    {$pl = [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion),
           [PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion) }
    {Set-Variable pl ([PreemptionLogger]::CreatePreemptionLogger($log,$constructorEntered,$allowCompletion)) }

    {$pl =
        New-PreemptionLoggerCmdlet               `
            -Log             $log                `
            -Entered         $constructorEntered `
            -AllowCompletion $allowCompletion     }
    {$pl =
        New-PreemptionLoggerFunction             `
            -Log             $log                `
            -Entered         $constructorEntered `
            -AllowCompletion $allowCompletion     }
) |
ForEach-Object {
    $scriptblock = $_
    $log                = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
    $constructorEntered = [System.Threading.ManualResetEventSlim]::new()
    $job =
        Start-ThreadJob                                         `
            -ArgumentList $log,$constructorEntered,$scriptblock `
            -ScriptBlock {
                param($log,$constructorEntered,$scriptblock)
                begin {
                    Add-Type                     `
                            -Path      .\test.cs `
                            -PassThru            |
                        % Assembly               |
                        Import-Module -wa si
                    function New-PreemptionLoggerFunction {
                        param(
                            [System.Collections.Concurrent.ConcurrentQueue[string]]$Log,
                            [System.Threading.ManualResetEventSlim]$Entered,
                            [System.Threading.ManualResetEventSlim]$AllowCompletion
                        )
                        [PreemptionLogger]::new($Log,$Entered,$AllowCompletion)
                    }
                }
                end {
                    Using-PipelineStopEvent |
                    . { process {
                        $allowCompletion = $_
                        . ($scriptblock.Ast.GetScriptBlock())
                    }}
                    Out-Null
                    $log.Enqueue('end{}')
                }
                clean {
                    $log.Enqueue('clean{}')
                    $log.Enqueue("pl: $pl")
                }
            }

    $constructorEntered.Wait()
    $log.Enqueue('StopJobAsync()')
    $job.StopJobAsync() | Out-Null
    $job | Wait-Job | Out-Null
    $log.Enqueue('Wait-Job complete')
    $log | Write-Verbose
    $job | Receive-Job -Wait
    $job| Remove-Job

    [pscustomobject]@{
        scriptblock = $scriptblock
        assigned    = ($assigned = $log -contains 'pl: PreemptionLogger')
        stopped     = ($stopped = $log -notcontains 'end{}')
        result      = $(if(-not $stopped) {'⚠️'} else {if(-not $assigned) {'❌'} else {'✅'}})
    }
} |
Format-Table `
    -Wrap `
    -Property @(
        @{ Name = 'assigned'   ;Expression = 'result'    ; Width = 'assigned'.Length }
        @{ Name = 'scriptblock';Expression = 'scriptblock' ; Width = 120 }
    )

$PSVersionTable

测试.cs

using System.Collections.Concurrent;
using System.Threading;
using System.Management.Automation;


[Cmdlet("Using","PipelineStopEvent")]
public class UsingPipelineStopEventCommand : Cmdlet {
    ManualResetEventSlim _event;
    protected override void EndProcessing() {
        _event = new ManualResetEventSlim();
        WriteObject(_event);
    }
    protected override void StopProcessing() {
        _event.Set();
    }
}
public class PreemptionLogger {
    public PreemptionLogger(
        ConcurrentQueue<string> log,
        ManualResetEventSlim    entered,
        ManualResetEventSlim    allowCompletion
    ) {
        log.Enqueue("constructor entered");
        entered.Set();
        if (allowCompletion.IsSet) {
            log.Enqueue("allowCompletion already set");
            return;
        }
        log.Enqueue("awaiting allowCompletion");
        allowCompletion.Wait();
        log.Enqueue("allowCompletion.Wait() complete");
    }
    public static PreemptionLogger CreatePreemptionLogger(
        ConcurrentQueue<string> log,
        ManualResetEventSlim    entered,
        ManualResetEventSlim    allowCompletion
    ) {
        return new PreemptionLogger(log,entered,allowCompletion);
    }
}

[Cmdlet(VerbsCommon.New,"PreemptionLoggerCmdlet")]
public class NewPreemptionLoggerCommand : Cmdlet {
    [Parameter(Mandatory=true)]
    public ConcurrentQueue<string> Log {get; set;}
    [Parameter(Mandatory=true)]
    public ManualResetEventSlim    Entered {get; set;}
    [Parameter(Mandatory=true)]
    public ManualResetEventSlim    AllowCompletion {get; set;}
    protected override void EndProcessing() {
        WriteObject(new PreemptionLogger(Log,Entered,AllowCompletion));
    }
}
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.