Подтвердить что ты не робот

Использование класса .NET BackgroundWorker в консольном приложении

Я относительно новичок в .NET-программировании и многопоточности в целом и задавался вопросом, нормально ли использовать .NET при условии, что BackgroundWorker создает рабочие потоки для выполнения некоторой работы в консольном приложении? Из различных документов в Интернете я вижу, что намерение для этого класса больше подходит для ориентированных на пользовательский интерфейс приложений, где вы хотите выполнить некоторую работу в фоновом режиме, но поддерживайте интерфейс пользователя и сообщайте о достигнутом прогрессе, отменяя обработку при необходимости и т.д.

В моем случае, в основном, у меня есть класс контроллера, в котором я хочу развернуть несколько рабочих потоков, чтобы выполнить некоторую обработку (ограничение максимального числа рабочих потоков, порожденных с помощью семафора). Затем я хочу, чтобы мой класс контроллера блокировался, пока все потоки не завершили обработку. Поэтому после того, как я начинаю рабочий поток, чтобы выполнить некоторую работу, я хочу, чтобы поток мог уведомлять поток контроллера, когда обработка завершена. Я вижу, что я могу использовать класс рабочего класса, и обрабатывать события DoWork и RunWorkerCompleted для выполнения этого, однако интересно было ли это хорошая идея? Есть ли лучшие способы достичь этого?

4b9b3361

Ответ 1

Это не будет работать в консольном приложении, так как SynchronizationContext.Current никогда не будет инициализироваться. Это инициализируется Windows Forms или WPF для вас, когда вы используете GUI-приложение.

Иными словами, нет причин для этого. Просто используйте ThreadPool.QueueUserWorkItem и событие reset (ManualResetEvent или AutoResetEvent), чтобы уловить состояние завершения и заблокировать основной поток.


Edit:

После просмотра некоторых комментариев OP я подумал, что добавлю это.

"Наилучшей" альтернативой, на мой взгляд, было бы получить копию Rx Framework, поскольку она включает в себя TPL в .NET 4. Это позволит вам использовать перегрузку Parallel.ForEach, которая предоставляет возможность поставки ParallelOptions. Это позволит вам ограничить общее количество одновременных операций и выполнить всю работу за вас:

// using collection of work items, such as: List<Action<object>> workItems;
var options = new ParallelOptions();
options.MaxDegreeOfParallelism = 10; // Restrict to 10 threads, not recommended!

// Perform all actions in the list, in parallel
Parallel.ForEach(workItems, options, item => { item(null); });

Однако, используя Parallel.ForEach, я лично позволю системе управлять степенью parallelism. Он автоматически назначит соответствующее количество потоков (особенно когда/если это переместится на .NET 4).

Ответ 2

Если ваше требование просто заблокировать, пока все потоки не закончатся, это очень просто - просто запустите новые потоки, а затем вызовите Thread.Join на каждом из них:

using System;
using System.Collections.Generic;
using System.Threading;

public class Test
{
    static void Main()
    {
        var threads = new List<Thread>();
        for (int i = 0; i < 10; i++)
        {
            int copy = i;
            Thread thread = new Thread(() => DoWork(copy));
            thread.Start();
            threads.Add(thread);
        }

        Console.WriteLine("Main thread blocking");
        foreach (Thread thread in threads)
        {
            thread.Join();
        }
        Console.WriteLine("Main thread finished");
    }

    static void DoWork(int thread)
    {
        Console.WriteLine("Thread {0} doing work", thread);
        Random rng = new Random(thread); // Seed with unique numbers
        Thread.Sleep(rng.Next(2000));
        Console.WriteLine("Thread {0} done", thread);
    }
}

EDIT: если у вас есть доступ к .NET 4.0, то TPL, безусловно, правильный путь. В противном случае я бы предложил использовать очередь производителей/потребителей (там было много примеров кода). В основном у вас есть очередь рабочих элементов и столько потребительских потоков, сколько у вас есть ядра (при условии, что они связаны с процессором, вы хотите настроить его на свою рабочую нагрузку). Каждый потребительский поток будет брать элементы из очереди и обрабатывать их по одному за раз. Точно как вы справляетесь с этим, это будет зависеть от вашей ситуации, но это не очень сложно. Это еще проще, если вы можете придумать всю работу, которую вам нужно сделать для начала, чтобы потоки могли просто выйти, когда они обнаружили, что очередь пуста.

Ответ 3

Вы правы, что BackgroundWorker здесь не работает. Это действительно предназначено для работы с средами графического интерфейса (WinForms и WPF), где основной поток постоянно занят проверкой/выполнением Message Pump (который обрабатывает все события Windows, такие как Click и Resize, а также те, которые отправляются из backgroundWorker). Без очереди событий Mainthread закончится. И без очереди событий BackgroundWorker также не может вызывать обратные вызовы в основном потоке.

Действительно, многопоточность на консоли в десятки раз сложнее, потому что у вас нет Message Pump для решения двух больших проблем: как сохранить главный поток и как сообщить основной поток изменений, необходимых для пользовательского интерфейса. Программирование на основе событий было идеальным местом для обучения многопоточности.

Есть несколько решений:

Разверните консольное приложение с помощью Message Pump. Поскольку это немного больше, чем коллективная коллекция делегатов (где другие потоки только добавляют Stuff) и цикл while на удивление легко: Консольное приложение С# + обработка событий

Классы ThreadPool и Thread. Они были до тех пор, как BackgroundWorker (начиная с .NET 2.0) и, похоже, предназначены для решения проблемы для консоли. Вам все равно нужно заблокировать основной поток контуром. ThreadPool также ограничивает количество потоков для чего-то доступного на Real-машине. Вам следует избегать фиксированных ограничений по потоку и вместо этого полагаться на функции ThreadPooling для ОС, чтобы решить вопрос "сколько потоков должно выполняться сразу?". Например, фиксированное число из 5 нитей может быть идеальным для вашей 6-й основной машины разработки. Но для одного основного настольного компьютера это будет слишком много. И было бы бессмысленным "Узким местом" на сервере, который легко мог бы переместиться между 16 и 64 ядрами с помощью GiB RAM.

Если у вас есть .NET 4.0 или новее, новый параллелизм задач может стоить особого внимания. http://msdn.microsoft.com/en-us/library/dd537609.aspx Он построил в ThreadPooling, ограниченные возможности Priorization, и он может легко и быстро подключиться к нескольким задачам (все, что может сделать Thread, Task может сделать). Использование задачи также позволяет использовать async и ждать Ключевые слова: http://msdn.microsoft.com/en-us/library/hh191443.aspx Опять же, вам не обойти блокировку главного потока в консольном приложении.