Страницы

Поиск по вопросам

Показаны сообщения с ярлыком очередь-задач. Показать все сообщения
Показаны сообщения с ярлыком очередь-задач. Показать все сообщения

воскресенье, 12 января 2020 г.

Почему всегда threadPoolExecutor.getActiveCount() <= maximumPoolSize/2?

#java #очередь_задач


Есть очередь задач, создана следующим образом:

LinkedBlockingQueue queue = new LinkedBlockingQueue();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(maximumPoolSize, maximumPoolSize,
50000L, TimeUnit.MILLISECONDS, queue);


Значение maximumPoolSize равно 200. Во время работы в queue попадает большое количество
потоков (больше тысячи), но значение, возвращаемое методом threadPoolExecutor.getActiveCount()
всегда меньше либо равно 100. Например, значения threadPoolExecutor.getActiveCount()
и queue.size() логируются следующим образом:

logger.debug("Активные потоки: " + threadPoolExecutor.getActiveCount() + ". В очереди
" + queue.size() + " потоков.");


и в итоге мы получаем следующую картину:

Активные потоки: 1. В очереди 0 потоков.
Активные потоки: 2. В очереди 0 потоков.
Активные потоки: 3. В очереди 0 потоков.
Активные потоки: 4. В очереди 0 потоков.
Активные потоки: 5. В очереди 0 потоков.
...
Активные потоки: 86. В очереди 0 потоков.
Активные потоки: 87. В очереди 1 потоков.
Активные потоки: 88. В очереди 1 потоков.
Активные потоки: 89. В очереди 1 потоков.
Активные потоки: 90. В очереди 1 потоков.
...
Активные потоки: 99. В очереди 1 потоков.
Активные потоки: 100. В очереди 1 потоков.
Активные потоки: 100. В очереди 2 потоков.
Активные потоки: 100. В очереди 3 потоков.
Активные потоки: 100. В очереди 4 потоков.
...
Активные потоки: 100. В очереди 1874 потоков.
Активные потоки: 100. В очереди 1875 потоков.
Активные потоки: 100. В очереди 1876 потоков.
Активные потоки: 100. В очереди 1877 потоков.
Активные потоки: 100. В очереди 1878 потоков.
Активные потоки: 100. В очереди 1879 потоков.
Активные потоки: 100. В очереди 1880 потоков.
Активные потоки: 100. В очереди 1881 потоков.
Активные потоки: 100. В очереди 1882 потоков.
Активные потоки: 100. В очереди 1883 потоков.


В документации написано, что метод threadPoolExecutor.getActiveCount() возвращает
приблизительное количество потоков с активно выполняющимися задачами. Но почему максимальный
порог этого приблизительного значения тут равен maximumPoolSize/2?

P.S. Это логи не с моего компьютера, у меня такую ситуацию воспроизвести не удалось
- у меня в данном случае количество активных потоков равняется 200, как и ожидалось.
Может ли это быть зависимость от количества процессоров/количества ядер процессоров/какой-либо
конфигурации ПО?
    


Ответы

Ответ 1



"Есть очередь задач, создана следующим образом: ... new ThreadPoolExecutor(maximumPoolSize, maximumPoolSize, .." Вы уверены, что maximumPoolSize - это одна переменная, и она имеет одно значение? Полагаю, что причина в том, что corePoolSize равен 100, а maximumPoolSize равен 200 и у Вас происходит следующее? При добавлении нового потока в пул выполняет следующие действия последовательно: Проверяет достигнут ли предел потоков, задаваемых переменной corePoolSize если можно еще запустить поток - запускает его. Если количество потоков равно или больше corePoolSize переходит к следующему шагу. Проверяет не заполнена ли очередь и если в ней еще есть место добавляет туда поток. Если очередь заполнена переходит к следующему шагу. Пытается увеличить количество работающих потоков до значения maximumPoolSize если можно добавить еще поток, то он добавляется и в нем и запускается задание. Если количество работающих потоков равно maximumPoolSize то выбрасывается исключение java.util.concurrent.RejectedExecutionException или вызывается перехватчик отклонения задачи определяемый программистом. В вашем случае в виду того, что очередь увеличивается без ограничений к третьему шагу алгоритм никогда не переходит. Но можно «изменить» порядок следования шагов 2 и 3. Для этого необходимо создать очередь в которой мы переопределим вызываемый ThreadPoolExecutor метод offer так, чтобы он возвращал всегда false (очередь заполнена). Теперь нам нужно задать собственный перехватчик RejectedExecutionHandler. В перехватчике мы добавляем полученный поток в очередь. Пример изложенного выше в виде кода: LinkedBlockingQueue queue = new LinkedBlockingQueue (100){ public boolean offer(Runnable e) {return false;} }; ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 200, 1, TimeUnit.MINUTES, queue, new RejectedExecutionHandler() { public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { try { executor.getQueue().put(r); } catch (InterruptedException e) { e.printStackTrace(); } } });

суббота, 30 ноября 2019 г.

Ревью класса для очереди команд

#c_sharp #многопоточность #очередь #очередь_задач #инспекция_кода


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

public class ConcurrentCommandsQueue : IDisposable
{
    private readonly object _lockForSyncExecuting;
    private readonly Queue _commandQueue;

    private readonly Action _executeCommand;
    private readonly Action _onStartCommandsExecution;
    private readonly Action _onEndCommandsExecution;

    private volatile bool _isDisposed;
    private volatile bool _isExecuting;
    private volatile Task _taskForAction;

    public bool IsExecuting
    {
        get { return _isExecuting; }
    }

    public ConcurrentCommandsQueue(Action executeCommand) : this(executeCommand,
null, null) { }
    public ConcurrentCommandsQueue(Action executeCommand, 
        Action onStartCommandsExecution, Action onEndCommandsExecution)
    {
        _lockForSyncExecuting = new object();
        _commandQueue = new Queue(10);

        _isDisposed = false;
        _isExecuting = false;
        _executeCommand = executeCommand;
        _onStartCommandsExecution = onStartCommandsExecution;
        _onEndCommandsExecution = onEndCommandsExecution;
        _taskForAction = new TaskCompletionSource().Task;
    }

    private void ExecuteCommands()
    {
        if (_onStartCommandsExecution != null)
            _onStartCommandsExecution();
        T command = default(T);
        while (true)
        {
            lock (_lockForSyncExecuting)
            {
                if (_commandQueue.Count == 0)
                {
                    _isExecuting = false;
                    break;
                }
                command = _commandQueue.Dequeue();
            }
            _executeCommand(command);
        }
        if (_onEndCommandsExecution != null)
            _onEndCommandsExecution(command);
    }

    public void AddCommand(T command)
    {
        if (_isDisposed)
            throw new ObjectDisposedException(GetType().ToString());

        lock (_lockForSyncExecuting)
        {
            if (_isDisposed)
                return;
            _commandQueue.Enqueue(command);
            if (!_isExecuting)
            {
                _isExecuting = true;
                _taskForAction = Task.Run(new Action(ExecuteCommands));
            }
        }
    }
    public void ClearCommandQueue()
    {
        if (_isDisposed)
            throw new ObjectDisposedException(GetType().ToString());

        lock (_lockForSyncExecuting)
            _commandQueue.Clear();
    }
    public void Dispose()
    {
        if (_isDisposed)
            return;

        _isDisposed = true;
        lock (_lockForSyncExecuting)
            _commandQueue.Clear();
        if (_taskForAction != null)
            _taskForAction.Wait();
    }
}

    


Ответы

Ответ 1



