4

我注意到 BackgroundWorkers 的一些奇怪行为以及它们正在触发的事件,其中事件似乎在一个线程中排队,而实际上并未使用 CPU。

基本上系统的设计是,基于用户交互,创建一个线程来发送 Web 请求以获取一些数据。根据结果​​,它可能会触发许多其他异步请求,对每个请求使用 BackgroundWorkers。我这样做是因为管理请求的代码使用锁来确保一次只发送一个请求(以避免向服务器发送多个同时请求的垃圾邮件,可能导致服务器忽略/阻止它们)。可能有更好的设计,我很想听听(我对 C#/Windows 窗体编程比较陌生,可以使用这些建议)。但是,无论设计更改如何,我都有兴趣了解导致我所看到的行为的原因。

我编写了一个相对简单的测试应用程序来演示这个问题。它基本上只是一个带有按钮和文本框的表单来显示结果(您可能可以不使用表单并在控制台上显示结果,但我这样做是为了复制我的实际应用程序所做的事情)。这是代码:

delegate void AddToLogCallback(string str);

private void AddToLog(string str)
{
    if(textBox1.InvokeRequired)
    {
        AddToLogCallback callback = new AddToLogCallback(AddToLog);
        Invoke(callback, new object[] { str });
    }
    else
    {
        textBox1.Text += DateTime.Now.ToString() + "   " + str + System.Environment.NewLine;
        textBox1.Select(textBox1.Text.Length, 0);
        textBox1.ScrollToCaret();
    }
}

private void Progress(object sender, ProgressChangedEventArgs args)
{
    AddToLog(args.UserState.ToString());
}

private void Completed(object sender, RunWorkerCompletedEventArgs args)
{
    AddToLog(args.Result.ToString());
}

private void DoWork(object sender, DoWorkEventArgs args)
{
    BackgroundWorker worker = sender as BackgroundWorker;
    lock (typeof(Form1)) // Ensure only a single request at a time
    {
        worker.ReportProgress(0, "Start");
        Thread.Sleep(2000); // Simulate waiting on the request
        worker.ReportProgress(50, "Middle");
        Thread.Sleep(2000); // Simulate handling the response from the request
        worker.ReportProgress(100, "End");
        args.Result = args.Argument;
    }
}

private void button1_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(RunMe);
    thread.Start();
}

private void RunMe()
{
    for(int i=0; i < 20; i++)
    {
        AddToLog("Starting " + i.ToString());
        BackgroundWorker worker = new BackgroundWorker();
        worker.WorkerReportsProgress = true;
        worker.DoWork += DoWork;
        worker.RunWorkerCompleted += Completed;
        worker.ProgressChanged += Progress;
        worker.RunWorkerAsync(i);
    }
}

这是我回来的结果:

30/07/2009 2:43:22 PM   Starting 0
30/07/2009 2:43:22 PM   Starting 1
<snip>
30/07/2009 2:43:22 PM   Starting 18
30/07/2009 2:43:22 PM   Starting 19
30/07/2009 2:43:23 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:36 PM   End
30/07/2009 2:43:36 PM   0
30/07/2009 2:43:36 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:36 PM   End
30/07/2009 2:43:36 PM   1
30/07/2009 2:43:36 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:36 PM   End
30/07/2009 2:43:36 PM   8
30/07/2009 2:43:36 PM   Start
30/07/2009 2:43:36 PM   Middle
30/07/2009 2:43:38 PM   13
30/07/2009 2:43:38 PM   End
30/07/2009 2:43:38 PM   Start
30/07/2009 2:43:40 PM   Middle
30/07/2009 2:43:42 PM   18
30/07/2009 2:43:42 PM   Start
30/07/2009 2:43:42 PM   End
30/07/2009 2:43:44 PM   Middle
30/07/2009 2:43:46 PM   End
30/07/2009 2:43:46 PM   2
30/07/2009 2:43:46 PM   Start
30/07/2009 2:43:48 PM   Middle

如您所见,在显示第一条“开始”消息后有 13 秒的延迟,之后它会处理大约 15 条消息(尽管大多数消息被触发之间有 2 秒的延迟)。

有谁知道发生了什么?

4

3 回答 3

3

编辑:好的,我从头开始。这是一个简短但完整的控制台应用程序,它显示了问题。它记录消息的时间和它所在的线程:

using System;
using System.Threading;
using System.ComponentModel;

class Test
{
    static void Main()
    {
        for(int i=0; i < 20; i++)
        {
            Log("Starting " + i);
            BackgroundWorker worker = new BackgroundWorker();
            worker.WorkerReportsProgress = true;
            worker.DoWork += DoWork;
            worker.RunWorkerCompleted += Completed;
            worker.ProgressChanged += Progress;
            worker.RunWorkerAsync(i);
        }
        Console.ReadLine();
    }

