我正在使用foreach循环来处理要处理的数据列表(一旦处理就删除了所述数据 - 这是在锁内)。此方法偶尔会导致ArgumentException。
抓住它本来就很贵,所以我试着追查这个问题,但我无法理解。
我已经切换到for循环,问题似乎已经消失了。有人能解释发生了什么吗?即使有异常消息,我也不太了解幕后发生的事情。
为什么for循环显然有效?我是否设置了foreach循环错误或什么?
这几乎是我的循环设置方式:
foreach (string data in new List<string>(Foo.Requests))
{
// Process the data.
lock (Foo.Requests)
{
Foo.Requests.Remove(data);
}
}
和
for (int i = 0; i < Foo.Requests.Count; i++)
{
string data = Foo.Requests[i];
// Process the data.
lock (Foo.Requests)
{
Foo.Requests.Remove(data);
}
}
编辑:for *循环是这样的设置,如下所示:
while (running)
{
// [...]
}
编辑:根据要求添加了有关异常的更多信息。
System.ArgumentException: Destination array was not long enough. Check destIndex and length, and the array's lower bounds
at System.Array.Copy (System.Array sourceArray, Int32 sourceIndex, System.Array destinationArray, Int32 destinationIndex, Int32 length) [0x00000]
at System.Collections.Generic.List`1[System.String].CopyTo (System.String[] array, Int32 arrayIndex) [0x00000]
at System.Collections.Generic.List`1[System.String].AddCollection (ICollection`1 collection) [0x00000]
at System.Collections.Generic.List`1[System.String]..ctor (IEnumerable`1 collection) [0x00000]
编辑:锁定的原因是有另一个线程添加数据。此外,最终,多个线程将处理数据(因此,如果整个设置错误,请提供建议)。
编辑:很难找到一个好的答案。我发现埃里克·利珀特的评论值得,但他并没有真正回答(无论如何都投了他的评论)。
帕维尔·米娜耶夫,乔尔·科霍恩和索拉林都给出了我喜欢和投票的答案。索拉林还需要额外的20分钟来编写一些有用的代码。我可以接受所有3并且让它分享声誉但是唉。
Pavel Minaev是下一个应得的人,所以他获得了荣誉。
感谢帮助好人。 :)
答案 0 :(得分:13)
您的问题是List<T>
的构造函数从IEnumerable
创建一个新列表(这就是您所调用的)对于其参数而言不是线程安全的。会发生什么事情是这样的:
new List<string>(Foo.Requests)
正在执行,另一个线程更改Foo.Requests
。你必须在通话期间锁定它。
正如Eric所指出的那样,另一个问题List<T>
并不能保证读者在另一个线程正在改变它时更安全。即并发读者是好的,但并发读者和作者不是。当你把你的写作相互锁定时,你不会把你的读取锁定在你的写入上。
答案 1 :(得分:5)
您的锁定方案已损坏。您需要在整个循环期间锁定Foo.Requests()
,而不仅仅是在删除项目时。否则,在“处理数据”操作过程中,该项可能会变为无效,并且枚举可能会在从一个项目移动到另一个项目之间发生变化。这假设您不需要在此间隔期间插入集合。如果是这种情况,您确实需要重新考虑使用适当的生产者/消费者队列。
答案 2 :(得分:5)
看到你的异常后;它看起来我正在构建浅拷贝时改变Foo.Requests。把它改成这样的东西:
List<string> requests;
lock (Foo.Requests)
{
requests = new List<string>(Foo.Requests);
}
foreach (string data in requests)
{
// Process the data.
lock (Foo.Requests)
{
Foo.Requests.Remove(data);
}
}
话虽如此,我有点怀疑以上是你想要的。如果在处理期间有新请求进入,则当foreach循环终止时,它们将不会被处理。因为我很无聊,所以我认为你正在尝试实现这一点:
class RequestProcessingThread
{
// Used to signal this thread when there is new work to be done
private AutoResetEvent _processingNeeded = new AutoResetEvent(true);
// Used for request to terminate processing
private ManualResetEvent _stopProcessing = new ManualResetEvent(false);
// Signalled when thread has stopped processing
private AutoResetEvent _processingStopped = new AutoResetEvent(false);
/// <summary>
/// Called to start processing
/// </summary>
public void Start()
{
_stopProcessing.Reset();
Thread thread = new Thread(ProcessRequests);
thread.Start();
}
/// <summary>
/// Called to request a graceful shutdown of the processing thread
/// </summary>
public void Stop()
{
_stopProcessing.Set();
// Optionally wait for thread to terminate here
_processingStopped.WaitOne();
}
/// <summary>
/// This method does the actual work
/// </summary>
private void ProcessRequests()
{
WaitHandle[] waitHandles = new WaitHandle[] { _processingNeeded, _stopProcessing };
Foo.RequestAdded += OnRequestAdded;
while (true)
{
while (Foo.Requests.Count > 0)
{
string request;
lock (Foo.Requests)
{
request = Foo.Requests.Peek();
}
// Process request
Debug.WriteLine(request);
lock (Foo.Requests)
{
Foo.Requests.Dequeue();
}
}
if (WaitHandle.WaitAny(waitHandles) == 1)
{
// _stopProcessing was signalled, exit the loop
break;
}
}
Foo.RequestAdded -= ProcessRequests;
_processingStopped.Set();
}
/// <summary>
/// This method will be called when a new requests gets added to the queue
/// </summary>
private void OnRequestAdded()
{
_processingNeeded.Set();
}
}
static class Foo
{
public delegate void RequestAddedHandler();
public static event RequestAddedHandler RequestAdded;
static Foo()
{
Requests = new Queue<string>();
}
public static Queue<string> Requests
{
get;
private set;
}
public static void AddRequest(string request)
{
lock (Requests)
{
Requests.Enqueue(request);
}
if (RequestAdded != null)
{
RequestAdded();
}
}
}
这还有一些问题,我将留给读者:
答案 3 :(得分:2)
三件事:
- 我不会把它们锁在for(每个)声明中,但不在它之外
- 我不会锁定实际的集合,而是锁定本地静态对象
- 您无法修改您要枚举的列表/集合
有关更多信息,请检查:
http://msdn.microsoft.com/en-us/library/c5kehkcz(VS.80).aspx
lock (lockObject) {
foreach (string data in new List<string>(Foo.Requests))
Foo.Requests.Remove(data);
}
答案 4 :(得分:2)
说实话,我建议重构一下。您正在迭代该对象,同时也会迭代该对象。在处理完所有项目之前,你的循环实际上可以退出。
答案 5 :(得分:2)
问题在于表达式
new List<string>(Foo.Requests)
在你的foreach里面,因为它没有锁定。我假设当.NET将您的请求集合复制到新列表时,该列表会被另一个线程修改
答案 6 :(得分:1)
foreach (string data in new List<string>(Foo.Requests))
{
// Process the data.
lock (Foo.Requests)
{
Foo.Requests.Remove(data);
}
}
假设您有两个执行此代码的线程。
在System.Collections.Generic.List1 [System.String] .. ctor
您的锁定方案有误。在for循环示例中甚至是错误的。
每次访问共享资源时都需要锁定 - 甚至是读取或复制它。这并不意味着您需要锁定整个操作。这意味着共享此共享资源的每个人都需要参与锁定方案。
还要考虑防御性复制:
List<string> todos = null;
List<string> empty = new List<string>();
lock(Foo.Requests)
{
todos = Foo.Requests;
Foo.Requests = empty;
}
//now process local list todos
即便如此,所有共享Foo.Requests的人都必须参与锁定方案。
答案 7 :(得分:0)
您正在尝试从迭代列表中删除列表中的对象。 (好的,从技术上讲,你不是这样做的,但这是你想要实现的目标)。
以下是您正确执行的操作:迭代时,构建另一个要删除的条目列表。只需构造另一个(临时)列表,将要从原始列表中删除的所有条目放入临时列表。
List entries_to_remove = new List(...);
foreach( entry in original_list ) {
if( entry.someCondition() == true ) {
entries_to_remove.add( entry );
}
}
// Then when done iterating do:
original_list.removeAll( entries_to_remove );
使用List类的“removeAll”方法。
答案 8 :(得分:0)
我知道这不是你要求的,但仅仅是为了我自己的理智,以下内容代表了你的代码的意图:
private object _locker = new object();
// ...
lock (_locker) {
Foo.Requests.Clear();
}