从单独的线程更新cmdlet进度的最佳方法

时间:2012-10-12 04:56:49

标签: c# .net multithreading powershell cmdlet

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

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

3 个答案:

答案 0 :(得分: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);
    }
}

答案 1 :(得分: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

答案 2 :(得分:1)

Despertar提供的答案将有效,但可以略微改进。

使用ThreadReleEvent替换Thread.Sleep循环中的轮询。这将导致主线程仅唤醒&#34;当实际存在可用数据时,可以允许cmdlet以超过100ms的速度完成。 Thread.Sleep将始终使cmdlet至少需要100毫秒,即使它可以运行必须更快。如果你有一个简单的cmdlet,这可能不是问题,但是如果你将它插入到一个复杂的管道中,这个100ms就可以很容易地繁殖并导致事情慢慢地运行非常。此外,在Listen方法内的主线程上访问Queue时应该进行锁定。

故事的寓意:如果你做跨线程同步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
    }
}

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