С# поместить поток в спящий режим при исключении из очереди?

Я пытаюсь использовать WebClient для асинхронной загрузки нескольких файлов. Насколько я понимаю, это возможно, но вам нужно иметь один объект WebClient для каждой загрузки. Так что я решил, что просто поставлю кучу их в очередь в начале моей программы, затем вытащу их по одному и скажу им загрузить файл. Когда файл загружается, они могут быть возвращены в очередь.

Толкать вещи в мою очередь не должно быть так уж плохо, мне просто нужно сделать что-то вроде:

lock(queue) {
    queue.Enqueue(webClient);
}

Правильно? Но как насчет того, чтобы их сбросить? Я хочу, чтобы мой основной поток спал, когда очередь пуста (подождите, пока другой веб-клиент не будет готов, чтобы он мог начать следующую загрузку). Я полагаю, что мог бы использовать Semaphore рядом с очередью, чтобы отслеживать, сколько элементов находится в очереди, и это при необходимости переводило бы мой поток в спящий режим, но это не похоже на очень хорошее решение. Что произойдет, если я забуду уменьшить/увеличить свой семафор каждый раз, когда я помещаю/извлекаю что-то из своей очереди, и они теряют синхронизацию? Это было бы плохо. Нет ли какого-нибудь хорошего способа, чтобы queue.Dequeue() автоматически засыпал, пока не появится элемент, который нужно удалить из очереди, а затем продолжить?

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


person mpen    schedule 23.11.2009    source источник


Ответы (3)


Вот пример использования семафора. IMO это намного чище, чем использование монитора:

public class BlockingQueue<T>
{
    Queue<T> _queue = new Queue<T>();
    Semaphore _sem = new Semaphore(0, Int32.MaxValue);

    public void Enqueue(T item)
    {
        lock (_queue)
        {
            _queue.Enqueue(item);
        }

        _sem.Release();
    }

    public T Dequeue()
    {
        _sem.WaitOne();

        lock (_queue)
        {
            return _queue.Dequeue();
        }
    }
}
person DSO    schedule 23.11.2009
comment
В любом случае вам не нужна отдельная переменная-счетчик - очередь ведет свой собственный счет. - person Jon Skeet; 23.11.2009
comment
Да... я тоже об этом подумал. Просто находите странным, что это не стандартный класс. Я, вероятно, пойду с этим решением, спасибо! - person mpen; 23.11.2009
comment
@Jon: Да, он ведет свой собственный подсчет, но получение подсчета не является потокобезопасным, не так ли? Таким образом, вам придется заблокировать очередь, чтобы получить счетчик, а затем, если счетчик равен 0, освободить его, подождать, снова заблокировать ..... это становится еще более неприятным, не так ли? - person mpen; 23.11.2009
comment
Если у вас уже есть блокировка всякий раз, когда вы обращаетесь к очереди (что у вас есть в моем примере), то все в порядке. Моя версия работает без проблем, но не имеет отдельной переменной-счетчика. - person Jon Skeet; 23.11.2009
comment
Верно, что у очереди есть собственный счетчик (не уверен, почему другой ответ с использованием монитора объявил явный счетчик). Тем не менее, я думаю, что семафор немного более элегантен, чем монитор для этой конкретной проблемы, но я думаю, что это субъективно. - person DSO; 23.11.2009
comment
Я согласен с ДСО. Проще для понимания и без циклов. В моей программе это работает довольно хорошо :D - person mpen; 25.11.2009

Вам нужна очередь производителей/потребителей.

У меня есть простой пример в моем руководстве по работе с потоками — прокрутите примерно половину вниз по этой странице. Он был написан до дженериков, но его должно быть достаточно легко обновлять. Вам может потребоваться добавить различные функции, такие как возможность «остановить» очередь: это часто выполняется с помощью своего рода токена «нулевого рабочего элемента»; вы вводите в очередь столько «стоповых» элементов, сколько у вас есть потоков, исключающих очередь, и каждый из них прекращает удаление из очереди, когда достигает одного.

Поиск «очереди производителя-потребителя» вполне может предоставить вам лучшие образцы кода — на самом деле это просто демонстрация ожидания/пульсации.

