C# производитель / потребитель / наблюдатель?

у меня есть производитель / потребитель очереди, за исключением того, что есть определенные типы объектов. Таким образом, не только любой потребитель может потреблять добавленный объект. Я не хочу создавать определенную очередь для каждого типа, так как их слишком много. (Это своего рода растягивает определение производителя/потребителя, но я не уверен, что это правильный термин.)

есть ли такая вещь, как EventWaitHandle, которая позволяет импульсы с параметром? например,myHandle.Set(AddedType = "foo"). Прямо сейчас я использую Monitor.Wait и тогда каждый потребитель проверяет, действительно ли пульс предназначался для них, но это кажется бессмысленным.

версия pseduocode того, что у меня сейчас:

class MyWorker {
    public string MyType {get; set;}
    public static Dictionary<string, MyInfo> data;

    public static void DoWork(){
        while(true){
             if(Monitor.Wait(data, timeout)){
                   if (data.ContainsKey(MyType)){
                        // OK, do work
                   }
             }
        }
    }
}

как вы можете видеть, я могу получить импульсы, когда другие вещи добавляются в дикт. Меня волнует только, когда MyType добавляется в дикт. Есть ли способ сделать это? Это не огромное дело, но, например, я должен вручную обрабатывать тайм-ауты сейчас, потому что каждый get блокировки может преуспеть в течение тайм-аута, но MyType не добавлены дикт внутри timeout.

2 ответов


это интересный вопрос. Похоже, что ключом к решению является блокирующий вариант a приоритет в очереди. Java имеет PriorityBlockingQueue, но, к сожалению, эквивалент для .NET BCL отсутствует. Однако, как только у вас есть один, реализация проста.

class MyWorker 
{
    public string MyType {get; set;}
    public static PriorityBlockingQueue<string, MyInfo> data; 

    public static void DoWork()
    {
        while(true)
        {
            MyInfo value;
            if (data.TryTake(MyType, timeout, out value))
            {
                // OK, do work
            }
        }
    }
}

осуществляет PriorityBlockingQueue это не очень сложно. Следуя той же схеме, что и BlockingCollection С помощью Add и Take методы стиля я придумал следующее код.

public class PriorityBlockingQueue<TKey, TValue>
{
    private SortedDictionary<TKey, TValue> m_Dictionary = new SortedDictionary<TKey,TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (m_Dictionary)
        {
            m_Dictionary.Add(key, value);
            Monitor.Pulse(m_Dictionary);
        }
    }

    public TValue Take(TKey key)
    {
        TValue value;
        TryTake(key, TimeSpan.FromTicks(long.MaxValue), out value);
        return value;
    }

    public bool TryTake(TKey key, TimeSpan timeout, out TValue value)
    {
        value = default(TValue);
        DateTime initial = DateTime.UtcNow;
        lock (m_Dictionary)
        {
            while (!m_Dictionary.TryGetValue(key, out value))
            {
                if (m_Dictionary.Count > 0) Monitor.Pulse(m_Dictionary); // Important!
                TimeSpan span = timeout - (DateTime.UtcNow - initial);
                if (!Monitor.Wait(m_Dictionary, span))
                {
                    return false;
                }
            }
            m_Dictionary.Remove(key);
            return true;
        }
    }
}

это была быстрая реализация и у него есть несколько проблем. Во-первых, я его вообще не тестировал. Во-вторых, он использует красно-черное дерево (через SortedDictionary) в качестве базовой структуры данных. Это означает TryTake метод будет иметь сложность O(log(n)). Очереди приоритетов обычно имеют сложность удаления O(1). Обычно для очередей приоритетов выбирается структура данных кучу, но я считаю, что пропустить списки на самом деле лучше в практики по нескольким причинам. Ни один из них не существует в .NET BCL, поэтому я использовал SortedDictionary вместо этого, несмотря на его низкую производительность в этом сценарии.

я должен указать здесь, что это на самом деле не решает бессмысленное Wait/Pulse поведение. Он просто инкапсулирован в PriorityBlockingQueue класса. Но, по крайней мере, это, безусловно, очистит основную часть вашего кода.

не похоже, что ваш код обрабатывал несколько объектов на ключ, но это было бы легко добавить с помощью Queue<MyInfo> вместо обычного MyInfo при добавлении в словарь.


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

шаблон наблюдателя в .Net обычно реализуется с использованием событий C#. Вам просто нужно будет вызвать обработчик событий для объект и один или несколько наблюдателей будут вызваны через него. Целевой код сначала должен был бы зарегистрироваться с наблюдаемым объектом, добавив себя к событию для уведомления о прибытии работы.