C# Background Workers - 我应该同时使用多少个?

C# Background Workers - How many should I use simultaneously?

我正在用 C# 编写一个 MVVM (Caliburn.Micro) 应用程序,它使用 PowerShell 在远程计算机上 运行 WMI 查询。这些计算机是从 selected Active Directory OU 加载的,因此可以有任意数量的计算机。 WMI 查询的结果将显示在 UI 上,我想同时 运行 多个查询并在查询完成后立即显示每个查询。我正在使用多个后台工作人员来实现这一目标,目前它正在运行。但是,我当前的代码将为 OU 中的每台计算机创建一个后台工作程序,没有任何形式的队列或限制。

private void QueryComputers()
{
    foreach (RemoteComputer computer in Computers)
    {
        BackgroundWorker bw = new BackgroundWorker();
        bw.WorkerReportsProgress = true;
        bw.DoWork += BackgroundWorker_DoWork;
        bw.ProgressChanged += BackgroundWorker_ProgressChanged;
        bw.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
        bw.RunWorkerAsync(computer.DNSHostName);
    }

}

我想如果 selected OU 中有足够多的计算机,这会对性能产生很大影响。我应该将此限制为多少同时后台工作人员?您会使用静态数字还是基于 CPU 核心数?

此外,您将如何为此实现队列?我想过做这样的事情:

private int bwCount = 0;
private int bwLimit = 5; // 10, 20, 200??

private void QueryComputers()
{
    int stopAt = lastIndex + (bwLimit - bwCount);
    if (stopAt > Computers.Count - 1) stopAt = Computers.Count - 1;
    if (stopAt > lastIndex)
    {
        for (int i = lastIndex; i <= lastIndex + (bwLimit - bwCount); i++) {
            BackgroundWorker bw = new BackgroundWorker();
            bw.WorkerReportsProgress = true;
            bw.DoWork += BackgroundWorker_DoWork;
            bw.ProgressChanged += BackgroundWorker_ProgressChanged;
            bw.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
            bw.RunWorkerAsync(Computers[i].DNSHostName);

            lastIndex = i;
            bwCount++;
        }
    }
}

private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // Handle Result etc...

    bwCount--;
    QueryComputers();
}

编辑:

尝试使用任务并行库

我从我的应用程序中采用了一种方法,该方法从远程计算机检索登录用户并尝试使用 TPL 而不是后台工作者。问题是它不是异步 运行ning 并且 UI 在 运行s 时挂起。

private void GetLoggedOnUsersTPL()
{
    Parallel.ForEach(Computers, (computer) =>
    {
        using (PowerShell ps = PowerShell.Create())
        {

            computer.Status = RemoteComputer.ComputerStatus.UpdatingStatus;

            // Ping the remote computer to check if it's available to connect to
            ps.AddScript($"Test-Connection -ComputerName {computer.DNSHostName} -Count 1 -Quiet");
            Collection<PSObject> psOutput = ps.Invoke();
            if ((bool)psOutput[0].BaseObject) // If the computer responded to the Ping
            {
                ps.Commands.Clear(); // Remove the Test-Connection (Ping) command

                // Use a WMI query to find out who is logged on to the remote computer
                ps.AddScript($"Get-CimInstance -ComputerName {computer.DNSHostName} -Class Win32_ComputerSystem -Property UserName");
                psOutput = ps.Invoke();

                if (psOutput.Count < 1) // If there are no results we will try using a DCOM connection instead of WSMAN
                {
                    ps.Commands.Clear();
                    ps.AddScript("$opt = New-CimSessionOption -Protocol DCOM");
                    ps.AddScript($"$cims = New-CimSession -ComputerName {computer.DNSHostName} -SessionOption $opt");
                    ps.AddScript($"Get-CimInstance -Class Win32_ComputerSystem -Property UserName -CimSession $cims");
                    psOutput = ps.Invoke();
                }

                if (psOutput.Count > 0) // Check if we had any results
                {
                    string userName = psOutput[0].Members["UserName"].Value.ToString();
                    if (userName == null || userName == "")
                    {
                        computer.LoggedOnUser = "Nobody is logged on...";
                        computer.Status = RemoteComputer.ComputerStatus.Online;
                    }
                    else
                    {
                        computer.LoggedOnUser = userName;
                        computer.Status = RemoteComputer.ComputerStatus.Online;

                    }
                }
                else
                {
                    computer.Status = RemoteComputer.ComputerStatus.Blocked;
                }

            }
            else
            { 
                computer.Status = RemoteComputer.ComputerStatus.Offline;
            }
        }
    });
}