    static void Log(object o)
    {
        Console.WriteLine("{0:HH:mm:ss.fff} : {1} : {2}",
            DateTime.Now, Thread.CurrentThread.ManagedThreadId, o);
    }

    private static void Progress(object sender,
                                 ProgressChangedEventArgs args)
    {
        Log(args.UserState);
    }

    private static void Completed(object sender,
                                  RunWorkerCompletedEventArgs args)
    {
        Log(args.Result);
    }

    private static void DoWork(object sender, DoWorkEventArgs args)
    {
        BackgroundWorker worker = (BackgroundWorker) sender;
        Log("Worker " + args.Argument + " started");
        lock (typeof(Test)) // Ensure only a single request at a time
        {
            worker.ReportProgress(0, "Start");
            Thread.Sleep(2000); // Simulate waiting on the request
            worker.ReportProgress(50, "Middle");
            Thread.Sleep(2000); // Simulate handling the response
            worker.ReportProgress(100, "End");
            args.Result = args.Argument;
        }
    }
}

样本输出:

14:51:35.323 : 1 : Starting 0
14:51:35.328 : 1 : Starting 1
14:51:35.330 : 1 : Starting 2
14:51:35.330 : 3 : Worker 0 started
14:51:35.334 : 4 : Worker 1 started
14:51:35.332 : 1 : Starting 3
14:51:35.337 : 1 : Starting 4
14:51:35.339 : 1 : Starting 5
14:51:35.340 : 1 : Starting 6
14:51:35.342 : 1 : Starting 7
14:51:35.343 : 1 : Starting 8
14:51:35.345 : 1 : Starting 9
14:51:35.346 : 1 : Starting 10
14:51:35.350 : 1 : Starting 11
14:51:35.351 : 1 : Starting 12
14:51:35.353 : 1 : Starting 13
14:51:35.355 : 1 : Starting 14
14:51:35.356 : 1 : Starting 15
14:51:35.358 : 1 : Starting 16
14:51:35.359 : 1 : Starting 17
14:51:35.361 : 1 : Starting 18
14:51:35.363 : 1 : Starting 19
14:51:36.334 : 5 : Worker 2 started
14:51:36.834 : 6 : Start
14:51:36.835 : 6 : Worker 3 started
14:51:37.334 : 7 : Worker 4 started
14:51:37.834 : 8 : Worker 5 started
14:51:38.334 : 9 : Worker 6 started
14:51:38.836 : 10 : Worker 7 started
14:51:39.334 : 3 : Worker 8 started
14:51:39.335 : 11 : Worker 9 started
14:51:40.335 : 12 : Worker 10 started
14:51:41.335 : 13 : Worker 11 started
14:51:42.335 : 14 : Worker 12 started
14:51:43.334 : 4 : Worker 13 started
14:51:44.335 : 15 : Worker 14 started
14:51:45.336 : 16 : Worker 15 started
14:51:46.335 : 17 : Worker 16 started
14:51:47.334 : 5 : Worker 17 started
14:51:48.335 : 18 : Worker 18 started
14:51:49.335 : 19 : Worker 19 started
14:51:50.335 : 20 : Middle
14:51:50.336 : 20 : End
14:51:50.337 : 20 : Start
14:51:50.339 : 20 : 0
14:51:50.341 : 20 : Middle
14:51:50.343 : 20 : End
14:51:50.344 : 20 : 1
14:51:50.346 : 20 : Start
14:51:50.348 : 20 : Middle
14:51:50.349 : 20 : End
14:51:50.351 : 20 : 2
14:51:50.352 : 20 : Start
14:51:50.354 : 20 : Middle
14:51:51.334 : 6 : End
14:51:51.335 : 6 : Start
14:51:51.334 : 20 : 3
14:51:53.334 : 20 : Middle

(ETC)

现在试图弄清楚发生了什么......但重要的是要注意工作线程启动时间间隔为 1 秒。

编辑:进一步调查:如果我ThreadPool.SetMinThreads(500, 500)甚至在我的 Vista 盒子上打电话,它显示工人基本上都是一起开始的。

如果您尝试上述程序,无论是否调用 ,您的盒子会发生什么SetMinThreads?如果它在这种情况下有帮助,但对您的实际程序没有帮助,您能否制作一个类似的简短但完整的程序,表明即使有SetMinThreads调用它仍然是一个问题?


我相信我明白了。我认为ReportProgress正在添加一个新ThreadPool任务来处理消息......同时,您正忙于向线程池添加 20 个任务。现在关于线程池的事情是,如果没有足够的线程可以在请求到达时立即为它提供服务,那么池会在创建新线程之前等待半秒。这是为了避免为一组请求创建大量线程,如果您只是等待现有任务完成,这些请求可以很容易地在一个线程上处理。

因此,在 10 秒内,您只是将任务添加到长队列并每半秒创建一个新线程。20 个“主要”任务都是相对较长的任务,而这些ReportProgress任务都很短——所以只要你有足够的线程来处理所有长时间运行的请求和一个短的请求,你就离开了。消息很快通过。