IIRC, в .NET 4.0 есть типы (как часть Parallel Extensions), которые будут делать то же самое, но намного лучше :) Я думаю, что вам нужен BlockingCollection, обертывающий ConcurrentQueue.

person Jon Skeet    schedule 23.11.2009
comment
Зачем вам нужен цикл while вокруг Monitor.Wait(listLock);? Разве потребитель не должен просыпаться только тогда, когда товар готов к употреблению? - person mpen; 23.11.2009
comment
Кроме того, вы не можете lock на любом объекте? Почему бы не заблокировать очередь, а не использовать отдельный объект блокировки? - person mpen; 23.11.2009
comment
+1. Еще одно хорошее руководство по многопоточности с очередью производителя-потребителя: part2.aspx#_Synchronization_Essentials (просто найдите производителя) - person RichardOD; 23.11.2009
comment
@Mark: Если у вас нет цикла while, потребитель может быть разбужен, но тогда другой потребитель может прийти и забрать предмет раньше разбуженного. Вызовы ожидания должны всегда иметь цикл while, проверяющий какое-либо условие. И да, вы можете (к сожалению, IMO) заблокировать любой объект, но я считаю, что это плохая идея. Это означает, что вы не знаете, какой другой код собирается его заблокировать. Блокирует ли Queue сама себя? Если это произойдет, что произойдет? Может быть, в данном случае это было бы нормально... но мне легче думать, если я буду держать отдельные приватные замки. - person Jon Skeet; 23.11.2009
comment
О, это просто сбивает с толку! Потому что все потребители весело входят в этот блокирующий блок и расслабляются в Monitor.Wait. Вы могли бы подумать, что поскольку это блокировка, там будет только один поток, но это не так, потому что Monitor.Wait освобождает блокировку и впускает всех остальных. Это, и теперь у вас есть форма ленивого ожидания ; вздремнуть и зайти позже. Не могу сказать, что мне очень нравится это решение. Вы можете сэкономить на счетчике, но накладные расходы на то, чтобы обернуть вокруг него голову, для меня того не стоят. Однако приятно знать, так что спасибо. - person mpen; 25.11.2009
comment
Это не занятое ожидание — это не просто проверка позже; поток пробуждается, когда монитор пульсирует. Что касается поведения Monitor.Wait - если вы собираетесь писать свои собственные потоковые коллекции, вы действительно должны понимать эти вещи до того, как начнете. Как только вы это сделаете, это вообще не проблема. - person Jon Skeet; 25.11.2009
comment
Да, но я имею в виду, что когда он пульсирует, он должен снова проверить счетчик очереди. Потенциально один поток может застрять в этом цикле и проверять это количество раз больше, чем необходимо. Интересно, однако, что эти мониторы пригодились для решения несвязанной проблемы :D Снятие блокировки удобно. - person mpen; 28.12.2009
comment
@Mark: Это не занято ожиданием с практической точки зрения. Да, теоретически возможно, чтобы другой поток каждый раз входил в точное время, необходимое для кражи следующего предмета, но как часто, по вашему мнению, это происходит в реальной жизни? - person Jon Skeet; 28.12.2009

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

public class BlockingQueue<T> : IEnumerable<T>
{
    private int _count = 0;
    private Queue<T> _queue = new Queue<T>();

    public T Dequeue()
    {
        lock (_queue)
        {
            while (_count <= 0)
                Monitor.Wait(_queue);
            _count--;
            return _queue.Dequeue();
        }
    }

    public void Enqueue(T data)
    {
        if (data == null)
            throw new ArgumentNullException("data");
        lock (_queue)
        {
            _queue.Enqueue(data);
            _count++;
            Monitor.Pulse(_queue);
        }
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        while (true)
            yield return Dequeue();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable<T>) this).GetEnumerator();
    }
}

Просто используйте это вместо обычной очереди, и она должна делать то, что вам нужно.

person Richard    schedule 23.11.2009
comment
Есть ли причина использовать свой собственный счет вместо того, чтобы спрашивать очередь? - person Jon Skeet; 23.11.2009
comment
Никогда не используйте Monitor.Pulse, всегда есть лучшие альтернативы. - person erikkallen; 23.11.2009
comment
@erikkallen: Это смелое заявление без объяснения причин. Мне кажется разумным в данном случае - что бы вы сделали по-другому? - person Jon Skeet; 23.11.2009