我尝试制作方法 asyncprivate async void GetLoggedOnUsersTPL() 但这告诉我我需要使用 await,我不确定在这个例子中在哪里使用它。

编辑 2:

第二次尝试使用任务并行库

我现在正在尝试使用 Task.Run 而不是最常用的 Parallel.ForEach。任务正在执行并且 UI 没有挂起,但是如果我 select 在所有任务完成执行之前从 TreeView 中创建一个新的 OU,调试器会在 token.ThrowIfCancellationRequested(); 行中断,所以他们没有被抓住。有人能指出我在这里做错了什么吗?

public override bool IsSelected // << AD OU IsSelected in TreeView
{
    get { return isSelected; }
    set
    {
        if (isSelected != value)
        {
            isSelected = value;

            if (getLoggedOnUsersTokenSource != null) // If any 'GetLoggedOnUsers' tasks are still running, cancel them
            {
                getLoggedOnUsersTokenSource.Cancel(); 
            }

            LoadComputers(); // Load computers from the selected OU
            GetLoggedOnUsersTPL();
        }
    }
}

private CancellationTokenSource getLoggedOnUsersTokenSource;
private async void GetLoggedOnUsersTPL()
{
    getLoggedOnUsersTokenSource = new CancellationTokenSource();
    CancellationToken token = getLoggedOnUsersTokenSource.Token;

    List<Task> taskList = new List<Task>();
    foreach (RemoteComputer computer in Computers)
    {
        taskList.Add(Task.Run(() => GetLoggedOnUsersTask(computer, token), token));

    }

    try
    {
        await Task.WhenAll(taskList);
    } catch (OperationCanceledException) // <<<< Not catching all cancelled exceptions
    {
        getLoggedOnUsersTokenSource.Dispose();
    }

}

private void GetLoggedOnUsersTask(RemoteComputer computer, CancellationToken token)
{
    using (PowerShell ps = PowerShell.Create())
    {
        if (token.IsCancellationRequested)
        {
            token.ThrowIfCancellationRequested();
        }

        // Ping remote computer to check if it's online

        if ((bool)psOutput[0].BaseObject) // If the computer responded to the Ping
        {
            if (token.IsCancellationRequested)
            {
                token.ThrowIfCancellationRequested();
            }

            // Run WMI query to get logged on user using WSMAN

            if (psOutput.Count < 1) // If there were no results, try DCOM
            {

                if (token.IsCancellationRequested)
                {
                    token.ThrowIfCancellationRequested();
                }

                // Run WMI query to get logged on user using DCOM

                // Process results
            }
        }
    }
}

我在 WPF 应用程序中有一个将财务记录从一个总帐数据库移动到下一个数据库的应用程序。每个操作都是独立的,运行 在后台线程上,有时 spring 会启动或处于休眠状态并向 wpf 应用程序的视图报告,该应用程序忠实地记录了它们的实时状态。

在测试期间,我的想法是最终限制总操作以确保顺利操作。

这种限制 从未 实施,我将应用程序发布到生产环境中,不同的人 运行 该应用程序针对他们的特定模式。

所以我的建议是做类似的事情,你可以 运行 超过 200 个线程执行内部异步操作而不费力......所以这取决于操作的负载以及它们正在做什么比特定数字更能影响该决定。

I'm using multiple background workers to achieve this and at the moment it's working.

BackgroundWorker 是一种相当过时的类型,不能很好地处理动态需求。如果您的工作负载是同步的(看起来是),Parallel 是更好的方法。

The problem is it's not running asynchronously and the UI is hanging while it runs.

Parallel.ForEach 是一个很好的解决方案。要解锁 UI,只需将其推送到线程池线程即可。所以这个并行方法:

private void GetLoggedOnUsersTPL()
{
  Parallel.ForEach(Computers, (computer) =>
  {
    ...
  });
}

应该这样称呼:

await Task.Run(() => GetLoggedOnUsersTPL());