考虑以下代码,它打开一个文件以在作业中写入,停止该作业,然后再次打开同一文件以进行写入:
$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 }
)
``
TL;博士
停止 PowerShell 时以下行是原子的吗?
$fs = [System.IO.StreamWriter]::new($filePath)
是的。
停止 PowerShell 时,.Net 方法的分配是否具有原子性?
这至少取决于以下几点:
=
与Set-Variable
)=
右侧的表达式是如何组成的这些答案基于以下实证结果。 我还没有找到这些问题的分析答案,但我认为该测试充分确定了将 .Net 方法生成的值分配给 PowerShell 变量的原子性。
问题归结为
$fs = [System.IO.StreamWriter]::new($filePath)
...
...如果作业在
已经开始时停止,但对
new()
的分配尚未完成,会发生什么情况?作业完成了吗?
$fs
为了测试在这种情况下会发生什么,我们需要设计以下内容:
通过这样的设置,只需检查
clean{}
块中的分配是否完成即可。
下面的代码实现了此设置。
运行测试代码会在下面的“数据”部分中产生输出。 有一些结果令我惊讶。 结果非常微妙,以至于我不愿意在没有像下面这样的原子性单元测试来确认的情况下依赖该行为。
所测试的涉及 PowerShell 命令的赋值语句都不是原子的。 也就是说,对于以下所有内容,构造函数完成并输入
clean{}
,并将变量 $pl
分配给:
Set-Variable pl ([PreemptionLogger]::new($log,$constructorEntered,$allowCompletion))
$pl = New-Object PreemptionLogger
$pl = New-PreemptionLoggerCmdlet
:这是一个二进制命令,它构造一个对象并使用 WriteObject()
方法输出它。$pl = New-PreemptionLoggerFunction
:这是一个PowerShell函数,它构造一个对象并将其输出到管道。=
变量赋值都是原子的所有测试的将 .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
[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
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));
}
}