在C#BlockingCollection中的某个地方丢失项目,使用GetConsumingEnumerable()

在C#BlockingCollection中的某个地方丢失项目,使用GetConsumingEnumerable(),c#,blockingcollection,C#,Blockingcollection,我正在尝试通过WAN向多个目标执行并行SqlBulkCopy,其中许多目标可能连接速度慢和/或连接中断;他们的连接速度从2兆位到50兆位不等,我是从一个1000兆位的上传连接发送的;许多目标需要多次重试才能正确完成 我目前正在BlockingCollection(队列)的getconsumineGenumerable()上使用一个Parallel.ForEach;然而,我不是偶然发现了一些bug,就是在完全理解它的用途时遇到了问题,或者只是出了点问题。。 代码从不调用blockingcollec

我正在尝试通过WAN向多个目标执行并行SqlBulkCopy,其中许多目标可能连接速度慢和/或连接中断;他们的连接速度从2兆位到50兆位不等,我是从一个1000兆位的上传连接发送的;许多目标需要多次重试才能正确完成


我目前正在BlockingCollection(
队列
)的
getconsumineGenumerable()
上使用一个
Parallel.ForEach
;然而,我不是偶然发现了一些bug,就是在完全理解它的用途时遇到了问题,或者只是出了点问题。。 代码从不调用blockingcollection的
CompleteAdding()
方法, 似乎在并行foreach循环的某个地方,一些目标丢失了。 即使对此有不同的方法,并且不考虑它在循环中所做的工作,blockingcollection也不应该像本例中那样工作,是吗

在foreach循环中,我完成了这项工作,如果目标成功完成,我会将其添加到
results
-集合中,如果发生错误,我会将目标重新添加到BlockingCollection中,直到目标达到最大重试次数阈值;此时,我将其添加到
结果
-集合中

在另一个任务中,我循环,直到
结果
-集合的计数等于目标的初始计数;然后我对阻塞集合执行
CompleteAdding()

我已经尝试使用锁定对象对
结果
-集合(改为使用
列表
)和队列执行操作,但运气不佳,但无论如何都不需要这样做。我还尝试将重试次数添加到一个单独的集合中,并在另一个任务中(而不是在parallel.foreach中)将重试次数重新添加到BlockingCollection中。 为了好玩,我还试着用.NET从4.5编译到4.8,以及不同的C语言版本

以下是一个简化的示例:

List targets=newlist();
对于(int i=0;i<200;i++)
{
目标。添加(0);
}
BlockingCollection队列=新建BlockingCollection(新建ConcurrentQueue());
ConcurrentBag结果=新ConcurrentBag();
targets.ForEach(f=>queue.Add(f));
//以菲利亚伦为单位的批量副本:
Task.Run(()=>
{
而(results.Count
{
尝试
{
//模拟bulkcopy的问题:
抛出新异常();
结果:增加(目标);
}
捕获(例外)
{
如果(目标<最大重试次数)
{
目标++;
如果(!queue.TryAdd(目标))
Console.WriteLine($“{target.ToString(“D3”)}:错误,无法添加到队列!”);
}
其他的
{
结果:增加(目标);
WriteLine($“在{target+1}尝试|{results.Count}/{targets.Count}项完成后中止”);
}
}
});
我预计
结果
-集合的计数最终将是
目标
-列表的精确计数,但它似乎从未达到该数字,这导致BlockingCollection从未标记为已完成,因此代码从未完成

我真的不明白为什么不是所有的目标最终都被添加到
结果
-集合中!添加的计数总是不同的,并且大多数情况下只是在预期的最终计数之前

编辑:我删除了重试部分,并用一个简单的int计数器替换了ConcurrentBag,但它大部分时间仍然无法工作:

List targets=newlist();
对于(int i=0;i<500;i++)
目标。添加(0);
BlockingCollection队列=新建BlockingCollection(新建ConcurrentQueue());
//ConcurrentBag结果=新ConcurrentBag();
int completed=0;
targets.ForEach(f=>queue.Add(f));
变量线程=新线程(()=>
{
while(已完成
{
联锁。增量(参考完成);
});

很抱歉,找到了答案:blockingcollection和parallel foreach使用的默认分区器是区块和缓冲,这导致foreach循环永远等待下一个区块的足够项目。。对我来说,它在那里坐了一整晚,没有处理最后几件物品

因此,不是:

ParallelOptions=newparalleloptions{maxdegreeofpparallelism=4};
Parallel.ForEach(queue.getconsumineGenumerable(),选项,目标=>
{
联锁。增量(参考完成);
});
您必须使用:

var partitioner=partitioner.Create(queue.getconsumineGenumerable(),EnumerablePartitionOptions.NoBuffering);
ParallelOptions=new ParallelOptions{MaxDegreeOfpParallelism=4};
Parallel.ForEach(分区器,选项,目标=>
{
联锁。增量(参考完成);
});

并行。ForEach
用于数据并行(即使用所有8个核处理100K行),而不是并发操作。这本质上是一个发布/订阅和异步问题,如果不是管道问题的话。在这种情况下,CPU无需执行任何操作,只需启动异步操作并等待它们完成

.NET从.NET 4.5开始通过
async Task MyBulkCopyMethod(string connectionString,DataTable data)
{
    using(var bcp=new SqlBulkCopy(connectionString))
    {
        //Set up mappings etc.
        //....
        await bcp.WriteToServerAsync(data);   
    }
}
class DataMessage
{
    public string Connection{get;set;}
    public DataTable Data {get;set;} 
}
var options=new ExecutionDataflowBlockOptions { 
                    MaxDegreeOfParallelism = 50,
                    BoundedCapacity = 8
            };
var block=new ActionBlock<DataMessage>(msg=>MyBulkCopyMethod(msg.Connection,msg.Data, options);
var data=.....;
var servers=new[]{connString1, connString2,....};
var messages= from sv in servers
              select new DataMessage{ ConnectionString=sv,Table=data};

foreach(var msg in messages)
{
    await block.SendAsync(msg);
}
//Tell the block we are done
block.Complete();
//Await for all messages to finish processing
await block.Completion;
var block=new ActionBlock<DataMessage>(async msg=> {
    try {
        await MyBulkCopyMethod(msg.Connection,msg.Data, options);
    }
    catch(SqlException exc) when (some retry condition)
    {
        //Post without awaiting
        retryBlock.Post(msg);
    });
block.Completion.ContinueWith(_=>retryBlock.Complete());
var retryOptions=new ExecutionDataflowBlockOptions { 
                MaxDegreeOfParallelism = 5
        };
var retryBlock=new ActionBlock<DataMessage>(async msg=>{
    await Task.Delay(1000);
    try {
        await MyBulkCopyMethod(msg.Connection,msg.Data, options);
    }
    catch (Exception ....)
    {
        ...
    }
});