C# 异步生产者/消费者

C# 异步生产者/消费者,c#,multithreading,concurrency,parallel-processing,producer-consumer,C#,Multithreading,Concurrency,Parallel Processing,Producer Consumer,我有一个从多个线程访问的类的实例。这个类接受这个调用并将一个元组添加到数据库中。我需要以串行方式完成这项工作,因为由于某些db约束,并行线程可能会导致数据库不一致 由于我对C语言中的并行性和并发性不熟悉,我做了以下工作: private BlockingCollection<Task> _tasks = new BlockingCollection<Task>(); public void AddDData(string info) { Task t = new

我有一个从多个线程访问的类的实例。这个类接受这个调用并将一个元组添加到数据库中。我需要以串行方式完成这项工作,因为由于某些db约束,并行线程可能会导致数据库不一致

由于我对C语言中的并行性和并发性不熟悉,我做了以下工作:

private BlockingCollection<Task> _tasks = new BlockingCollection<Task>();

public void AddDData(string info)
{
    Task t = new Task(() => { InsertDataIntoBase(info); });
    _tasks.Add(t);
}

private void InsertWorker()
{
    Task.Factory.StartNew(() =>
    {
        while (!_tasks.IsCompleted)
        {
            Task t;
            if (_tasks.TryTake(out t))
            {
                t.Start();
                t.Wait();
            }
        }
    });
}
注意,我摆脱了任务,因为我依赖于Synched InsertIntoDB调用,因为它在循环中,但仍然没有运气。。。生成过程很好,我绝对确信只有唯一的实例才会进入队列。但不管我怎么做,有时同一个对象会被使用两次。

从你的评论中

我简化了这里显示的代码,因为数据不是字符串


我假设传入AddDData的info参数是一个可变的引用类型。确保调用者没有使用相同的info实例进行multple调用,因为该引用是在任务lambda中捕获的。

我认为这样应该可以:

    private static BlockingCollection<string> _itemsToProcess = new BlockingCollection<string>();

    static void Main(string[] args)
    {
        InsertWorker();
        GenerateItems(10, 1000);
        _itemsToProcess.CompleteAdding();
    }

    private static void InsertWorker()
    {
        Task.Factory.StartNew(() =>
        {
            while (!_itemsToProcess.IsCompleted)
            {
                string t;
                if (_itemsToProcess.TryTake(out t))
                {
                    // Do whatever needs doing here
                    // Order should be guaranteed since BlockingCollection 
                    // uses a ConcurrentQueue as a backing store by default.
                    // http://msdn.microsoft.com/en-us/library/dd287184.aspx#remarksToggle
                    Console.WriteLine(t);
                }
            }
        });
    }

    private static void GenerateItems(int count, int maxDelayInMs)
    {
        Random r = new Random();
        string[] items = new string[count];

        for (int i = 0; i < count; i++)
        {
            items[i] = i.ToString();
        }

        // Simulate many threads adding items to the collection
        items
            .AsParallel()
            .WithDegreeOfParallelism(4)
            .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
            .Select((x) =>
            {
                Thread.Sleep(r.Next(maxDelayInMs));
                _itemsToProcess.Add(x);
                return x;
            }).ToList();
    }

这确实意味着使用者是单线程的,但允许多个生产者线程。

根据您提供的跟踪,唯一的逻辑可能性是您调用了InsertWorker两次或更多次。因此,有两个后台线程等待项目出现在集合中,有时它们都会设法抓取一个项目并开始执行它。

如何生成主键?实际上,我简化了此处显示的代码,因为数据不是字符串,而是一个非常复杂的对象。PK实际上是两个对象字段:名称字符串和日期时间值。我无法控制数据库。我认为一个简单的锁就足以序列化调用。要确认确实要执行两次相同的任务,请将PK和TaskID task.CurrentId写入命令行并查看输出。当启动1M+任务时,我无法重现此情况…从:删除项的顺序取决于用于创建BlockingCollection实例的集合类型。创建BlockingCollection对象时,可以指定要使用的集合类型。例如,可以为先进先出FIFO行为指定ConcurrentQueue对象。BlockingCollection的默认集合类型是ConcurrentQueue。是的,它是一个可变引用类型,我将再次检查它,尽管我认为这不是问题所在。竞争条件可能会导致这种行为,但使用BlockingCollection作为缓冲区不会降低这种风险?Rafa Borges:我误读了代码-没有竞争条件,因为它确保项目按顺序执行。它不排除在不同任务中捕获相同项目的可能性。我尝试了10000000次迭代,但没有得到重复项。我会再考虑的。请注意,您可以通过替换while来简化您的工作人员!在_itemsToProcess.GetConsumingEnumerable中使用单个foreach字符串t完成并尝试生成。请参阅。@JimMischel我考虑过这一点,但排序在这里很重要,并且说使用此方法时不能保证排序。@sga101经过数小时的调试和日志记录,我发现错误不在代码的这一部分,而是在数据库插入中。似乎有一个讨厌的bug是由我用这个代码气味创建的竞争条件引起的。当你的代码引导我发现这一点时,为你竖起大拇指,万分感谢@sga101:我假设你说的是这样一行:不能保证项目是按照生产者线程添加的相同顺序枚举的。老实说,我不知道那句话是什么意思。我向您保证,GetConsumingEnumerable确实会按FIFO顺序删除内容。查看一下.NET Framework源代码可以确认:GetConsumingEnumerable只是一个循环中的一组TryTake调用。项目的删除顺序与插入顺序完全相同。
    private static BlockingCollection<string> _itemsToProcess = new BlockingCollection<string>();

    static void Main(string[] args)
    {
        InsertWorker();
        GenerateItems(10, 1000);
        _itemsToProcess.CompleteAdding();
    }

    private static void InsertWorker()
    {
        Task.Factory.StartNew(() =>
        {
            while (!_itemsToProcess.IsCompleted)
            {
                string t;
                if (_itemsToProcess.TryTake(out t))
                {
                    // Do whatever needs doing here
                    // Order should be guaranteed since BlockingCollection 
                    // uses a ConcurrentQueue as a backing store by default.
                    // http://msdn.microsoft.com/en-us/library/dd287184.aspx#remarksToggle
                    Console.WriteLine(t);
                }
            }
        });
    }

    private static void GenerateItems(int count, int maxDelayInMs)
    {
        Random r = new Random();
        string[] items = new string[count];

        for (int i = 0; i < count; i++)
        {
            items[i] = i.ToString();
        }

        // Simulate many threads adding items to the collection
        items
            .AsParallel()
            .WithDegreeOfParallelism(4)
            .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
            .Select((x) =>
            {
                Thread.Sleep(r.Next(maxDelayInMs));
                _itemsToProcess.Add(x);
                return x;
            }).ToList();
    }