【发布时间】:2013-09-11 18:59:43
【问题描述】:
我有一个从多个线程访问的类的实例。此类接受此调用并将元组添加到数据库中。我需要以串行方式完成此操作,因为由于某些 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();
}
}
});
}
AddDData 是由多个线程调用的,InsertDataIntoBase 是一个非常简单的插入,应该需要几毫秒。
问题是,由于某种原因,我缺乏知识并无法弄清楚,有时一个任务被调用了两次!它总是这样:
T1 T2 T3 T1
我是否理解 .Take() 完全错误,是我遗漏了什么还是我的生产者/消费者实现真的很糟糕?
最好的问候, 拉斐尔
更新:
按照建议,我用这种架构做了一个快速的沙盒测试实现,正如我所怀疑的,它不能保证在前一个任务完成之前不会触发任务。
所以问题仍然存在:如何正确地对任务进行排队并按顺序触发它们?
更新 2:
我简化了代码:
private BlockingCollection<Data> _tasks = new BlockingCollection<Data>();
public void AddDData(Data info)
{
_tasks.Add(info);
}
private void InsertWorker()
{
Task.Factory.StartNew(() =>
{
while (!_tasks.IsCompleted)
{
Data info;
if (_tasks.TryTake(out info))
{
InsertIntoDB(info);
}
}
});
}
请注意,我摆脱了 Tasks,因为我依赖于同步的 InsertIntoDB 调用(因为它在循环内),但仍然没有运气......一代很好,我绝对确定只有唯一的实例是去排队。但不管我怎么尝试,有时同一个对象会被使用两次。
【问题讨论】:
-
你是如何生成主键的?
-
实际上我简化了这里显示的代码,因为数据不是字符串,而是一个非常复杂的对象。 PK 实际上是 2 个对象字段(名称字符串和日期时间值)。我无法控制数据库。
-
我认为一个简单的
lock足以序列化调用。 -
要确认您确实在执行两次相同的任务,请将 PK 和 TaskID (
Task.CurrentId) 写入命令行并查看输出。启动 1M+ 任务时我无法重现此问题... -
来自msdn:删除项目的顺序取决于用于创建 BlockingCollection
实例的集合类型。创建 BlockingCollection 对象时,可以指定要使用的集合类型。例如,您可以为先进先出 (FIFO) 行为指定 ConcurrentQueue 对象。 BlockingCollection 的默认集合类型是 ConcurrentQueue 。
标签: c# multithreading concurrency parallel-processing producer-consumer