【问题标题】:Await async C# method from PowerShell等待来自 PowerShell 的异步 C# 方法
【发布时间】:2021-01-14 09:33:27
【问题描述】:

我想通过使用静态成员访问器从 PowerShell 调用静态异步 C# 方法,例如:

PowerShell

function CallMyStaticMethod([parameter(Mandatory=$true)][string]$myParam)
{
    ...
    [MyNamespace.MyClass]::MyStaticMethod($myParam)
    ...
}

C#

public static async Task MyStaticMethod(string myParam)
{
    ...
    await ...
    ...
}

由于我的 C# 方法是异步的,因此我的 C# 方法是否会在没有来自 PowerShell 的某种“等待”调用的情况下正常运行?

【问题讨论】:

    标签: c# powershell async-await


    【解决方案1】:

    它自己会运行良好,但如果你想等待它完成,你可以使用它

    $null = [MyNamespace.MyClass]::MyStaticMethod($myParam).GetAwaiter().GetResult()
    

    这将解开 AggregateException 的包装,如果您改用 $task.Result 之类的东西,则会抛出该 AggregateException

    但是,这将阻塞直到完成,这将阻止 CTRL + C 正确停止管道。您可以等待它完成,同时仍然遵守这样的管道停止

     $task = [MyNamespace.MyClass]::MyStaticMethod($myParam)
     while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
     $null = $task.GetAwaiter().GetResult()
    

    如果异步方法确实返回了一些东西,删除$null =

    【讨论】:

    • 我很好奇,这段代码如何尊重CTRL+C? .Net 任务仍在 PowerShell 进程中执行。只需向while 循环发出CTRL+C 只会打破该PowerShell 循环。
    • @ChrisLynch 它不只是跳出循环,它会为当前的交互式管道发出管道停止。在这种情况下,这意味着它也会跳过下一行。将上面的示例粘贴到提示符中,将示例静态方法切换为[Threading.Tasks.Task]::Delay(5000),您就会明白我的意思了。
    • 是的,我已经看到了这种行为。我的问题是试图解决另一个问题:这里有两个管道,PowerShell 和 .Net。执行 CTRL+C 只会影响 PowerShell。要停止 .Net 任务/事件,您应该显示 Try/Finally 块。最后将在使用 CTRL+C 时执行,然后您可以为 .Net 异步任务/事件调用 .Stop() 或 .Halt() 方法(如果支持)。
    • 不幸的是,没有通用的方法来停止任务正在执行的实际工作。有些方法支持取消标记,有些方法需要在另一个线程上调用不同的方法,还有很多根本不支持取消。 FWIW,通过假装取消来实现StopProcessing 的命令并不罕见,同时仍继续在另一个线程中工作,即使它最终会被丢弃。虽然不是很好,但它仍然比完全忽略停止处理请求更好 (imo)。
    【解决方案2】:

    借用 Patrick Meinecke 的回答,可以创建一个可以为您解决任务(或任务列表)的流水线功能:

    function Await-Task {
        param (
            [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
            $task
        )
    
        process {
            while (-not $task.AsyncWaitHandle.WaitOne(200)) { }
            $task.GetAwaiter().GetResult()
        }
    }
    

    用法:

    $results = Get-SomeTasks $paramA $paramB | Await-Task
    

    【讨论】:

    • 以上内容对我不起作用,但似乎我能够使用类似的方式进行管道化(换行很重要,这将丢失它们):function Await-Task { process { $task = $ _ while (-not $task.AsyncWaitHandle.WaitOne(200)) { } $task.GetAwaiter().GetResult() } }
    【解决方案3】:

    我最近遇到了这个问题,发现创建 PowerShell 作业似乎也能很好地解决问题。这为您提供了标准作业功能(Wait-Job、Receive-Job 和 Remove-Job)。 工作可能令人生畏,但这很简单。它是用 C# 编写的,因此您可能需要使用 Add-Type 添加它(需要对其编写方式进行一些调整,当我使用 lambda 时,Add-Type -TypeDefintition '...' 似乎失败了,所以它们需要替换为正确的 Get 访问器)或编译它。

    using System;
    using System.Management.Automation;
    using System.Threading;
    using System.Threading.Tasks;
    namespace MyNamespace
    {
        public class TaskJob : Job
        {
            private readonly Task _task;
            private readonly CancellationTokenSource? _cts;
            public override bool HasMoreData => Error.Count > 0 || Output.Count > 0;
            public sealed override string Location => Environment.MachineName;
            public override string StatusMessage => _task.Status.ToString();
            public override void StopJob()
            {
                // to prevent the job from hanging, we'll say the job is stopped
                // if we can't stop it. Otherwise, we'll cancel _cts and let the
                // .ContinueWith() invocation set the job's state.
                if (_cts is null)
                {
                    SetJobState(JobState.Stopped);
                }
                else
                {
                    _cts.Cancel();
                }
            }
            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    _task.Dispose();
                    _cts?.Dispose();
                }
                base.Dispose(disposing);
            }
            public TaskJob(string? name, string? command, Task task, CancellationTokenSource? cancellationTokenSource)
                : base(command, name)
            {
                PSJobTypeName = nameof(TaskJob);
                if (task is null)
                {
                    throw new ArgumentNullException(nameof(task));
                }
                _task = task;
                task.ContinueWith(OnTaskCompleted);
                _cts = cancellationTokenSource;
            }
            public virtual void OnTaskCompleted(Task task)
            {
                if (task.IsCanceled)
                {
                    SetJobState(JobState.Stopped);
                }
                else if (task.Exception != null)
                {
                    Error.Add(new ErrorRecord(
                        task.Exception,
                        "TaskException",
                        ErrorCategory.NotSpecified,
                        task)
                    {
                        ErrorDetails = new ErrorDetails($"An exception occurred in the task. {task.Exception}"),
                    }
                        );
                    SetJobState(JobState.Failed);
                }
                else
                {
                    SetJobState(JobState.Completed);
                }
            }
        }
        public class TaskJob<T> : TaskJob
        {
            public TaskJob(string? name, string? command, Task<T> task, CancellationTokenSource? cancellationTokenSource)
                : base(name, command, task, cancellationTokenSource)
            {
            }
            public override void OnTaskCompleted(Task task)
            {
                if (task is Task<T> taskT)
                {
                    try
                    {
                        Output.Add(PSObject.AsPSObject(taskT.GetAwaiter().GetResult()));
                    }
                    // error handling dealt with in base.OnTaskCompleted
                    catch { }
                }
                base.OnTaskCompleted(task);
            }
        }
    }
    
    

    将此类添加到您的 PowerShell 会话后,您可以非常轻松地将任务转换为任务:

    $task = [MyNamespace.MyClass]::MyStaticMethod($myParam)
    $job = ([MyNamespace.TaskJob]::new('MyTaskJob', $MyInvocation.Line, $task, $null))
    # Add the job to the repository so that it can be retrieved later. This requires that you're using an advanced script or function (has an attribute declaration, particularly [CmldetBinding()] before the param() block). If not, you can always make a Register-Job function to just take an unregistered job and add it to the job repository.
    $PSCmdlet.JobRepository.Add($job)
    # now you can do all this with your task
    Get-Job 'MyTaskJob' | Wait-Job
    Get-Job 'MyTaskJob' | Receive-Job
    Get-Job 'MyTaskJob' | Remove-Job
    

    我会指出我对任务并不是非常熟悉,所以如果有人看到那里看起来很糟糕的东西,请告诉我,我一直在寻找改进的方法。 :)

    可以在this TaskJob gist 中找到更完善的概念。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-10-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多