我有一个消息流,根据一些标准,我希望每个消费者都能够并行处理其中的一些。每个消费者都应该能够动态订阅和取消订阅。
我有以下输入数据约束:
到目前为止,这是我所拥有的:
public class Test
{
static void Main()
{
var consumer1 = new Consumer("Consumer1");
consumer1.SubscribeForCategory(1);
consumer1.SubscribeForCategory(2);
var consumer2 = new Consumer("Consumer2");
consumer2.SubscribeForCategory(2);
consumer2.SubscribeForCategory(3);
consumer2.SubscribeForCategory(4);
var consumer3 = new Consumer("Consumer3");
consumer3.SubscribeForCategory(3);
consumer3.SubscribeForCategory(4);
var consumers = new[] {consumer1, consumer2, consumer3};
var publisher = new Publisher(consumers);
var message1 = new Message(1, "message1 test");
var message2 = new Message(2, "message2");
var message3 = new Message(1, "message3");
var message4 = new Message(3, "message4 test");
var message5 = new Message(4, "message5");
var message6 = new Message(3, "message6");
var messages = new[] {message1, message2, message3, message4, message5, message6};
foreach (var message in messages)
{
publisher.Publish(message);
}
Console.ReadLine();
}
}
public class Message
{
public Message(int categoryId, string data)
{
CategoryId = categoryId;
Data = data;
}
public int CategoryId { get; }
public string Data { get; }
}
public class Publisher
{
private readonly IEnumerable<Consumer> _consumers;
public Publisher(IEnumerable<Consumer> consumers)
{
_consumers = consumers;
}
public void Publish(Message message)
{
IEnumerable<Consumer> consumers = _consumers.Where(c => c.CategoryIds.Contains(message.CategoryId));
foreach (Consumer consumer in consumers)
{
consumer.AddMessage(message);
}
}
}
public class Consumer
{
private readonly HashSet<int> _categoryIds;
private readonly ActionBlock<Message> _queue;
public Consumer(string name)
{
Name = name;
_categoryIds = new HashSet<int>();
_queue = new ActionBlock<Message>(async m => { await Foo(m); },
new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = 1,
SingleProducerConstrained = true
});
}
public string Name { get; }
public IReadOnlyCollection<int> CategoryIds => _categoryIds;
public void AddMessage(Message message)
{
bool accepted = _queue.Post(message);
if (!accepted)
{
Console.WriteLine("Message has been rejected!");
}
}
public void SubscribeForCategory(int categoryId)
{
_categoryIds.Add(categoryId);
}
private async Task Foo(Message message)
{
// process message
await Task.Delay(10);
if (message.Data.Contains("test"))
{
_categoryIds.Remove(message.CategoryId);
}
Console.WriteLine($"[{Name}] - category id: [{message.CategoryId}] data: [{message.Data}]");
}
}
不幸的是,该解决方案存在几个问题:
一种可能的解决方案是将所有消息转发给每个消费者(每个消费者都应该决定是否应该处理它),但我担心这会慢得多。
我知道基于 Actor 模型的框架,例如 Akka.Net 和 Microsoft Orleans,但我希望所有这些都在进程中发生(当然,如果可以实现的话)。
有没有人有更优雅的解决方案?您对我如何改进当前方法有什么建议吗?
答案 0 :(得分:1)
我认为您的模型中缺少实体 Category
,添加它不仅会在概念上而且会在性能方面改进您的模型。每个类别都可以包含订阅该类别的消费者列表,因此只需向订阅的消费者发送消息就变得很简单。
为了解决线程安全问题,我的建议是使用 immutable collections 而不是可变的 HashSet<T>
或 List<T>
。不可变集合的优势在于它们可以使用低锁技术(ImmutableInterlocked.Update
方法)安全和原子地更新,并且可以随时提供其内容的快照,不受未来修改的影响。如果您问如何改变不可变集合,答案是您没有改变它,而是用不同的不可变集合替换引用。这些结构的实现方式允许其内部零碎的高度可重用性。例如,在已经包含 1,000,000 个项目的 ImmutableHashSet<T>
中添加一个项目,不需要分配包含所有旧项目和新项目的新内存块。只会分配少量的小对象(内部二叉树中的节点)。
这种便利是有代价的:大多数对不可变集合的操作比对可变集合的相同操作至少慢 10 倍。从总体上看,这种开销很可能可以忽略不计,但您可能希望自己分析和衡量它,并判断它是否有影响。
Category
类:
public class Category
{
private ImmutableHashSet<Consumer> _consumers;
public int Id { get; }
public ImmutableHashSet<Consumer> Consumers => Volatile.Read(ref _consumers);
public Category(int id)
{
this.Id = id;
_consumers = ImmutableHashSet.Create<Consumer>();
}
public void SubscribeConsumer(Consumer consumer) =>
ImmutableInterlocked.Update(ref _consumers, col => col.Add(consumer));
public void UnsubscribeConsumer(Consumer consumer) =>
ImmutableInterlocked.Update(ref _consumers, col => col.Remove(consumer));
}
请注意 Volatile.Read
,它确保所有访问 _consumers
属性的线程都可以立即看到存储在 Consumers
字段中的最新引用。
Consumer
类:
public class Consumer
{
private readonly ActionBlock<Message> _block;
private IImmutableList<Category> _categories;
public string Name { get; }
public IImmutableList<Category> Categories => Volatile.Read(ref _categories);
public Consumer(string name)
{
this.Name = name;
_categories = ImmutableArray.Create<Category>();
_block = new ActionBlock<Message>(async message =>
{
if (!Categories.Any(cat => cat.Id == message.CategoryId)) return;
// Process message...
});
}
public void SendMessage(Message message)
{
bool accepted = _block.Post(message);
Debug.Assert(accepted);
}
public void SubscribeForCategory(Category category)
{
ImmutableInterlocked.Update(ref _categories, col => col.Add(category));
category.SubscribeConsumer(this);
}
public void UnsubscribeForCategory(Category category)
{
ImmutableInterlocked.Update(ref _categories, col => col.Remove(category));
category.UnsubscribeConsumer(this);
}
}
请注意,SubscribeForCategory
方法还负责添加反向关系(类别 ->
消费者)。在上面的实现中,这两个关系没有相互原子地添加,这意味着观察者可以看到订阅了一个类别的消费者,以及没有订阅消费者的类别。根据您的描述,您的应用中似乎不存在这样的观察者,因此这种不一致可能没有太大关系。
Publisher
类需要保存类别列表,而不是消费者:
public class Publisher
{
private readonly Dictionary<int, Category> _categories;
public Publisher(IEnumerable<Category> categories)
{
_categories = categories.ToDictionary(cat => cat.Id);
}
public void Publish(Message message)
{
var category = _categories[message.CategoryId];
foreach (Consumer consumer in category.Consumers)
consumer.SendMessage(message);
}
}
注意 Publish
方法是多么简单。