Кажется, поток, создающий BackgroundWorkers, ставит в очередь события Completed - PullRequest
4 голосов
/ 30 июля 2009

Я замечаю какое-то странное поведение с BackgroundWorkers и событиями, которые они запускают, когда события, кажется, стоят в очереди в одном потоке, в то время как процессор фактически не используется.

По сути, дизайн системы заключается в том, что на основе взаимодействия с пользователем создается поток для отправки веб-запроса на получение некоторых данных. Основываясь на результатах, он может запустить множество других асинхронных запросов, используя 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 ]

3 голосов
/ 30 июля 2009

РЕДАКТИРОВАТЬ: Хорошо, я начинаю с нуля. Вот короткое, но complete консольное приложение, которое показывает проблему. Он регистрирует время сообщения и поток, в котором он находится:

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 различных задач в разных потоках, но только одна из них может выполняться одновременно (из-за блокировки). В любом случае вы эффективно сериализуете запросы, так зачем использовать несколько потоков? Я надеюсь, что у вашего реального приложения нет этой проблемы.

2 голосов
/ 30 июля 2009

Класс BackgroundWorker будет выдавать обратные вызовы в создаваемом потоке, это очень удобно для задач пользовательского интерфейса, поскольку вам не нужно выполнять дополнительную проверку 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 );
            }
        }
    }
}
0 голосов
/ 01 февраля 2011

У меня была такая же проблема, потоки BackgroundWorker запускались в последовательном режиме. Решением было просто добавить следующую строку в мой код:

ThreadPool.SetMinThreads(100, 100);

По умолчанию MinThreads равен 1, поэтому (возможно, главным образом на одноядерном процессоре) планировщик потоков, вероятно, предположит, что 1 допустимо в качестве числа одновременных потоков, если вы используете BackgroundWorker или ThreadPool для создания потоков, что вызывает потоки работать в серийном режиме т.е. чтобы последующие запущенные потоки ожидали окончания предыдущих. Заставив его разрешить более высокий минимум, вы заставляете его запускать несколько потоков параллельно, то есть срезы времени, если вы запускаете больше потоков, чем у вас ядер.

Это поведение не проявляется для класса Thread, то есть thread.start (), который, кажется, работает правильно одновременно, даже если вы не увеличиваете значения в SetMinThreads.

Если вы также обнаружите, что ваши звонки на веб-службу работают максимум до 2 одновременно, то это потому, что 2 является максимумом по умолчанию для вызовов веб-службы. Чтобы увеличить это, вы должны добавить следующий код в ваш файл app.config:

<system.net>
  <connectionManagement>
    <add address="*" maxconnection="100" />
  </connectionManagement>
</system.net>
...