C# 为什么“我的工人工作分配计数”不等于此System.Threading.Channel示例中已生产项目的总数?
在这篇文章之后,我一直在玩System.Threading.Channel,以获得足够的自信,并在我的生产代码中使用它,取代我目前使用的基于Threads/Monitor.Pulse/Wait的方法 基本上,我创建了一个带有有界通道的示例,在该示例中,我在开始时运行两个生产者任务,然后在不等待的情况下启动消费者任务,消费者任务开始从通道中推送元素。 在等待制作者任务完成后,我会向频道发送完成信号,这样消费者任务就可以停止收听新的频道元素 我的通道是一个通道,在每个操作中,我都会增加WorkDistribution并发字典中每个给定工作者的计数,在示例结束时,我会打印它,以便我可以检查我消耗的项目是否与预期的一样多,以及通道如何在消费者之间分配操作 由于某些原因,此工作分配页脚打印的项目数与生产者任务生成的项目总数不相同 我错过了什么? 添加一些变量的唯一目的是帮助进行故障排除 以下是完整的代码:C# 为什么“我的工人工作分配计数”不等于此System.Threading.Channel示例中已生产项目的总数?,c#,producer-consumer,system.threading.channels,C#,Producer Consumer,System.threading.channels,在这篇文章之后,我一直在玩System.Threading.Channel,以获得足够的自信,并在我的生产代码中使用它,取代我目前使用的基于Threads/Monitor.Pulse/Wait的方法 基本上,我创建了一个带有有界通道的示例,在该示例中,我在开始时运行两个生产者任务,然后在不等待的情况下启动消费者任务,消费者任务开始从通道中推送元素。 在等待制作者任务完成后,我会向频道发送完成信号,这样消费者任务就可以停止收听新的频道元素 我的通道是一个通道,在每个操作中,我都会增加WorkDis
public class ChannelSolution
{
object LockObject = new object();
Channel<Action<string>> channel;
int ItemsToProduce;
int WorkersCount;
int TotalItemsProduced;
ConcurrentDictionary<string, int> WorkDistribution;
CancellationToken Ct;
public ChannelSolution(int workersCount, int itemsToProduce, int maxAllowedItems,
CancellationToken ct)
{
WorkersCount = workersCount;
ItemsToProduce = itemsToProduce;
channel = Channel.CreateBounded<Action<string>>(maxAllowedItems);
Console.WriteLine($"Created channel with max {maxAllowedItems} items");
WorkDistribution = new ConcurrentDictionary<string, int>();
Ct = ct;
}
async Task ProduceItems(int cycle)
{
for (var i = 0; i < ItemsToProduce; i++)
{
var index = i + 1 + (ItemsToProduce * cycle);
bool queueHasRoom;
var stopwatch = new Stopwatch();
stopwatch.Start();
do
{
if (Ct.IsCancellationRequested)
{
Console.WriteLine("exiting read loop - cancellation requested !");
break;
}
queueHasRoom = await channel.Writer.WaitToWriteAsync();
if (!queueHasRoom)
{
if (Ct.IsCancellationRequested)
{
Console.WriteLine("exiting read loop - cancellation"
+ " requested !");
break;
}
if (stopwatch.Elapsed.Seconds % 3 == 0)
Console.WriteLine("Channel reached maximum capacity..."
+ " producer waiting for items to be freed...");
}
}
while (!queueHasRoom);
channel.Writer.TryWrite((workerName) => action($"A{index}", workerName));
Console.WriteLine($"Channel has room, item {index} added"
+ $" - channel items count: [{channel.Reader.Count}]");
Interlocked.Increment(ref TotalItemsProduced);
}
}
List<Task> GetConsumers()
{
var tasks = new List<Task>();
for (var i = 0; i < WorkersCount; i++)
{
var workerName = $"W{(i + 1).ToString("00")}";
tasks.Add(Task.Run(async () =>
{
while (await channel.Reader.WaitToReadAsync())
{
if (Ct.IsCancellationRequested)
{
Console.WriteLine("exiting write loop - cancellation"
+ "requested !");
break;
}
if (channel.Reader.TryRead(out var action))
{
Console.WriteLine($"dequed action in worker [{workerName}]");
action(workerName);
}
}
}));
}
return tasks;
}
void action(string actionNumber, string workerName)
{
Console.WriteLine($"processing {actionNumber} in worker {workerName}...");
var secondsToWait = new Random().Next(2, 5);
Thread.Sleep(TimeSpan.FromSeconds(secondsToWait));
Console.WriteLine($"action {actionNumber} completed by worker {workerName}"
+ $" after {secondsToWait} secs! channel items left:"
+ $" [{channel.Reader.Count}]");
if (WorkDistribution.ContainsKey(workerName))
{
lock (LockObject)
{
WorkDistribution[workerName]++;
}
}
else
{
var succeeded = WorkDistribution.TryAdd(workerName, 1);
if (!succeeded)
{
Console.WriteLine($"!!! failed incremeting dic value !!!");
}
}
}
public void Summarize(Stopwatch stopwatch)
{
Console.WriteLine("--------------------------- Thread Work Distribution "
+ "------------------------");
foreach (var kv in this.WorkDistribution)
Console.WriteLine($"thread: {kv.Key} items consumed: {kv.Value}");
Console.WriteLine($"Total actions consumed: "
+ $"{WorkDistribution.Sum(w => w.Value)} - Elapsed time: "
+ $"{stopwatch.Elapsed.Seconds} secs");
}
public void Run(int producerCycles)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
var producerTasks = new List<Task>();
Console.WriteLine($"Started running at {DateTime.Now}...");
for (var i = 0; i < producerCycles; i++)
{
producerTasks.Add(ProduceItems(i));
}
var consumerTasks = GetConsumers();
Task.WaitAll(producerTasks.ToArray());
Console.WriteLine($"-------------- Completed waiting for PRODUCERS -"
+ " total items produced: [{TotalItemsProduced}] ------------------");
channel.Writer.Complete(); //just so I can complete this demo
Task.WaitAll(consumerTasks.ToArray());
Console.WriteLine("----------------- Completed waiting for CONSUMERS "
+ "------------------");
//Task.WaitAll(GetConsumers().Union(producerTasks/*.Union(
// new List<Task> { taskKey })*/).ToArray());
//Console.WriteLine("Completed waiting for tasks");
Summarize(stopwatch);
}
}
快速查看,ProduceItems方法中的queueHasRoom变量周围存在竞争条件。您不需要这个变量。该方法将告诉您通道的缓冲区中是否有空间。或者,您可以简单地等待该方法,而不是使用WaitToWriteAsync/TryWrite组合。AFAIK此组合旨在作为前一种方法的性能优化。如果您在尝试发布值之前绝对需要知道是否有可用空间,那么通道可能不是适合您的用例的容器。在检查可用空间->创建值->发布值的整个操作过程中,您需要找到可以锁定的内容,以便使此操作成为原子操作 另外,使用锁来保护ConcurrentDictionary的更新是多余的。ConcurrentDictionary提供了一种方法,可以用另一个值原子地替换它包含的值。如果字典包含可变对象,您可能必须锁定,并且您需要使用线程安全性对这些对象进行变异。但在您的例子中,值的类型是Int32,这是一个不可变的结构。您不需要更改它,只需将其替换为基于现有值创建的新Int32:
WorkDistribution.AddOrUpdate(workerName, 1, (_, existing) => existing + 1);
快速查看,ProduceItems方法中的queueHasRoom变量周围存在竞争条件。您不需要这个变量。该方法将告诉您通道的缓冲区中是否有空间。或者,您可以简单地等待该方法,而不是使用WaitToWriteAsync/TryWrite组合。AFAIK此组合旨在作为前一种方法的性能优化。如果您在尝试发布值之前绝对需要知道是否有可用空间,那么通道可能不是适合您的用例的容器。在检查可用空间->创建值->发布值的整个操作过程中,您需要找到可以锁定的内容,以便使此操作成为原子操作 另外,使用锁来保护ConcurrentDictionary的更新是多余的。ConcurrentDictionary提供了一种方法,可以用另一个值原子地替换它包含的值。如果字典包含可变对象,您可能必须锁定,并且您需要使用线程安全性对这些对象进行变异。但在您的例子中,值的类型是Int32,这是一个不可变的结构。您不需要更改它,只需将其替换为基于现有值创建的新Int32:
WorkDistribution.AddOrUpdate(workerName, 1, (_, existing) => existing + 1);
谢谢你的帮助。您能否指定竞态条件发生的位置?生产者代码中唯一的共享资源是一个channel writer和b TotalItems Produced。后者以线程安全的方式递增,我假设channel.writer操作也这样做,但没有检查源代码。究竟是什么原因导致您怀疑的竞争条件发生?@Veverke TryWrite的返回值未被检查,很可能为false。仅仅因为刚才有可用的空间,并不意味着仍然有可用的空间。毕竟这是一个多线程的环境。检查它,你是对的。现在计数一直如预期的那样。谢谢你,谢谢你的帮助。您能否指定竞态条件发生的位置?生产者代码中唯一的共享资源是一个channel writer和b TotalItems Produced。后者以线程安全的方式递增,我假设channel.writer操作也这样做,但没有检查源代码。究竟是什么原因导致您怀疑的竞争条件发生?@Veverke TryWrite的返回值未被检查,很可能为false。就因为有足够的空间
提前一刻贴上标签并不意味着还有空间。毕竟这是一个多线程的环境。检查它,你是对的。现在计数一直如预期的那样。非常感谢。