C# 为什么“我的工人工作分配计数”不等于此System.Threading.Channel示例中已生产项目的总数?

C# 为什么“我的工人工作分配计数”不等于此System.Threading.Channel示例中已生产项目的总数?,c#,producer-consumer,system.threading.channels,C#,Producer Consumer,System.threading.channels,在这篇文章之后,我一直在玩System.Threading.Channel,以获得足够的自信,并在我的生产代码中使用它,取代我目前使用的基于Threads/Monitor.Pulse/Wait的方法 基本上,我创建了一个带有有界通道的示例,在该示例中,我在开始时运行两个生产者任务,然后在不等待的情况下启动消费者任务,消费者任务开始从通道中推送元素。 在等待制作者任务完成后,我会向频道发送完成信号,这样消费者任务就可以停止收听新的频道元素 我的通道是一个通道,在每个操作中,我都会增加WorkDis

在这篇文章之后,我一直在玩System.Threading.Channel,以获得足够的自信,并在我的生产代码中使用它,取代我目前使用的基于Threads/Monitor.Pulse/Wait的方法

基本上,我创建了一个带有有界通道的示例,在该示例中,我在开始时运行两个生产者任务,然后在不等待的情况下启动消费者任务,消费者任务开始从通道中推送元素。 在等待制作者任务完成后,我会向频道发送完成信号,这样消费者任务就可以停止收听新的频道元素

我的通道是一个通道,在每个操作中,我都会增加WorkDistribution并发字典中每个给定工作者的计数,在示例结束时,我会打印它,以便我可以检查我消耗的项目是否与预期的一样多,以及通道如何在消费者之间分配操作

由于某些原因,此工作分配页脚打印的项目数与生产者任务生成的项目总数不相同

我错过了什么? 添加一些变量的唯一目的是帮助进行故障排除

以下是完整的代码:

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。就因为有足够的空间
提前一刻贴上标签并不意味着还有空间。毕竟这是一个多线程的环境。检查它,你是对的。现在计数一直如预期的那样。非常感谢。