Использование класса .NET BackgroundWorker в консольном приложении - PullRequest
12 голосов
/ 04 февраля 2010

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

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

Ответы [ 3 ]

9 голосов
/ 04 февраля 2010

Если вам нужно просто заблокировать, пока все потоки не закончатся, это действительно просто - просто запустите новые потоки и затем вызовите 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);
    }
}

РЕДАКТИРОВАТЬ: Если у вас есть доступ к .NET 4.0, то TPL, безусловно, правильный путь. В противном случае я бы предложил использовать очередь производителя / потребителя (вокруг достаточно примеров кода). По сути, у вас есть очередь рабочих элементов и столько же потребительских потоков, сколько у вас ядер (при условии, что они связаны с процессором; вы хотите адаптировать его к своей рабочей нагрузке). Каждый потребительский поток будет брать элементы из очереди и обрабатывать их по одному. Как именно вы справитесь с этим, будет зависеть от вашей ситуации, но это не очень сложно. Еще проще, если вы сможете выполнить всю работу, с которой вам нужно начать, чтобы потоки могли просто выйти, когда обнаружили, что очередь пуста.

8 голосов
/ 04 февраля 2010

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

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


Edit:

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

Самой «хорошей» альтернативой, на мой взгляд, было бы получить копию 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, я лично позволил бы системе управлять степенью параллелизма. Он автоматически назначит соответствующее количество потоков (особенно, когда / если он переходит на .NET 4).

1 голос
/ 24 февраля 2014

Вы правы, что BackgroundWorker здесь не будет работать.Он действительно предназначен для работы со средами с графическим интерфейсом (например, WinForms и WPF), где основной поток постоянно занят проверкой / выполнением насоса сообщений (который обрабатывает все события Windows, такие как Click и Resize, а также события, происходящие из backgroundWorker).Без очереди событий Mainthread закончится.А без очереди событий BackgroundWorker также не может вызывать обратные вызовы в главном потоке.

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

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

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

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

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

...