У вас получился код в стиле .NET 1.0, который слегка эволюционировал с приходом лямбд (Delegate механически заменён на Action) и задач (new Thread().Start() механически заменён на Task.Run()), но по сути совершенно не изменился: вы TPL с async/await толком не используете, а существование concurrent коллекций вообще упустили из виду. В .NET 4.5 это всё ненужно в принципе. Если вам нужно выполнение задач строго в одном потоке, то можно просто кидать задачи в однопоточный планировщик задач (task scheduler). Собственно, на этой первой строчке весь код и заканчивается. Если нужно выполнение кода перед всеми операциями и после всех операций, то код перед и после этой строчки и пишется. Отмечу, что при этой однострочной реализации вы ещё имеете бонусом возможность использования токена отмены (cancellation token) и прочих радостей жизни. Рассмотрим пример. Допустим, надо выполнить две очереди задач: в каждой очереди задачи выполняются последовательно, но очереди выполняются параллельно. Создаём два планировщика задач с ограничением параллелизма, затем создаём задачи с указанием нужного нам планировщика. class Program { static readonly Random _rnd = new Random(); static readonly LimitedConcurrencyTaskScheduler _schedulerFoo = new LimitedConcurrencyTaskScheduler(1); static readonly LimitedConcurrencyTaskScheduler _schedulerBar = new LimitedConcurrencyTaskScheduler(1); static void Main () => new Program().Run().Wait(); async Task Run () { Task queueFoo = RunQueue("Foo", _schedulerFoo, Enumerable.Range(0, 3).Select(i => (Action)(() => Foo("Foo")))); Task queueBar = RunQueue("Bar", _schedulerBar, Enumerable.Range(0, 3).Select(i => (Action)(() => Foo("Bar")))); await Task.WhenAll(queueFoo, queueBar); Console.WriteLine("Done!"); Console.ReadKey(); } async Task RunQueue (string name, TaskScheduler scheduler, IEnumerable commands) { Console.WriteLine($"{name}: Start"); await Task.WhenAll(commands.Select(c => RunTask(c, scheduler))); Console.WriteLine($"{name}: Finish"); } async Task RunTask (Action task, TaskScheduler scheduler) { await Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler); } void Foo (string name) { int timeout = _rnd.Next(200); Console.WriteLine($"{name}: Start {timeout}"); Thread.Sleep(timeout); Console.WriteLine($"{name}: Finish {timeout}"); } } Пример вывода: Foo: Start Bar: Start Foo: Start 165 Bar: Start 50 Bar: Finish 50 Bar: Start 39 Bar: Finish 39 Bar: Start 115 Foo: Finish 165 Foo: Start 116 Bar: Finish 115 Bar: Finish Foo: Finish 116 Foo: Start 0 Foo: Finish 0 Foo: Finish Done! Хотя в этом примере все задачи и кидаются одновременно, задачи можно добавлять в любой момент. Правда тогда смысл "начала" и "конца" несколько теряется. Если вы объясните, что вы собираетесь с ними делать, тогда можно будет добавить. Здесь я воспользовался планировщиком LimitedConcurrencyTaskScheduler из примеров на MSDN: Samples for Parallel Programming with the .NET Framework (статья с описанием: ParallelExtensionsExtras Tour - #7 - Additional TaskSchedulers). /// /// Provides a task scheduler that ensures a maximum concurrency level while running on top of the ThreadPool. /// Source: http://code.msdn.microsoft.com/ParExtSamples /// Documentation: http://blogs.msdn.com/b/pfxteam/archive/2010/04/09/9990424.aspx /// License: MS-LPL /// public class LimitedConcurrencyTaskScheduler : TaskScheduler { [ThreadStatic] private static bool _currentThreadIsProcessingItems; private readonly int _maxDegreeOfParallelism; private readonly LinkedList _tasks = new LinkedList(); // protected by lock(_tasks) private int _delegatesQueuedOrRunning = 0; // protected by lock(_tasks) /// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the specified degree of parallelism. /// The maximum degree of parallelism provided by this scheduler. public LimitedConcurrencyTaskScheduler (int maxDegreeOfParallelism) { if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); _maxDegreeOfParallelism = maxDegreeOfParallelism; } /// Gets the maximum concurrency level supported by this scheduler. public override sealed int MaximumConcurrencyLevel => _maxDegreeOfParallelism; protected override sealed IEnumerable GetScheduledTasks () { bool lockTaken = false; try { Monitor.TryEnter(_tasks, ref lockTaken); if (lockTaken) return _tasks.ToArray(); else throw new NotSupportedException(); } finally { if (lockTaken) Monitor.Exit(_tasks); } } protected override sealed void QueueTask (Task task) { // Add the task to the list of tasks to be processed. If there aren't enough // delegates currently queued or running to process tasks, schedule another. lock (_tasks) { _tasks.AddLast(task); if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) { ++_delegatesQueuedOrRunning; NotifyThreadPoolOfPendingWork(); } } } /// Informs the ThreadPool that there's work to be executed for this scheduler. private void NotifyThreadPoolOfPendingWork () { ThreadPool.UnsafeQueueUserWorkItem(_ => { // Note that the current thread is now processing work items. // This is necessary to enable inlining of tasks into this thread. _currentThreadIsProcessingItems = true; try { // Process all available items in the queue. while (true) { Task item; lock (_tasks) { // When there are no more items to be processed, // note that we're done processing, and get out. if (_tasks.Count == 0) { --_delegatesQueuedOrRunning; break; } // Get the next item from the queue item = _tasks.First.Value; _tasks.RemoveFirst(); } // Execute the task we pulled out of the queue TryExecuteTask(item); } } finally { // We're done processing items on the current thread _currentThreadIsProcessingItems = false; } }, null); } protected override sealed bool TryExecuteTaskInline (Task task, bool taskWasPreviouslyQueued) { // If this thread isn't already processing a task, we don't support inlining if (!_currentThreadIsProcessingItems) return false; // If the task was previously queued, remove it from the queue if (taskWasPreviouslyQueued) TryDequeue(task); // Try to run the task. return TryExecuteTask(task); } protected override sealed bool TryDequeue (Task task) { lock (_tasks) return _tasks.Remove(task); } } Ещё можно воспользоваться BlockingCollection<> с ConcurrentQueue<> внутри. По окончанию добавления надо будет не забыть вызвать CompleteAdding. Ещё есть TPL Dataflow. Там параллелизмы и прочие настраиваются без кастомных планировщиков. Можно ещё Rx добавить в качестве вишенки на торте. Вариантов море. Вообще, сейчас ещё @Vlad прибежит, расскажет про хитрые мозговыносящие способы решить эту задачу. :)

Ответ 2



Вам нужен MailboxProcessor - агент для обработки сообщений, который выполняет асинхронные операции. Реализация паттерна "много писателей, один читатель".

воскресенье, 24 ноября 2019 г.

Имплементация Producer/Consumer pattern


Паттерн producer/consumer достаточно часто встречается в многопоточном программировании
Его смысл состоит в том, что один или несколько потоков производят данные, и параллельно этому один или несколько потоков потребляют их.

Как правильно имплементировать этот паттерн в популярных языках программирования
Задача сама по себе нетривиальна, поскольку включает синхронизацию между потоками, и потенциальную гонку между несколькими производителями и потребителями.



Справка.

Производящий поток (или потоки) называется «производитель», «поставщик» или просто «producer», потребляющий (-ие) — «потребитель» или «consumer».

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

Примеры:

Произведённые данные могут представлять вычислительно интенсивное задание. В это
случае разумно иметь единственный производящий поток, и несколько выполняющих потоков (например, столько, сколько в системе ядер процессора, если узкое место обработки — вычисления).

Или производящие потоки загружают данные из сети, а по окончанию загрузки выполняющи
потоки производит разбор загруженных данных. В этом случае разумно иметь по одному производителю на сайт и, и ограничивать число производителей, если предел доступной скорости сети исчерпан.



Этот вопрос — адаптация одноименного исследования с Хэшкода.
    


Ответы

Ответ 1



Реализация на C# Для современных версий языка (начиная с C# 4.0), имеет смысл не писать реализаци вручную, а (руководствуясь советом @Flammable), воспользоваться классом BlockingCollection, представляющим нужную функциональность. Для чтения в consumer-потоках используем просто циклы по последовательности, котору даёт GetConsumingEnumerable(). В producer-потоках пользуемся Add, и в конце не забываем CompleteAdding, чтобы consumer-потоки смогли остановиться. Пример: class Program { static public void Main() { new Program().Run(); } BlockingCollection q = new BlockingCollection(); void Run() { var threads = new [] { new Thread(Consumer), new Thread(Consumer) }; foreach (var t in threads) t.Start(); string s; while ((s = Console.ReadLine()).Length != 0) q.Add(s); q.CompleteAdding(); // останавливаем foreach (var t in threads) t.Join(); } void Consumer() { foreach (var s in q.GetConsumingEnumerable()) { Console.WriteLine("Processing: {0}", s); Thread.Sleep(2000); Console.WriteLine("Processed: {0}", s); } } } BlockingCollection позволяет ограничить количество элементов, так что попытк добавить элемент в переполненную очередь также может быть заблокирована до освобождения места. Заметьте, что GetConsumingEnumerable корректно работает даже в случае, когда у вас много консьюмеров. Это не так уж и очевидно. Если вы работаете со старой версией C#, вам придётся писать нужную функциональност вручную. Вы можете воспользоваться встроенным классом Monitor (который является аналогом mutex + condition variable из pthreads). public class ProducerConsumer where T : class { object mutex = new object(); Queue queue = new Queue(); bool isDead = false; public void Enqueue(T task) { if (task == null) throw new ArgumentNullException("task"); lock (mutex) { if (isDead) throw new InvalidOperationException("Queue already stopped"); queue.Enqueue(task); Monitor.Pulse(mutex); } } public T Dequeue() { lock (mutex) { while (queue.Count == 0 && !isDead) Monitor.Wait(mutex); if (queue.Count == 0) return null; return queue.Dequeue(); } } public void Stop() { lock (mutex) { isDead = true; Monitor.PulseAll(mutex); } } } Использование (аналогичный пример): class Program { static public void Main() { new Program().Run(); } ProducerConsumer q = new ProducerConsumer(); void Run() { var threads = new [] { new Thread(Consumer), new Thread(Consumer) }; foreach (var t in threads) t.Start(); string s; while ((s = Console.ReadLine()).Length != 0) q.Enqueue(s); q.Stop(); foreach (var t in threads) t.Join(); } void Consumer() { while (true) { string s = q.Dequeue(); if (s == null) break; Console.WriteLine("Processing: {0}", s); Thread.Sleep(2000); Console.WriteLine("Processed: {0}", s); } } }

Ответ 2



Реализация на C с библиотекой pthreads В C, в соответствии с духом языка, нет встроенных высокоуровневых синхронизирующихс коллекций. Наверное самой популярной и широко используемой библиотекой, реализующей многопоточность, является pthreads. С её помощью паттерн можно реализовать так: #include // объявляем структуру данных для одного задания struct producer_consumer_queue_item { struct producer_consumer_queue_item *next; // здесь идут собственно данные. вы можете поменять этот кусок, // использовав структуру, более специфичную для вашей задачи void *data; }; // объявляем очередь с дополнительными структурами для синхронизации. // в этой очереди будут храниться произведённые, но ещё не потреблённые задания. struct producer_consumer_queue { struct producer_consumer_queue_item *head, *tail; // head == tail == 0, если очередь пуста pthread_mutex_t lock; // мьютекс для всех манипуляций с очередью pthread_cond_t cond; // этот cond "сигналим", когда очередь стала НЕ ПУСТОЙ int is_alive; // показывает, не закончила ли очередь свою работу }; Теперь нам нужны процедуры добавления и извлечения заданий из очереди. void enqueue (void *data, struct producer_consumer_queue *q) { // упакуем задание в новую структуру struct producer_consumer_queue_item *p = (typeof(p))malloc(sizeof(*p)); p->data = data; p->next = 0; // получим "эксклюзивный" доступ к очереди заданий pthread_mutex_lock(&q->lock); // ... и добавим новое задание туда: if (q->tail) q->tail->next = p; else { q->head = p; // очередь была пуста, а теперь нет -- надо разбудить потребителей pthread_cond_broadcast(&q->cond); } q->tail = p; // разрешаем доступ всем снова pthread_mutex_unlock(&q->lock); } void * dequeue(struct producer_consumer_queue *q) { // получаем эксклюзивный доступ к очереди: pthread_mutex_lock(&q->lock); while (!q->head && q->is_alive) { // очередь пуста, делать нечего, ждем... pthread_cond_wait(&q->cond, &q->lock); // wait разрешает доступ другим на время ожидания } // запоминаем текущий элемент или 0, если очередь умерла struct producer_consumer_queue_item *p = q->head; if (p) { // и удаляем его из очереди q->head = q->head->next; if (!q->head) q->tail = q->head; } // возвращаем эксклюзивный доступ другим участникам pthread_mutex_unlock(&q->lock); // отдаём данные void *data = p ? p->data : 0; // 0 означает, что данных больше не будет free(p); return data; } Ещё нужна процедура для инициализации очереди: struct producer_consumer_queue * producer_consumer_queue_create() { struct producer_consumer_queue *q = (typeof(q))malloc(sizeof(*q)); q->head = q->tail = 0; q->is_alive = 1; pthread_mutex_init(&q->lock, 0); pthread_cond_init(&q->cond, 0); return q; } И процедура для закрытия очереди: void producer_consumer_queue_stop(struct producer_consumer_queue *q) { // для обращения к разделяемым переменным необходим эксклюзивный доступ pthread_mutex_lock(&q->lock); q->is_alive = 0; pthread_cond_broadcast(&q->cond); pthread_mutex_unlock(&q->lock); } Отлично, у нас есть всё, что нам надо. Как использовать это? Нужно: запустить несколько потоков-«производителей» и несколько «потребителей» придумать структуру данных для задания Пример: (производитель — главный поток, потребители — 2 потока) // это поток-потребитель void * consumer_thread (void *arg) { struct producer_consumer_queue *q = (typeof(q))arg; for (;;) { void *data = dequeue(q); // это сигнал, что очередь окончена if (!data) break; // значит, пора закрывать поток char *str = (char *)data; // тут наша обработка данных printf ("consuming: %s\n", str); sleep(2); printf ("consumed: %s\n", str); free(str); } return 0; } int main () { pthread_t consumer_threads[2]; void *res = 0; char *in = NULL; size_t sz; // создадим очередь: struct producer_consumer_queue *q = producer_consumer_queue_create(); // и потоки-«потребители» pthread_create(&consumer_threads[0], 0, consumer_thread, (void *)q); pthread_create(&consumer_threads[1], 0, consumer_thread, (void *)q); // главный цикл // получаем данные с клавиатуры: while (getline(&in, &sz, stdin) > 0) { enqueue(in, q); in = NULL; } producer_consumer_queue_stop(q); if (pthread_join(consumer_threads[0], &res) || pthread_join(consumer_threads[1], &res)) perror("join"); return (long)res; } Это реализация задачи с "бесконечной" очередью. На практике же иногда (или почт всегда?) более полезно ограничить размер очереди и таким образом сбалансировать скорость производителей, иногда переводя их в спящее состояние, с возможностями потребителей. Для этого немного изменим нашу producer_consumer_queue struct producer_consumer_queue { struct producer_consumer_queue_item *head, *tail; // head == tail == 0, если очередь пуста pthread_mutex_t lock; // мьютекс для всех манипуляций с очередью pthread_cond_t condp; // этот cond "сигналим", когда очередь стала НЕ ПУСТОЙ pthread_cond_t condc; // этот cond "сигналим", когда в очереди ПОЯВИЛОС СВОБОДНОЕ МЕСТО int is_alive; // показывает, не закончила ли очередь свою работу int max, cnt, // максимальный размер очереди и число заданий в ней pqcnt; // количество производителей, ждущих свободного места в очереди }; Добавляем pthread_cond_t condc для "засыпания/пробуждения" потоков производителей их счетчик в очереди на отправку сообщения и пару переменных, содержащих максимальный размер очереди и текущее количество заданий в ней. Соответственно меняются функции для постановки задания в очередь (enqueue), выборк его из очереди (dequeue), инициализации очереди (producer_consumer_queue_create) и ее остановки (producer_consumer_queue_stop): void enqueue (void *data, struct producer_consumer_queue *aq) { volatile struct producer_consumer_queue *q = aq; // упакуем задание в новую структуру struct producer_consumer_queue_item *p = (typeof(p))malloc(sizeof(*p)); p->data = data; p->next = 0; // получим "эксклюзивный" доступ к очереди заданий pthread_mutex_lock(&aq->lock); // проверим не переполнена ли она if (q->max <= q->cnt) { q->pqcnt++; asm volatile ("" : : : "memory"); // зафиксируем изменения очереди в памяти // будем ждать пока потребители ее слегка не опустошат while(q->max <= q->cnt & q->is_alive) pthread_cond_wait(&aq->condc, &aq->lock); q->pqcnt--; asm volatile ("" : : : "memory"); } // ... и добавим новое задание туда: if (q->tail) q->tail->next = p; else { q->head = p; // очередь была пуста, а теперь нет -- надо разбудить потребителей pthread_cond_broadcast(&aq->condp); } q->tail = p; q->cnt++; asm volatile ("" : : : "memory"); // разрешаем доступ всем снова pthread_mutex_unlock(&aq->lock); } void * dequeue(struct producer_consumer_queue *aq) { volatile struct producer_consumer_queue *q = aq; // получаем эксклюзивный доступ к очереди: pthread_mutex_lock(&aq->lock); if (q->pqcnt && q->max > q->cnt) // в очереди есть место, а кто-то спит, разбудим их pthread_cond_broadcast(&aq->condc); while (!q->head && q->is_alive) { // очередь пуста, делать нечего, ждем... pthread_cond_wait(&aq->condp, &aq->lock); // wait разрешает доступ другим на время ожидания } // запоминаем текущий элемент или 0, если очередь умерла struct producer_consumer_queue_item *p = q->head; if (p) { // и удаляем его из очереди q->head = q->head->next; if (!q->head) q->tail = q->head; q->cnt--; asm volatile ("" : : : "memory"); // зафиксируем изменения очереди в памяти // разбудим поставщиков в их очереди pthread_cond_broadcast(&aq->condc); } // возвращаем эксклюзивный доступ другим участникам pthread_mutex_unlock(&aq->lock); // отдаём данные void *data = p ? p->data : 0; // 0 означает, что данных больше не будет free(p); return data; } struct producer_consumer_queue * producer_consumer_queue_create(int max) { struct producer_consumer_queue *q = (typeof(q))malloc(sizeof(*q)); q->head = q->tail = 0; q->is_alive = 1; q->max = max; q->cnt = 0; q->pqcnt = 0; pthread_mutex_init(&q->lock, 0); pthread_cond_init(&q->condc, 0); pthread_cond_init(&q->condp, 0); return q; } // И процедура для закрытия очереди: void producer_consumer_queue_stop(struct producer_consumer_queue *aq) { volatile struct producer_consumer_queue *q = aq; // для обращения к разделяемым переменным необходим эксклюзивный доступ pthread_mutex_lock(&aq->lock); q->is_alive = 0; asm volatile ("" : : : "memory"); pthread_cond_broadcast(&aq->condc); pthread_cond_broadcast(&aq->condp); pthread_mutex_unlock(&aq->lock); } Здесь же показан memory barrier (asm volatile ("" : : : "memory");), использование которого запрещает компилятору переупорядочивать операции чтения-записи из RAM. Данная реализация не обеспечивает "упорядоченность" производителей, ожидающих свое очереди для отправки сообщения. Т.е. поток производитель, "заснувший" первым из-за отсутствия свободного места в очереди не обязательно проснется первым. Если такое поведение нас не устраивает, то придется внести некоторые изменения наши данные, прежде всего добавив очередь поставщиков из структур producer_queue_item (которая будет частью структуры producer_consumer_queue. Получаем следующие структуры данных: // объявляем структуру данных для одного задания struct producer_consumer_queue_item { struct producer_consumer_queue_item *next; // здесь идут собственно данные. вы можете поменять этот кусок, // использовав структуру, более специфичную для вашей задачи void *data; }; // струкура данных для спящего (ждущего свободного места) потока-производителя struct producer_queue_item { struct producer_queue_item *next; struct producer_consumer_queue_item *item; // данные для которых нет места pthread_cond_t cond; // этот cond "сигналим", когда в очереди появилось место #if DEBUG pid_t tid; // linux thread id for debug print int signaled; // индикатор "побудки" for debug print #endif }; // объявляем очередь данных с дополнительными структурами для синхронизации. // в этой очереди будут храниться произведённые, но ещё не потреблённые задания. struct producer_consumer_queue { struct producer_consumer_queue_item *head, *tail; // head == tail == 0, если очередь пуста pthread_mutex_t lock; // мьютекс для всех манипуляций с очередью pthread_cond_t cond; // этот cond "сигналим", когда очередь стала НЕ ПУСТОЙ int is_alive; // показывает, не закончила ли очередь свою работу int max, cnt; // максимальный размер очереди и число заданий в ней // очередь потоков-производителей, ждущих свободного места для своих данных struct producer_queue_item *pqhead, *pqtail; }; и реализацию основных функций: void enqueue (void *data, struct producer_consumer_queue *q) { volatile struct producer_consumer_queue *vq = q; // упакуем задание в новую структуру struct producer_consumer_queue_item *p = (typeof(p))malloc(sizeof(*p)); p->data = data; p->next = 0; // получим "эксклюзивный" доступ к очереди заданий pthread_mutex_lock(&q->lock); #if DEBUG printf("%ld (cnt: %d) ---> %s", (long)gettid(), vq->cnt, (char *)(p->data)); #endif // ... и добавим новое задание туда: if (vq->max <= vq->cnt || vq->pqtail) {// производитель должен ждать #if DEBUG if (vq->cnt < vq->max) { puts("========================"); print_queue(q, 0); puts("========================"); } #endif struct producer_queue_item *pq = (typeof(pq))malloc(sizeof(*pq)); pthread_cond_init(&pq->cond, 0); // cond по которому его разбудят pq->next = 0; pq->item = p; // сохраним данные на время сна #if DEBUG pq->tid = gettid(); #endif // поместим себя в очередь спящих производителей if (vq->pqtail) vq->pqtail->next = pq; else vq->pqhead = pq; vq->pqtail = pq; asm volatile ("" : : : "memory"); // зафиксируем изменения очереди в памяти #if DEBUG int at = 0; // счетчик циклов пробуждения #endif do { // пойдем спать до появления свободного места в очереди данных #if DEBUG printf ("%ld prod cond wait (cnt: %d at: %d) %s", (long)gettid(), vq->cnt, at++, (char *)(p->data)); pq->signaled = 0; #endif pthread_cond_wait(&pq->cond, &q->lock); } while(vq->max <= vq->cnt && vq->is_alive); // проснулись и владеем очередью /* Вот тонкий момент. Порядок активизации потоков не определен, а нам надо соблюдать очередность данных. Поэтому переустановим локальные переменные из очереди, хотя это могут быть данные, положенные туда другим потоком. */ #if DEBUG if (pq != vq->pqhead) { printf ("BAAAD %ld (cnt: %d at: %d) %s", (long)gettid(), vq->cnt, at, (char *)(p->data)); print_queue(q, 0); if (vq->is_alive) exit(1); // совсем плохо, такого быть не должно else puts("CONTINUE"); } #endif pq = vq->pqhead; // в любом случае берем голову очереди производителей if ((vq->pqhead = pq->next) == 0) // и удаляем ее vq->pqtail = 0; asm volatile ("" : : : "memory"); p = pq->item; free(pq); #if DEBUG printf ("%ld prod enqueued after wait (cnt: %d at: %d) %s", (long)gettid(), vq->cnt, at, (char *)(p->data)); #endif } // вот тут реально кладем data в очередь для потребителей if (vq->tail) vq->tail->next = p; else { vq->head = p; // очередь была пуста, а теперь нет -- надо разбудить потребителей pthread_cond_broadcast(&q->cond); } vq->tail = p; vq->cnt++; asm volatile ("" : : : "memory"); // сбросим изменения очереди в память // разрешаем доступ всем снова pthread_mutex_unlock(&q->lock); } #if DEBUG #define cond_signal_producer(q) ({ \ if ((q)->pqhead) { \ (q)->pqhead->signaled = 1; \ pthread_cond_signal(&(q)->pqhead->cond); \ } \ }) #else #define cond_signal_producer(q) ({ \ if ((q)->pqhead) \ pthread_cond_signal(&(q)->pqhead->cond); \ }) #endif void * dequeue(struct producer_consumer_queue *q) { volatile struct producer_consumer_queue *vq = q; // получаем эксклюзивный доступ к очереди: pthread_mutex_lock(&q->lock); // если есть спящие производители, то разбудим первого cond_signal_producer(vq); while (!vq->head && vq->is_alive) { // очередь пуста, делать нечего, ждем... pthread_cond_wait(&q->cond, &q->lock); // wait разрешает доступ другим на время ожидания } // запоминаем текущий элемент или 0, если очередь умерла struct producer_consumer_queue_item *p = vq->head; if (p) { // и удаляем его из очереди vq->head = vq->head->next; if (!vq->head) vq->tail = vq->head; vq->cnt--; asm volatile ("" : : : "memory"); // сбросим изменения очереди в память // разбудим первого поставщика в их очереди cond_signal_producer(vq); } // возвращаем эксклюзивный доступ другим участникам pthread_mutex_unlock(&q->lock); // отдаём данные void *data = p ? p->data : 0; // 0 означает, что данных больше не будет // согласно 7.20.3.2/2, можно не проверять на 0 free(p); return data; } struct producer_consumer_queue * producer_consumer_queue_create(int max) { struct producer_consumer_queue *q = (typeof(q))malloc(sizeof(*q)); q->head = q->tail = 0; q->pqhead = q->pqtail = 0; q->is_alive = 1; q->max = max; q->cnt = 0; pthread_mutex_init(&q->lock, 0); pthread_cond_init(&q->cond, 0); return q; } // И процедура для закрытия очереди: void producer_consumer_queue_stop(struct producer_consumer_queue *q) { volatile struct producer_consumer_queue *vq = q; // для обращения к разделяемым переменным необходим эксклюзивный доступ pthread_mutex_lock(&q->lock); vq->is_alive = 0; pthread_cond_broadcast(&q->cond); // разбудим потребителей volatile struct producer_queue_item *pq; for (pq = vq->pqhead; pq; pq = pq->next) { #if DEBUG pq->signaled = 1; asm volatile ("" : : : "memory"); #endif // будим каждого ждущего производителя pthread_cond_signal((pthread_cond_t *)&pq->cond); } pthread_mutex_unlock(&q->lock); } Все три программы (pq1.c, pq2.c и pq3.c) вместе с функцией gettid() находятся в http://pastebin.com/E23r9DZk . Для экспериментов скопируйте их в разные файлы и компилируйте, например, gcc pq3.c -pthread gettid.o

Ответ 3



Реализация на C#, библиотека Dataflow Ещё одной альтернативой является использование Майкрософтовской библиотеки Dataflow которая собственно и создана для того, чтобы управлять потоками данных. Для использовани кода в примерах, вам нужно подключить NuGet-пакет Microsoft.Tpl.Dataflow. Класс BufferBlock практически является готовым producer/consumer'ом, но, в отличие от блокирующего интерфейса BlockingCollection, он имеет async-интерфейс! Для асинхронного добавления задания поставщик может воспользоваться SendAsync. Асинхронно добавление нужно, так как очередь может быть ограниченной длины, а значит, добавление должно будет дожидаться наличия свободного места! По окончанию добавления нужно вызвать Complete. async Task ProduceSingle(ITargetBlock queue, int howmuch) { Random r = new Random(); while (howmuch-- > 0) { // эмулируем длительную работу по подготовке следующего задания // длительность выбираем случайно, чтобы задания приходили в // непредсказуемые моменты времени await Task.Delay(1000 * r.Next(1, 3)); var v = string.Format("automatic {0}", r.Next(1, 10)); await queue.SendAsync(v); } queue.Complete(); } Если у вас несколько поставщиков, закрывать очередь нужно лишь когда они все отработают: async Task Produce1(ITargetBlock queue, int howmuch) { Random r = new Random(); while (howmuch-- > 0) { await Task.Delay(1000 * r.Next(1, 3)); var v = string.Format("automatic {0}", r.Next(1, 10)); await queue.SendAsync(v); } } // функция Console.ReadLine() -- блокирующая, поэтому выполняем её асинхронно // (иначе она заблокирует вызывающий поток) // у Console нет async-интерфейса. Task ReadConsole() { // блокирующую функцию выгружаем в thread pool return Task.Run(() => Console.ReadLine()); } async Task Produce2(ITargetBlock queue) { string s; while ((s = await ReadConsole()).Length != 0) await queue.SendAsync("manual " + s); } async Task ProduceAll(ITargetBlock queue) { var p1 = Produce1(queue, 20); var p2 = Produce2(queue); await Task.WhenAll(p1, p2); queue.Complete(); } Теперь, потребитель. Если потребитель лишь один, всё просто: async Task ConsumeSingle(ISourceBlock queue) { while (await queue.OutputAvailableAsync()) Console.WriteLine(await queue.ReceiveAsync()); } Для случая нескольких потребителей использовать ReceiveAsync — неверно, так как задани может быть взято из очереди другим потребителем! Функции TryReceiveAsync тоже нету, поэтому после асинхронного выяснения того, что очередь не пуста, используем TryReceive: async Task ConsumeCooperative(IReceivableSourceBlock queue, int number) { Random r = new Random(); while (await queue.OutputAvailableAsync()) { string v; // в этой точке данные могут быть уже уйти другому потребителю if (!queue.TryReceive(out v)) continue; // продолжаем ждать // цветной вывод и прочие плюшки // мне лень синхронизировать вывод на консоль, хотя конечно это разделяемый ресурс if (Console.CursorLeft != 0) Console.WriteLine(); var savedColor = Console.ForegroundColor; Console.ForegroundColor = (ConsoleColor)(number + 8); Console.WriteLine(string.Format("{0}[{1}]: {2}", new string(' ', number * 4), number, v)); Console.ForegroundColor = savedColor; // симулируем длительную обработку результата клиентом await Task.Delay(1000 * r.Next(1, 3)); } } Task ConsumeAll(IReceivableSourceBlock queue) { var c1 = ConsumeCooperative(queue, 1); var c2 = ConsumeCooperative(queue, 2); return Task.WhenAll(c1, c2); } Осталась обвязка: class Program { static void Main(string[] args) { new Program().RunAll().Wait(); } async Task RunAll() { BufferBlock queue = new BufferBlock(); var p = ProduceAll(queue); var c = ConsumeAll(queue); await Task.WhenAll(p, c, queue.Completion); } // остальные методы } В своей статье Async Producer/Consumer Queue using Dataflow Stephen Cleary предлагае другой подход, более в духе библиотеки Dataflow. В ней заложена симметрия между блоками-источникам (ISourceBlock), блоками-приёмниками (ITargetBlock), и блоками-преобразователями (IPropagatorBlock). В соответствии с этой идеологией, мы применяем для поставщика блок-приёмник ActionBlock: Task Consume2(ISourceBlock queue, int number) { var consumerOptions = new ExecutionDataflowBlockOptions { BoundedCapacity = 1 }; var consumer = new ActionBlock(v => ConsumeImpl2(v, number), consumerOptions); var linkOptions = new DataflowLinkOptions { PropagateCompletion = true }; queue.LinkTo(consumer, linkOptions); return consumer.Completion; } void ConsumeImpl2(string v, int number) { Console.WriteLine(string.Format("[{0}]: {1}", number, v)); Thread.Sleep(1500); } Task ConsumeAll2(ISourceBlock queue) { var c1 = Consume2(queue, 1); var c2 = Consume2(queue, 2); return Task.WhenAll(c1, c2); } Для чего нужно BoundedCapacity = 1? Дел в том, что по умолчанию ActionBlock имее «неограниченную» ёмкость, и таким образом за раз потребит все данные из очереди. Таким образом, если мы ввели ограничение на объём очереди queue = new BufferBlock(new DataflowBlockOptions { BoundedCapacity = 20 }); то данные будут всё равно накапливаться в ActionBlock'е. Чтобы роль хранилища исполнялас BufferBlock'ом, а потребителя — ActionBlock, и нужно ограничить его объём. Заметьте также, что ограничение ёмкости ActionBlock'а позволяет библиотеке Dataflow балансировать нагрузку, отправляя данные свободному блоку. Заметьте, что в этом случае то, в каком контексте (пул потоков? выделенный поток? исполняется ActionBlock, управляется не через стандартный механизм async/await, а посредством TaskScheduler'а в настройках. Подход со стыковкой блоков выполнения может быстро стать слишком тяжеловесным, поэтому я советовал бы использовать его лишь для достаточно сложных задач.

Ответ 4



Реализация на C#, библиотека Dataflow ("инверсный" алгоритм) Отличие данного алгоритма от приведенного VladD "прямого" - в том, что используется очередь потребителей вместо очереди элементов. Это позволяет избавиться от цикла приема в потребителе - ценой появления знания списк потребителей поставщиком данных. Иными словами, в такой конфигурации потребители выходят не активными - а пассивными. Кроме того, сам алгоритм получается очень простым. Потребитель в таком алгоритме не имеет никакого алгоритма, это лишь класс (или интерфейс либо вовсе делегат), в котором можно вызвать метод: class Consumer { public void Consume(string str) { Console.WriteLine(str); } } На стороне поставщика алгоритм немного сложнее: BufferBlock consumers = new BufferBlock(); public async void SendToConsumer(string str) { var consumer = await consumers.ReceiveAsync(); try { consumer.Consume(str); } finally { consumers.Post(consumer); } } async Task Produce(int howmuch) { Random r = new Random(); while (howmuch-- > 0) { // эмулируем длительную работу по подготовке следующего задания // длительность выбираем случайно, чтобы задания приходили в // непредсказуемые моменты времени await Task.Delay(1000 * r.Next(1, 3)); var v = string.Format("automatic {0}", r.Next(1, 10)); SendToConsumer(v); } } К сожалению, это еще не все. Дело в том, что в таком виде любое исключение, возникшее в методе Consume, обрушит программу. Поэтому надо сделать одну из двух вещей: добавить в метод контракт класса Consumer требование обязательной обработки исключений; или же установить глобальный обработчик неперехваченных исключений. А еще лучше - воспользоваться сразу обоими вариантами. И еще один важный момент. Пассивность потребителя означает, в частности, что потребител будет запущен в контексте синхронизации поставщика. Иногда это может быть нежелательным - в таком случае в код SendToConsumer следует добавить вызов Task.Run. Иногда же, напротив, производителю нужно дождаться полной обработки порции данных В таком случае надо сменить возвращаемое значение у метода SendToConsumer с void на Task.

четверг, 7 марта 2019 г.

Почему всегда threadPoolExecutor.getActiveCount() <= maximumPoolSize/2?

Есть очередь задач, создана следующим образом:
LinkedBlockingQueue queue = new LinkedBlockingQueue(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(maximumPoolSize, maximumPoolSize, 50000L, TimeUnit.MILLISECONDS, queue);
Значение maximumPoolSize равно 200. Во время работы в queue попадает большое количество потоков (больше тысячи), но значение, возвращаемое методом threadPoolExecutor.getActiveCount() всегда меньше либо равно 100. Например, значения threadPoolExecutor.getActiveCount() и queue.size() логируются следующим образом:
logger.debug("Активные потоки: " + threadPoolExecutor.getActiveCount() + ". В очереди " + queue.size() + " потоков.");
и в итоге мы получаем следующую картину:
Активные потоки: 1. В очереди 0 потоков. Активные потоки: 2. В очереди 0 потоков. Активные потоки: 3. В очереди 0 потоков. Активные потоки: 4. В очереди 0 потоков. Активные потоки: 5. В очереди 0 потоков. ... Активные потоки: 86. В очереди 0 потоков. Активные потоки: 87. В очереди 1 потоков. Активные потоки: 88. В очереди 1 потоков. Активные потоки: 89. В очереди 1 потоков. Активные потоки: 90. В очереди 1 потоков. ... Активные потоки: 99. В очереди 1 потоков. Активные потоки: 100. В очереди 1 потоков. Активные потоки: 100. В очереди 2 потоков. Активные потоки: 100. В очереди 3 потоков. Активные потоки: 100. В очереди 4 потоков. ... Активные потоки: 100. В очереди 1874 потоков. Активные потоки: 100. В очереди 1875 потоков. Активные потоки: 100. В очереди 1876 потоков. Активные потоки: 100. В очереди 1877 потоков. Активные потоки: 100. В очереди 1878 потоков. Активные потоки: 100. В очереди 1879 потоков. Активные потоки: 100. В очереди 1880 потоков. Активные потоки: 100. В очереди 1881 потоков. Активные потоки: 100. В очереди 1882 потоков. Активные потоки: 100. В очереди 1883 потоков.
В документации написано, что метод threadPoolExecutor.getActiveCount() возвращает приблизительное количество потоков с активно выполняющимися задачами. Но почему максимальный порог этого приблизительного значения тут равен maximumPoolSize/2?
P.S. Это логи не с моего компьютера, у меня такую ситуацию воспроизвести не удалось - у меня в данном случае количество активных потоков равняется 200, как и ожидалось. Может ли это быть зависимость от количества процессоров/количества ядер процессоров/какой-либо конфигурации ПО?


Ответ

"Есть очередь задач, создана следующим образом: ... new ThreadPoolExecutor(maximumPoolSize, maximumPoolSize, .."
Вы уверены, что maximumPoolSize - это одна переменная, и она имеет одно значение? Полагаю, что причина в том, что corePoolSize равен 100, а maximumPoolSize равен 200 и у Вас происходит следующее?
При добавлении нового потока в пул выполняет следующие действия последовательно:
Проверяет достигнут ли предел потоков, задаваемых переменной corePoolSize если можно еще запустить поток - запускает его. Если количество потоков равно или больше corePoolSize переходит к следующему шагу. Проверяет не заполнена ли очередь и если в ней еще есть место добавляет туда поток. Если очередь заполнена переходит к следующему шагу. Пытается увеличить количество работающих потоков до значения maximumPoolSize если можно добавить еще поток, то он добавляется и в нем и запускается задание. Если количество работающих потоков равно maximumPoolSize то выбрасывается исключение java.util.concurrent.RejectedExecutionException или вызывается перехватчик отклонения задачи определяемый программистом.
В вашем случае в виду того, что очередь увеличивается без ограничений к третьему шагу алгоритм никогда не переходит. Но можно «изменить» порядок следования шагов 2 и 3. Для этого необходимо создать очередь в которой мы переопределим вызываемый ThreadPoolExecutor метод offer так, чтобы он возвращал всегда false (очередь заполнена). Теперь нам нужно задать собственный перехватчик RejectedExecutionHandler. В перехватчике мы добавляем полученный поток в очередь. Пример изложенного выше в виде кода:
LinkedBlockingQueue queue = new LinkedBlockingQueue (100){ public boolean offer(Runnable e) {return false;} };
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100, 200, 1, TimeUnit.MINUTES, queue, new RejectedExecutionHandler() { public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { try { executor.getQueue().put(r); } catch (InterruptedException e) { e.printStackTrace(); } } });

четверг, 4 октября 2018 г.

Ревью класса для очереди команд

Мне потребовалась очередь команд, где множество потоков может добавлять команды на выполнение и один поток по очереди их выполняет. Тк я не знаю какие-либо стандартные реализации подобного, то пришлось делать самому. Можете оценить мой класс для этого, тк у меня сомнения на счет его качества, хотя он работает. И если кто-либо даст ссылку на реализации подобного, то тоже будет приятно.
public class ConcurrentCommandsQueue : IDisposable { private readonly object _lockForSyncExecuting; private readonly Queue _commandQueue;
private readonly Action _executeCommand; private readonly Action _onStartCommandsExecution; private readonly Action _onEndCommandsExecution;
private volatile bool _isDisposed; private volatile bool _isExecuting; private volatile Task _taskForAction;
public bool IsExecuting { get { return _isExecuting; } }
public ConcurrentCommandsQueue(Action executeCommand) : this(executeCommand, null, null) { } public ConcurrentCommandsQueue(Action executeCommand, Action onStartCommandsExecution, Action onEndCommandsExecution) { _lockForSyncExecuting = new object(); _commandQueue = new Queue(10);
_isDisposed = false; _isExecuting = false; _executeCommand = executeCommand; _onStartCommandsExecution = onStartCommandsExecution; _onEndCommandsExecution = onEndCommandsExecution; _taskForAction = new TaskCompletionSource().Task; }
private void ExecuteCommands() { if (_onStartCommandsExecution != null) _onStartCommandsExecution(); T command = default(T); while (true) { lock (_lockForSyncExecuting) { if (_commandQueue.Count == 0) { _isExecuting = false; break; } command = _commandQueue.Dequeue(); } _executeCommand(command); } if (_onEndCommandsExecution != null) _onEndCommandsExecution(command); }
public void AddCommand(T command) { if (_isDisposed) throw new ObjectDisposedException(GetType().ToString());
lock (_lockForSyncExecuting) { if (_isDisposed) return; _commandQueue.Enqueue(command); if (!_isExecuting) { _isExecuting = true; _taskForAction = Task.Run(new Action(ExecuteCommands)); } } } public void ClearCommandQueue() { if (_isDisposed) throw new ObjectDisposedException(GetType().ToString());
lock (_lockForSyncExecuting) _commandQueue.Clear(); } public void Dispose() { if (_isDisposed) return;
_isDisposed = true; lock (_lockForSyncExecuting) _commandQueue.Clear(); if (_taskForAction != null) _taskForAction.Wait(); } }


Ответ

У вас получился код в стиле .NET 1.0, который слегка эволюционировал с приходом лямбд (Delegate механически заменён на Action) и задач (new Thread().Start() механически заменён на Task.Run()), но по сути совершенно не изменился: вы TPL с async/await толком не используете, а существование concurrent коллекций вообще упустили из виду.
В .NET 4.5 это всё ненужно в принципе. Если вам нужно выполнение задач строго в одном потоке, то можно просто кидать задачи в однопоточный планировщик задач (task scheduler). Собственно, на этой первой строчке весь код и заканчивается. Если нужно выполнение кода перед всеми операциями и после всех операций, то код перед и после этой строчки и пишется. Отмечу, что при этой однострочной реализации вы ещё имеете бонусом возможность использования токена отмены (cancellation token) и прочих радостей жизни.

Рассмотрим пример. Допустим, надо выполнить две очереди задач: в каждой очереди задачи выполняются последовательно, но очереди выполняются параллельно. Создаём два планировщика задач с ограничением параллелизма, затем создаём задачи с указанием нужного нам планировщика.
class Program { static readonly Random _rnd = new Random(); static readonly LimitedConcurrencyTaskScheduler _schedulerFoo = new LimitedConcurrencyTaskScheduler(1); static readonly LimitedConcurrencyTaskScheduler _schedulerBar = new LimitedConcurrencyTaskScheduler(1);
static void Main () => new Program().Run().Wait();
async Task Run () { Task queueFoo = RunQueue("Foo", _schedulerFoo, Enumerable.Range(0, 3).Select(i => (Action)(() => Foo("Foo")))); Task queueBar = RunQueue("Bar", _schedulerBar, Enumerable.Range(0, 3).Select(i => (Action)(() => Foo("Bar")))); await Task.WhenAll(queueFoo, queueBar); Console.WriteLine("Done!"); Console.ReadKey(); }
async Task RunQueue (string name, TaskScheduler scheduler, IEnumerable commands) { Console.WriteLine($"{name}: Start"); await Task.WhenAll(commands.Select(c => RunTask(c, scheduler))); Console.WriteLine($"{name}: Finish"); }
async Task RunTask (Action task, TaskScheduler scheduler) { await Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler); }
void Foo (string name) { int timeout = _rnd.Next(200); Console.WriteLine($"{name}: Start {timeout}"); Thread.Sleep(timeout); Console.WriteLine($"{name}: Finish {timeout}"); } }
Пример вывода:
Foo: Start Bar: Start Foo: Start 165 Bar: Start 50 Bar: Finish 50 Bar: Start 39 Bar: Finish 39 Bar: Start 115 Foo: Finish 165 Foo: Start 116 Bar: Finish 115 Bar: Finish Foo: Finish 116 Foo: Start 0 Foo: Finish 0 Foo: Finish Done!
Хотя в этом примере все задачи и кидаются одновременно, задачи можно добавлять в любой момент. Правда тогда смысл "начала" и "конца" несколько теряется. Если вы объясните, что вы собираетесь с ними делать, тогда можно будет добавить.
Здесь я воспользовался планировщиком LimitedConcurrencyTaskScheduler из примеров на MSDN: Samples for Parallel Programming with the .NET Framework (статья с описанием: ParallelExtensionsExtras Tour - #7 - Additional TaskSchedulers).
///

/// Provides a task scheduler that ensures a maximum concurrency level while running on top of the ThreadPool. /// Source: http://code.msdn.microsoft.com/ParExtSamples /// Documentation: http://blogs.msdn.com/b/pfxteam/archive/2010/04/09/9990424.aspx /// License: MS-LPL /// public class LimitedConcurrencyTaskScheduler : TaskScheduler { [ThreadStatic] private static bool _currentThreadIsProcessingItems;
private readonly int _maxDegreeOfParallelism; private readonly LinkedList _tasks = new LinkedList(); // protected by lock(_tasks) private int _delegatesQueuedOrRunning = 0; // protected by lock(_tasks)
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the specified degree of parallelism. /// The maximum degree of parallelism provided by this scheduler. public LimitedConcurrencyTaskScheduler (int maxDegreeOfParallelism) { if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); _maxDegreeOfParallelism = maxDegreeOfParallelism; }
/// Gets the maximum concurrency level supported by this scheduler. public override sealed int MaximumConcurrencyLevel => _maxDegreeOfParallelism;
protected override sealed IEnumerable GetScheduledTasks () { bool lockTaken = false; try { Monitor.TryEnter(_tasks, ref lockTaken); if (lockTaken) return _tasks.ToArray(); else throw new NotSupportedException(); } finally { if (lockTaken) Monitor.Exit(_tasks); } }
protected override sealed void QueueTask (Task task) { // Add the task to the list of tasks to be processed. If there aren't enough // delegates currently queued or running to process tasks, schedule another. lock (_tasks) { _tasks.AddLast(task); if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) { ++_delegatesQueuedOrRunning; NotifyThreadPoolOfPendingWork(); } } }
/// Informs the ThreadPool that there's work to be executed for this scheduler. private void NotifyThreadPoolOfPendingWork () { ThreadPool.UnsafeQueueUserWorkItem(_ => { // Note that the current thread is now processing work items. // This is necessary to enable inlining of tasks into this thread. _currentThreadIsProcessingItems = true; try { // Process all available items in the queue. while (true) { Task item; lock (_tasks) { // When there are no more items to be processed, // note that we're done processing, and get out. if (_tasks.Count == 0) { --_delegatesQueuedOrRunning; break; } // Get the next item from the queue item = _tasks.First.Value; _tasks.RemoveFirst(); } // Execute the task we pulled out of the queue TryExecuteTask(item); } } finally { // We're done processing items on the current thread _currentThreadIsProcessingItems = false; } }, null); }
protected override sealed bool TryExecuteTaskInline (Task task, bool taskWasPreviouslyQueued) { // If this thread isn't already processing a task, we don't support inlining if (!_currentThreadIsProcessingItems) return false; // If the task was previously queued, remove it from the queue if (taskWasPreviouslyQueued) TryDequeue(task); // Try to run the task. return TryExecuteTask(task); }
protected override sealed bool TryDequeue (Task task) { lock (_tasks) return _tasks.Remove(task); } }

Ещё можно воспользоваться BlockingCollection<> с ConcurrentQueue<> внутри. По окончанию добавления надо будет не забыть вызвать CompleteAdding
Ещё есть TPL Dataflow. Там параллелизмы и прочие настраиваются без кастомных планировщиков.
Можно ещё Rx добавить в качестве вишенки на торте.
Вариантов море.
Вообще, сейчас ещё @Vlad прибежит, расскажет про хитрые мозговыносящие способы решить эту задачу. :)

Ревью класса для очереди команд

Мне потребовалась очередь команд, где множество потоков может добавлять команды на выполнение и один поток по очереди их выполняет. Тк я не знаю какие-либо стандартные реализации подобного, то пришлось делать самому. Можете оценить мой класс для этого, тк у меня сомнения на счет его качества, хотя он работает. И если кто-либо даст ссылку на реализации подобного, то тоже будет приятно.
public class ConcurrentCommandsQueue : IDisposable { private readonly object _lockForSyncExecuting; private readonly Queue _commandQueue;
private readonly Action _executeCommand; private readonly Action _onStartCommandsExecution; private readonly Action _onEndCommandsExecution;
private volatile bool _isDisposed; private volatile bool _isExecuting; private volatile Task _taskForAction;
public bool IsExecuting { get { return _isExecuting; } }
public ConcurrentCommandsQueue(Action executeCommand) : this(executeCommand, null, null) { } public ConcurrentCommandsQueue(Action executeCommand, Action onStartCommandsExecution, Action onEndCommandsExecution) { _lockForSyncExecuting = new object(); _commandQueue = new Queue(10);
_isDisposed = false; _isExecuting = false; _executeCommand = executeCommand; _onStartCommandsExecution = onStartCommandsExecution; _onEndCommandsExecution = onEndCommandsExecution; _taskForAction = new TaskCompletionSource().Task; }
private void ExecuteCommands() { if (_onStartCommandsExecution != null) _onStartCommandsExecution(); T command = default(T); while (true) { lock (_lockForSyncExecuting) { if (_commandQueue.Count == 0) { _isExecuting = false; break; } command = _commandQueue.Dequeue(); } _executeCommand(command); } if (_onEndCommandsExecution != null) _onEndCommandsExecution(command); }
public void AddCommand(T command) { if (_isDisposed) throw new ObjectDisposedException(GetType().ToString());
lock (_lockForSyncExecuting) { if (_isDisposed) return; _commandQueue.Enqueue(command); if (!_isExecuting) { _isExecuting = true; _taskForAction = Task.Run(new Action(ExecuteCommands)); } } } public void ClearCommandQueue() { if (_isDisposed) throw new ObjectDisposedException(GetType().ToString());
lock (_lockForSyncExecuting) _commandQueue.Clear(); } public void Dispose() { if (_isDisposed) return;
_isDisposed = true; lock (_lockForSyncExecuting) _commandQueue.Clear(); if (_taskForAction != null) _taskForAction.Wait(); } }


Ответ

У вас получился код в стиле .NET 1.0, который слегка эволюционировал с приходом лямбд (Delegate механически заменён на Action) и задач (new Thread().Start() механически заменён на Task.Run()), но по сути совершенно не изменился: вы TPL с async/await толком не используете, а существование concurrent коллекций вообще упустили из виду.
В .NET 4.5 это всё ненужно в принципе. Если вам нужно выполнение задач строго в одном потоке, то можно просто кидать задачи в однопоточный планировщик задач (task scheduler). Собственно, на этой первой строчке весь код и заканчивается. Если нужно выполнение кода перед всеми операциями и после всех операций, то код перед и после этой строчки и пишется. Отмечу, что при этой однострочной реализации вы ещё имеете бонусом возможность использования токена отмены (cancellation token) и прочих радостей жизни.

Рассмотрим пример. Допустим, надо выполнить две очереди задач: в каждой очереди задачи выполняются последовательно, но очереди выполняются параллельно. Создаём два планировщика задач с ограничением параллелизма, затем создаём задачи с указанием нужного нам планировщика.
class Program { static readonly Random _rnd = new Random(); static readonly LimitedConcurrencyTaskScheduler _schedulerFoo = new LimitedConcurrencyTaskScheduler(1); static readonly LimitedConcurrencyTaskScheduler _schedulerBar = new LimitedConcurrencyTaskScheduler(1);
static void Main () => new Program().Run().Wait();
async Task Run () { Task queueFoo = RunQueue("Foo", _schedulerFoo, Enumerable.Range(0, 3).Select(i => (Action)(() => Foo("Foo")))); Task queueBar = RunQueue("Bar", _schedulerBar, Enumerable.Range(0, 3).Select(i => (Action)(() => Foo("Bar")))); await Task.WhenAll(queueFoo, queueBar); Console.WriteLine("Done!"); Console.ReadKey(); }
async Task RunQueue (string name, TaskScheduler scheduler, IEnumerable commands) { Console.WriteLine($"{name}: Start"); await Task.WhenAll(commands.Select(c => RunTask(c, scheduler))); Console.WriteLine($"{name}: Finish"); }
async Task RunTask (Action task, TaskScheduler scheduler) { await Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler); }
void Foo (string name) { int timeout = _rnd.Next(200); Console.WriteLine($"{name}: Start {timeout}"); Thread.Sleep(timeout); Console.WriteLine($"{name}: Finish {timeout}"); } }
Пример вывода:
Foo: Start Bar: Start Foo: Start 165 Bar: Start 50 Bar: Finish 50 Bar: Start 39 Bar: Finish 39 Bar: Start 115 Foo: Finish 165 Foo: Start 116 Bar: Finish 115 Bar: Finish Foo: Finish 116 Foo: Start 0 Foo: Finish 0 Foo: Finish Done!
Хотя в этом примере все задачи и кидаются одновременно, задачи можно добавлять в любой момент. Правда тогда смысл "начала" и "конца" несколько теряется. Если вы объясните, что вы собираетесь с ними делать, тогда можно будет добавить.
Здесь я воспользовался планировщиком LimitedConcurrencyTaskScheduler из примеров на MSDN: Samples for Parallel Programming with the .NET Framework (статья с описанием: ParallelExtensionsExtras Tour - #7 - Additional TaskSchedulers).
///

/// Provides a task scheduler that ensures a maximum concurrency level while running on top of the ThreadPool. /// Source: http://code.msdn.microsoft.com/ParExtSamples /// Documentation: http://blogs.msdn.com/b/pfxteam/archive/2010/04/09/9990424.aspx /// License: MS-LPL /// public class LimitedConcurrencyTaskScheduler : TaskScheduler { [ThreadStatic] private static bool _currentThreadIsProcessingItems;
private readonly int _maxDegreeOfParallelism; private readonly LinkedList _tasks = new LinkedList(); // protected by lock(_tasks) private int _delegatesQueuedOrRunning = 0; // protected by lock(_tasks)
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the specified degree of parallelism. /// The maximum degree of parallelism provided by this scheduler. public LimitedConcurrencyTaskScheduler (int maxDegreeOfParallelism) { if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); _maxDegreeOfParallelism = maxDegreeOfParallelism; }
/// Gets the maximum concurrency level supported by this scheduler. public override sealed int MaximumConcurrencyLevel => _maxDegreeOfParallelism;
protected override sealed IEnumerable GetScheduledTasks () { bool lockTaken = false; try { Monitor.TryEnter(_tasks, ref lockTaken); if (lockTaken) return _tasks.ToArray(); else throw new NotSupportedException(); } finally { if (lockTaken) Monitor.Exit(_tasks); } }
protected override sealed void QueueTask (Task task) { // Add the task to the list of tasks to be processed. If there aren't enough // delegates currently queued or running to process tasks, schedule another. lock (_tasks) { _tasks.AddLast(task); if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) { ++_delegatesQueuedOrRunning; NotifyThreadPoolOfPendingWork(); } } }
/// Informs the ThreadPool that there's work to be executed for this scheduler. private void NotifyThreadPoolOfPendingWork () { ThreadPool.UnsafeQueueUserWorkItem(_ => { // Note that the current thread is now processing work items. // This is necessary to enable inlining of tasks into this thread. _currentThreadIsProcessingItems = true; try { // Process all available items in the queue. while (true) { Task item; lock (_tasks) { // When there are no more items to be processed, // note that we're done processing, and get out. if (_tasks.Count == 0) { --_delegatesQueuedOrRunning; break; } // Get the next item from the queue item = _tasks.First.Value; _tasks.RemoveFirst(); } // Execute the task we pulled out of the queue TryExecuteTask(item); } } finally { // We're done processing items on the current thread _currentThreadIsProcessingItems = false; } }, null); }
protected override sealed bool TryExecuteTaskInline (Task task, bool taskWasPreviouslyQueued) { // If this thread isn't already processing a task, we don't support inlining if (!_currentThreadIsProcessingItems) return false; // If the task was previously queued, remove it from the queue if (taskWasPreviouslyQueued) TryDequeue(task); // Try to run the task. return TryExecuteTask(task); }
protected override sealed bool TryDequeue (Task task) { lock (_tasks) return _tasks.Remove(task); } }

Ещё можно воспользоваться BlockingCollection<> с ConcurrentQueue<> внутри. По окончанию добавления надо будет не забыть вызвать CompleteAdding
Ещё есть TPL Dataflow. Там параллелизмы и прочие настраиваются без кастомных планировщиков.
Можно ещё Rx добавить в качестве вишенки на торте.
Вариантов море.
Вообще, сейчас ещё @Vlad прибежит, расскажет про хитрые мозговыносящие способы решить эту задачу. :)