创建BackgroundWorkers的线程似乎将已完成的事件排队

时间:2009-07-30 05:01:13

标签: multithreading backgroundworker

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

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

我写了一个相对简单的测试应用来证明这个问题。它基本上只是一个带有按钮和文本框的表单来显示结果(您可以在没有表单的情况下执行此操作,只是在控制台上显示结果,但我这样做是为了复制我的实际应用程序所做的事情)。这是代码:

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秒的延迟被触发)。

任何人都知道发生了什么事?

3 个答案:

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

(等)

现在试图找出正在发生的事情......但重要的是要注意工作线程 开始相隔1秒。

编辑:进一步调查:如果我打电话给ThreadPool.SetMinThreads(500, 500),那么即使在我的Vista盒子上,也会显示工人们基本上都在一起开始。

如果您尝试使用上述程序,无论是否拨打SetMinThreads,您的包装箱会发生什么?如果它在这种情况下有帮助而不是你真正的程序,你是否可以制作一个类似的简短但完整的程序,即使进行SetMinThreads调用也会显示它仍然存在问题?


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

因此,持续10秒,您只需将任务添加到长队列并每半秒创建一个新线程。 20个“主要”任务都是相对较长的任务,而ReportProgress任务非常短 - 所以只要你有足够的线程处理所有长时间运行的请求和一个短的请求,你就“离开了,所有的信息都快速传来。

如果您添加了对

的调用
ThreadPool.SetMaxThreads(50, 50);

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

关于你的设计的一个评论:你在不同的线程上有20个不同的任务,但是一次只能实现其中一个(由于锁定)。无论如何你都在有效地序列化请求,为什么要使用多个线程呢?我希望你真正的应用程序没有这个问题。

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

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