如果您将呼叫添加到

ThreadPool.SetMaxThreads(50, 50);

在这一切开始之前,您会看到它的行为与您期望的一样。我并不是建议您必须为您的实际应用程序这样做,而只是为了显示差异。这会在池中创建一堆线程开始,只是等待请求。

One comment on your design: you've got 20 different tasks on different threads, but only one of them can actually occur at a time (due to the lock). You're effectively serializing the requests anyway, so why use multiple threads? I'm hoping your real application doesn't have this problem.

于 2009-07-30T06:09:39.383 回答
2

BackgroundWorker 类将在创建线程上发出它的回调,这对于 UI 任务非常方便,因为您不需要对 InvokeRequired 后跟 Invoke() 或 BeginInvoke() 进行额外检查。

不利的一面是,如果您的创建代码被阻塞或处于紧密循环中,您的回调将排队。

解决方案是自己管理线程。您已经表明您知道如何手动创建线程,尽管您可能希望查看 ThreadPool 主题以获取有关执行此操作的更多信息。

更新:这是一个基于使用队列和自定义 SingletonWorker 线程的反馈的工作示例。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        SingletonWorker.ProgressHandler = Progress;
        SingletonWorker.CompleteHandler = Completed;
    }
    private void button1_Click( object sender, EventArgs e )
    {
        // this is based on an app requirement, seems odd but I'm sure there's a reason :)
        Thread thread = new Thread( AddTasks );
        thread.Start();
    }
    private void AddTasks()
    {
        for ( int i = 0; i < 5; i++ )
        {
            AddToLog( "Creating Task " + i );
            SingletonWorker.AddTask( new Task { NumberToWorkOn = i } );
        }
    }
    private void AddToLog( string message )
    {
        if( textBox1.InvokeRequired )
        {
            textBox1.Invoke( new Action<string>( AddToLog ), message );
            return;
        }
        textBox1.Text += DateTime.Now + "   " + message + System.Environment.NewLine;
        textBox1.Select( textBox1.Text.Length, 0 );
        textBox1.ScrollToCaret();
    }
    private void Progress( string message, int percentComplete )
    {
        AddToLog( String.Format( "{0}%, {1}", percentComplete, message ) );
    }
    private void Completed( string message )
    {
        AddToLog( message );
    }
}
public class Task
{
    public int NumberToWorkOn { get; set; }
}
public static class SingletonWorker
{
    private static readonly Thread Worker;
    private static readonly Queue<Task> Tasks;
    // assume params are 'message' and 'percent complete'
    // also assume only one listener, otherwise use events
    public static Action<string, int> ProgressHandler;
    public static Action<string> CompleteHandler;
    static SingletonWorker()
    {
        Worker = new Thread( Start );
        Tasks = new Queue<Task>();
        Worker.Start();
    }
    private static Task GetNextTask()
    {
        lock( Tasks )
        {
            if ( Tasks.Count > 0 )
                return Tasks.Dequeue();

            return null;
        }
    }
    public static void AddTask( Task task )
    {
        lock( Tasks )
        {
            Tasks.Enqueue( task );
        }
    }
    private static void Start()
    {
        while( true )
        {
            Task task = GetNextTask();
            if( task == null )
            {
                // sleep for 500ms waiting for another item to be enqueued
                Thread.Sleep( 500 );
            }
            else
            {
                // work on it
                ProgressHandler( "Starting on " + task.NumberToWorkOn, 0 );
                Thread.Sleep( 1000 );
                ProgressHandler( "Almost done with " + task.NumberToWorkOn, 50 );
                Thread.Sleep( 1000 );
                CompleteHandler( "Finished with " + task.NumberToWorkOn );
            }
        }
    }
}
于 2009-07-30T07:03:54.250 回答
0

我遇到了同样的问题,BackgroundWorker 线程正在以串行方式运行。解决方案只是将以下行添加到我的代码中:

ThreadPool.SetMinThreads(100, 100);

默认 MinThreads 为 1,因此(可能主要在单核 CPU 上)如果您使用 BackgroundWorker 或 ThreadPool 创建线程,线程调度程序可能会假设 1 作为并发线程数是可接受的,因此会导致线程在串行时尚,即。使随后启动的线程等待先前的线程结束。通过强制它允许更高的最小值,您可以强制它并行运行多个线程,即如果您运行的线程多于核心,则时间片。

对于 Thread 类,即 thread.start(),这种行为并不明显,即使您不增加 SetMinThreads 中的值,它似乎也可以同时正常工作。

如果您还发现您对 Web 服务的调用一次最多只能工作 2 次,那么这是因为 2 是 Web 服务调用的默认最大值。要增加它,您必须将以下代码添加到您的 app.config 文件中:

<system.net>
  <connectionManagement>
    <add address="*" maxconnection="100" />
  </connectionManagement>
</system.net>
于 2011-01-31T21:28:40.923 回答