C# 后台工作人员 - 我应该同时使用多少?

Posted

技术标签:

【中文标题】C# 后台工作人员 - 我应该同时使用多少?【英文标题】:C# Background Workers - How many should I use simultaneously? 【发布时间】:2020-05-12 07:06:09 【问题描述】:

我正在用 C# 编写一个 MVVM (Caliburn.Micro) 应用程序,它使用 PowerShell 在远程计算机上运行 WMI 查询。这些计算机是从选定的 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);
    


我想如果所选 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 而不是后台工作人员。问题是它没有异步运行,并且 UI 在运行时挂起。

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 Parallel Library

我现在正在尝试使用 Task.Run 而不是 Parallel.ForEach 主要工作。任务正在执行并且 UI 没有挂起,但是如果我在所有任务完成执行之前从 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
            
        
    

【问题讨论】:

只是好奇您为什么使用BackgroundWorker 而不是并行任务库? 对此的简单回答是我从未听说过,哈哈。我将编程作为一种爱好,从未接受过任何培训,当我遇到一个新问题时就开始学习。我相信多线程会变得相当复杂,并且认为后台工作人员可能是最简单的方法,尤其是我之前使用过它们。 你为什么使用powershell? 我进行了一些搜索,发现实现这一目标的最简单方法是使用我的大学不熟悉的 PowerShell。因此,我在 C# 和 PowerShell 中使用了类似的方法来获取和编辑配额(以及来自 AD 的用户主目录)。我只是决定以同样的方式编写这个应用程序,因为 PowerShell 已经有一组函数来完成我需要的一切。我可以删除 PowerShell 并仍然使用 C# 实现一切,但这个应用程序只会在远程机器上运行一些命令并显示结果,所以我不确定它是否值得额外的工作。 @HansPassant 谢谢,很高兴知道。不过我想我还是会以教育的名义去追求TPL方法:) 【参考方案1】:

我正在使用多个后台工作人员来实现这一点,目前它正在发挥作用。

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

问题是它没有异步运行,并且 UI 在运行时挂起。

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

private void GetLoggedOnUsersTPL()

  Parallel.ForEach(Computers, (computer) =>
  
    ...
  );

应该这样称呼:

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

【讨论】:

【参考方案2】:

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

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

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

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

【讨论】:

那么创建这么多 PowerShell 实例并通过 CIM 会话连接到远程计算机也不会有问题吗? 取决于当时计算机的资源以及power shell如何处理线程操作。

以上是关于C# 后台工作人员 - 我应该同时使用多少?的主要内容,如果未能解决你的问题,请参考以下文章

在家佛弟子对待工作的态度——世俗八正道

C# ABP源码详解 之 BackgroundJob,后台工作

C# - 后台工作者?

C# 用自定义错误完成后台工作人员

Godot C# 在后台运行

C# Ionic.Zip 进度条作为后台工作者