2

我有一个用 C#(源自PSCmdlet)编写的 powershell cmdlet,它将启动一个长时间运行的任务,该任务应该在WriteProgress()运行时更新其进度。由于powershell不允许使用单独的线程,WriteObject或者WriteProgress我必须在主线程中创建一个Queue<object>,并且我将要写入管道/进度的任务中的项目添加到队列中。当对象进入并写入管道/进度条时,while 循环将使对象出列。

这是有效的,但我想看看是否有任何更好的多线程实践,使用 C#/VB 编写的 powershell cmdlet。例如,UIComponent.Dispatcher.Invoke()如果我需要更新进度条或 UI 组件,我总是可以使用 WPF 进入 UI 线程。是否有任何等效的东西可以用来“踏上”powershell 线程以更新 UI 或写入管道?

4

3 回答 3

3

这是一个封装在一个类中的队列系统示例,因此它更易于使用并模仿 Cmdllet.WriteObject 的行为。这样,您可以从单独的线程中调用 WriteObject,并且该对象将被编组到 powershell 线程并写入管道。

[Cmdlet("Test", "Adapter")]
public class TestCmdlet : PSCmdlet
{
    protected override void ProcessRecord()
    {
        PowerShellAdapter adapter = new PowerShellAdapter(this, 100);
        Task.Factory.StartNew(() => {
            for (int x = 0; x < 100; x++) {
                adapter.WriteObject(x);
                Thread.Sleep(100);
            }
            adapter.Finished = true;
        });
        adapter.Listen();
    }
}   

public class PowerShellAdapter
{
    private Cmdlet Cmdlet { get; set; }
    private Queue<object> Queue { get; set; }
    private object LockToken { get; set; }
    public bool Finished { get; set; }
    public int Total { get; set; }
    public int Count { get; set; }

    public PowerShellAdapter(Cmdlet cmdlet, int total)
    {
        this.Cmdlet = cmdlet;
        this.LockToken = new object();
        this.Queue = new Queue<object>();
        this.Finished = false;
        this.Total = total;
    }

    public void Listen()
    {
        ProgressRecord progress = new ProgressRecord(1, "Counting to 100", " ");
        while (!Finished || Queue.Count > 0)
        {
            while (Queue.Count > 0)
            {
                progress.PercentComplete = ++Count*100 / Total;
                progress.StatusDescription = Count + "/" + Total;
                Cmdlet.WriteObject(Queue.Dequeue());
                Cmdlet.WriteProgress(progress);
            }

            Thread.Sleep(100);
        }
    }

    public void WriteObject(object obj)
    {
        lock (LockToken)
            Queue.Enqueue(obj);
    }
}
于 2012-10-14T18:04:55.547 回答
2

Despertar 提供的答案将起作用,但可以稍微改进。

使用 Thread.Sleep 在循环中轮询应替换为使用 AutoResetEvent。这将导致主线程仅在有实际可用数据时“唤醒”,并且可以让 cmdlet 以超过 100 毫秒的速度完成。Thread.Sleep 总是会导致 cmdlet 至少花费 100 毫秒,即使它可以运行得更快。如果您有一个简单的 cmdlet,这可能不是问题,但如果您将其插入到复杂的管道中,这 100 毫秒很容易成倍增加并导致运行速度非常慢。此外,在 Listen 方法内访问主线程上的队列时,应采取锁定。

故事的寓意:如果你做跨线程同步 Thread.Sleep 不是正确的工具。

using System.Threading;
public class PowerShellAdapter
{
    private Cmdlet Cmdlet { get; set; }
    private Queue<object> Queue { get; set; }
    AutoResetEvent sync;
    private object LockToken { get; set; }
    // volatile, since it will be written/read from different threads.
    volatile bool finished;
    public bool Finished
    {
        get { return finished; }
        set
        {
            this.finished = value;
            // allow the main thread to exit the outer loop.
            sync.Set();
        }
    }
    public int Total { get; set; }
    public int Count { get; set; }

    public PowerShellAdapter(Cmdlet cmdlet, int total)
    {
        this.Cmdlet = cmdlet;
        this.LockToken = new object();
        this.Queue = new Queue<object>();
        this.finished = false;
        this.Total = total;
        this.sync = new AutoResetEvent(false);
    }

    public void Listen()
    {
        ProgressRecord progress = new ProgressRecord(1, "Counting to 100", " ");
        while (!Finished)
        {
            while (true) { // loop until we drain the queue
                object item;
                lock (LockToken) {
                    if (Queue.Count == 0)
                        break; // exit while
                    item = Queue.Dequeue();
                }

                progress.PercentComplete = ++Count * 100 / Total;
                progress.StatusDescription = Count + "/" + Total;
                Cmdlet.WriteObject(item);
                Cmdlet.WriteProgress(progress);
            }
            sync.WaitOne();// wait for more data to become available
        }
    }

    public void WriteObject(object obj)
    {
        lock (LockToken)
        {
            Queue.Enqueue(obj);
        }
        sync.Set(); // alert that data is available
    }
}

请注意,我还没有实际测试过这段代码,但它说明了这个想法。

于 2017-07-14T20:27:08.783 回答
1

您可以查看 Start-Job cmdlet 以及 Get-Job、Wait-Job 和 Receive-Job。

Start-Job 将有效地启动一个新线程并输出一个 JobId,您可以使用 Receive-Job 查询以获取输出。然后,您可以遍历所有当前正在运行的作业并更新您的进度条。

看看http://blogs.technet.com/b/heyscriptingguy/archive/2012/08/10/use-background-jobs-to-run-a-powershell-server-uptime-report.aspx

于 2012-10-13T23:09:35.583 回答