Лучший способ обновить ход выполнения командлета из отдельного потока

У меня есть командлет PowerShell , написанный на C # (производный от PSCmdlet), который запускает длительную задачу, которая должна обновлять свой прогресс, используя WriteProgress() во время выполнения. Поскольку PowerShell не позволяет отдельному потоку использовать WriteObject или WriteProgress, мне пришлось создать Queue<object> в основном потоке, и я добавляю элементы в очередь из задачи, которую я хочу записать в конвейер / прогресс. Цикл while удаляет объекты из очереди по мере их поступления и записывает в конвейерную линию / индикатор выполнения.

Это работает, но я хотел посмотреть, есть ли какие-нибудь лучшие практики для многопоточности с помощью командлета PowerShell, написанного на C # / VB. Например, с WPF я всегда могу перейти в поток пользовательского интерфейса с помощью UIComponent.Dispatcher.Invoke(), если мне нужно обновить индикатор выполнения или компонент пользовательского интерфейса. Есть ли что-нибудь эквивалентное, что я могу использовать для «перехода» в поток PowerShell для обновления пользовательского интерфейса или записи в конвейер?


person Despertar    schedule 12.10.2012    source источник
comment
Если какие-либо будущие интернет-путешественники будут заинтересованы, я создал пакет nuget для решения этой самой проблемы: github.com/refactorsaurusrex/AsyncProgressReporter   -  person Nick Spreitzer    schedule 21.07.2020


Ответы (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);
    }
}
person Despertar    schedule 14.10.2012
comment
Это было очень полезно. В конечном итоге я использовал BlockingCollection<> вместо PowerShellAdapter. - person mungflesh; 23.02.2017
comment
Я тоже эффективно использовал то же самое. На самом деле я использовал ConcurrentQueue ‹›, но это структура данных по умолчанию, используемая BlockingCollection ‹›, если она не переопределена. - person Ants; 16.12.2019

Ответ, предоставленный Despertar, будет работать, но его можно немного улучшить.

Опрос в цикле с Thread.Sleep следует заменить использованием AutoResetEvent. Это приведет к тому, что основной поток будет «просыпаться» только тогда, когда действительно есть данные, и может позволить командлету завершиться быстрее, чем 100 мс. Thread.Sleep всегда заставляет командлет занимать не менее 100 мс, даже если он может выполняться быстрее. Это может не быть проблемой, если у вас есть простой командлет, но если вы вставите его в сложный конвейер, эти 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
    }
}

Обратите внимание: я на самом деле не тестировал этот код, но он иллюстрирует идею.

person MarkPflug    schedule 14.07.2017

Вы можете взглянуть на командлет Start-Job вместе с 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

person Reza    schedule 13.10.2012
comment
Спасибо за вклад, Реза. Но этот командлет написан на C #, а не на PowerShell, поэтому Start-Job / Get-Job / Receive-Job на самом деле не применимы. Задача C # на самом деле является эквивалентом задания PowerShell, единицы работы, которая может выполняться асинхронно или ждать результата. - person Despertar; 14.10.2012