Поток, создающий BackgroundWorkers, кажется, ставит в очередь события Completed

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

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

Кто-нибудь знает, что происходит?


person Community    schedule 30.07.2009    source источник


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

person Jon Skeet    schedule 30.07.2009
comment
Я попытался установить пул потоков, как было предложено, однако проблема осталась той же (т. е. даже попробовал до 200 максимальных потоков каждого, и он по-прежнему показывал те же симптомы). Как я понял, хотя события ReportProgress (вместе с другими) выполняются в потоке, создавшем BackgroundWorker, - это не так? Что касается основного приложения, идея заключается в том, что эти запросы могут поступать из нескольких потоков. То есть потоки A, B и C генерируют асинхронные веб-запросы через класс WebRequestManager, который затем сериализует их. - person ; 30.07.2009
comment
@Андрей: Интересно. Какую версию .NET вы используете и какую операционную систему? Для меня это имеет большое значение в .NET 3.5 на XP. Хотя обработчик ProgressChanged действительно выполняется в потоке WinForms, сам метод ReportProgress является асинхронным, и я полагаю, что он использует задачу ThreadPool, чтобы избежать блокировки, пока он ожидает, пока поток WinForms обработает событие изменения хода выполнения. - person Jon Skeet; 30.07.2009
comment
Я не уверен, почему создание дополнительных потоков заранее не работает для вас, но я достаточно уверен, что это то, что происходит. - person Jon Skeet; 30.07.2009
comment
Я пробовал одно и то же приложение в .NET 3.5 на Windows Server 2003 (рабочая машина) и на 64-разрядной версии Vista Ultimate (дома). Они оба показывают точно такие же задержки. Я также только что попробовал .NET 2.0 на Windows Server 2003, и он все еще имеет задержки. Ваши комментарии относительно метода ReportProgress звучат правильно, но, как и раньше, я пытался увеличить размер ThreadPool и все еще не добился успеха, поэтому я не совсем уверен, что еще с ним делать... - person ; 05.08.2009
comment
Хм. Завтра я попытаюсь воспроизвести это на своем компьютере с Vista и посмотрю, что у меня получится. - person Jon Skeet; 05.08.2009
comment
Я только что попытался получить количество доступных потоков во время DoWork, и все еще доступно более 180 рабочих потоков (после того, как я установил максимальное значение 200), так что определенно не похоже, что потоки заканчиваются. для обработки сообщений. - person ; 05.08.2009
comment
Наконец-то у меня появилась возможность проверить ваше последнее предложение - использование SetMinThreads также работает для меня. Таким образом, я предполагаю, что ThreadPool имеет только определенное количество потоков, доступных для начала, и, по-видимому, есть какой-то дроссель для создания новых потоков, тогда как если вы форсируете их создание с самого начала с помощью SetMinThreads, задержки нет. Большое спасибо за работу над этим для меня. С тех пор я переключил код на использование очереди для обработки запросов, но мне всегда нравится знать причину подобных вещей, даже если они больше не являются для меня проблемой. - person ; 07.08.2009
comment
Я сейчас злюсь на себя... Я почти уверен, что SetMaxThreads был опечаткой с самого начала... Я подозреваю, что имел в виду SetMinThreads с самого первого поста, и это то, что сработало для меня... - person Jon Skeet; 07.08.2009

Класс BackgroundWorker выполнит свои обратные вызовы в потоке создания, это очень удобно для задач пользовательского интерфейса, поскольку вам не нужно выполнять дополнительную проверку InvokeRequired, за которой следует Invoke() или BeginInvoke().

Недостатком является то, что если ваш создающий код блокируется или находится в тесном цикле, ваши обратные вызовы ставятся в очередь.

Решение состоит в том, чтобы управлять своими потоками самостоятельно. Вы уже продемонстрировали, что знаете, как создать поток вручную, хотя, возможно, вы захотите просмотреть тему ThreadPool для получения дополнительной информации о том, как это сделать.

ОБНОВЛЕНИЕ: вот рабочий пример, основанный на отзывах, в котором используется Queue и пользовательский поток 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 );
            }
        }
    }
}
person Timothy Walters    schedule 30.07.2009
comment
Спасибо. Недостатком является то, что если ваш создающий код блокируется или находится в тесном цикле, ваши обратные вызовы ставятся в очередь, но это часть, которая меня смущает. Поток, создавший объекты BackgroundWorker (RunMe), уже завершил выполнение, но все еще имеет большую задержку перед обработкой возвращаемых событий. Возможно, как вы говорите, мне следует просто отказаться от класса BackgroundWorker и написать свой собственный асинхронный код... - person ; 30.07.2009
comment
Эндрю, мне, вероятно, следует уточнить, что все, кроме вызова DoWork(), будет в потоке создания (в данном случае в потоке пользовательского интерфейса). Вы пытались поместить этот цикл непосредственно в обработчик OnClick? Затем вы можете избавиться от всех проверок InvokeRequired, поскольку методы Progress и Complete будут вызываться для вас в потоке пользовательского интерфейса. Вы вызываете двойные переходы, добавляя поток вокруг создания рабочих процессов. - person Timothy Walters; 03.08.2009
comment
Я закодировал решение, которое все еще использует BackgroundWorker, но интернет-фильтр здесь, на работе, вызывает боль, поэтому мне придется отправить его из дома, короткая версия: не создавайте поток, не используйте вызов , так как BackgroundWorker обрабатывает все это за вас, также тщательно подумайте, действительно ли вам нужен этот оператор lock() в DoWork(). - person Timothy Walters; 03.08.2009
comment
К сожалению, поток, который генерирует BackgroundWorkers, необходим - он копирует то, что делает мое основное приложение, и он должен генерировать этих рабочих из отдельного потока. Что касается блокировки, то опять же она обязательно нужна — код в DoWork должен быть сериализован. Я попытался написать свой собственный класс BackgroundWorker, который делает почти все то же самое, за исключением того, что события обрабатываются в потоке BackgroundWorker (не идеально, но и не критично). Это работает без каких-либо задержек, поэтому я могу его использовать, но мне все еще любопытна проблема с BackgroundWorker. - person ; 05.08.2009
comment
BackgroundWorker не годится для того, что вам нужно, поскольку он разработан для задач, которые должны взаимодействовать с пользовательским интерфейсом, и зависит от того, создается ли он потоком пользовательского интерфейса (если только вы не можете вызывать обратно пользовательский интерфейс для создания каждого BackgroundWorker?). ThreadPool не рекомендуется для блокирующего кода, поэтому избегайте и этого. Попробуйте использовать очередь для хранения информации о задаче и свой собственный «рабочий» поток, он просто выполняет свою задачу и ищет другую, когда закончит, спит в течение 500 мс или что-то еще, пока другая задача не будет готова, если это необходимо. - person Timothy Walters; 05.08.2009
comment
Да, я начинаю так думать. Я собирался начать искать поток, который просто использовал бы очередь и обрабатывал запросы самостоятельно, вместо того, чтобы создавать так много потоков, когда на самом деле они не используются как независимые потоки. Одна из вещей, которую я сейчас изучаю, — заставить этот поток уведомлять потоки вызывающего абонента о завершении его обработки (или во время репликации ReportProgress). Я пытаюсь решить, лучше ли работать над межпоточной связью или просто использовать что-то вроде ThreadPool.QueueUserWorkItem для обработки результатов. Мысли? - person ; 05.08.2009

У меня была та же проблема: потоки 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>
person Gavin    schedule 31.01.2011