8

我在以下程序中遇到难以重现的错误,其中多个线程并行更新并发字典,并且主线程在固定时间间隔后按排序顺序显示字典的状态,直到所有更新线程完成。

public void Function(IEnumerable<ICharacterReader> characterReaders, IOutputter outputter)
{
    ConcurrentDictionary<string, int> wordFrequencies = new ConcurrentDictionary<string, int>();
    Thread t = new Thread(() => UpdateWordFrequencies(characterReaders, wordFrequencies));
    bool completed = false;
    var q = from pair in wordFrequencies orderby pair.Value descending, pair.Key select new Tuple<string, int>(pair.Key, pair.Value);
    t.Start();
    Thread.Sleep(0);

    while (!completed)
    {
        completed = t.Join(1);
        outputter.WriteBatch(q);
    }            
}

该函数有一个字符流列表和一个输出器。该函数维护从每个字符流(并行)读取的单词的单词频率的并发字典。单词被一个新线程读入,主线程每 1 毫秒输出字典的当前状态(按排序顺序),直到所有输入流都被读取(实际上输出将类似于每 10 秒,但错误似乎只出现在非常小的值上)。WriteBatch 函数只是写入控制台:

public void WriteBatch(IEnumerable<Tuple<string, int>> batch)
{
    foreach (var tuple in batch)
    {
        Console.WriteLine("{0} - {1}", tuple.Item1, tuple.Item2);
    }
    Console.WriteLine();
}

大多数执行都很好,但有时我在 WriteBatch 函数的 foreach 语句中收到以下错误:

“未处理的异常:System.ArgumentException:索引等于或大于数组的长度,或者字典中的元素数大于从索引到目标数组末尾的可用空间。”

如果主线程在启动更新线程之后和开始显示循环之前休眠一小会儿,错误似乎确实消失了。如果删除 orderby 子句并且字典未在 linq 查询中排序,它似乎也消失了。有什么解释吗?

WriteBatch 函数中的foreach (var tuple in batch)语句给出了错误。堆栈跟踪如下:

未处理的异常:System.ArgumentException:索引等于或大于数组的长度,或者字典中的元素数大于从索引到目标数组末尾的可用空间。在 System.Collections.Concurrent.ConcurrentDictionary2.System.Collections.Generic.ICollection>.CopyTo(KeyValuePair2[] 数组,Int32 索引) 在 System.Linq.Buffer1..ctor(IEnumerable1 源) 在 System.Linq.OrderedEnumerable1。 d__0.MoveNext() at System.Linq.Enumerable.WhereSelectEnumerableIterator2.MoveNext() at MyProject.ConsoleOutputter.WriteBatch(IEnumerable1 batch) in C:\MyProject\ConsoleOutputter.cs:line 10 at MyProject.Function(IEnumerable1 characterReaders, IOutputter outputter)

4

2 回答 2

13

正如其他人所说,内部类的构造函数中存在竞争,System.Linq.Buffer<T>由 调用OrderBy

这是有问题的代码片段:

TElement[] array = null;
int num = 0;
if (collection != null)
{
    num = collection.Count;
    if (num > 0)
    {
        array = new TElement[num];
        collection.CopyTo(array, 0);
    }
}

collection在调用到之后collection.Count但在调用到之前将项目添加到 时,将引发异常collection.CopyTo


作为一种解决方法,您可以在对字典进行排序之前制作字典的“快照”副本。

您可以通过调用来做到这一点,ConcurrentDictionary.ToArray.
因为这是在ConcurrentDictionary类本身中实现的,所以它是安全的。

使用这种方法意味着您不必使用锁来保护集合,正如您所说,这首先破坏了使用并发集合的目的。

while (!completed)
{
    completed = t.Join(1);

    var q =
      from pair in wordFrequencies.ToArray() // <-- add ToArray here
      orderby pair.Value descending, pair.Key
      select new Tuple<string, int>(pair.Key, pair.Value);

    outputter.WriteBatch(q);
}            
于 2012-07-28T09:21:29.457 回答
1

在评论中与 ChrisShain 讨论后,得出的结论是,您应该在打印出来之前获得对字典的互斥访问权限,或者使用 a mutexoflock声明。

用锁做:

public void WriteBatch(IEnumerable<Tuple<string, int>> batch)
{
    lock (myLock) 
    {
        foreach (var tuple in batch)
        {
            Console.WriteLine("{0} - {1}", tuple.Item1, tuple.Item2);
        }
        Console.WriteLine();
    }
}

myLock假设您在类级别分配了一个对象。见例子

使用互斥锁执行此操作:

public void WriteBatch(IEnumerable<Tuple<string, int>> batch)
{
    mut.WaitOne();

    foreach (var tuple in batch)
    {
        Console.WriteLine("{0} - {1}", tuple.Item1, tuple.Item2);
    }
    Console.WriteLine();

    mut.ReleaseMutex();
}

Mutex同样,假设您在类级别分配了一个对象。见例子

于 2012-07-27T17:52:27.797 回答