为什么foreach循环在某些情况下不起作用?

Fak*_*hid 4 c# foreach for-loop exception

我正在使用foreach循环来处理要处理的数据列表(一旦处理就删除了所述数据 - 这是在锁内).此方法偶尔会导致ArgumentException.

抓住它本来是昂贵的,所以我试图追查这个问题,但我无法弄明白.

我已经切换到for循环,问题似乎已经消失了.有人能解释发生了什么吗?即使有异常消息,我也不太了解幕后发生了什么.

为什么for循环显然有效?我是否设置了foreach循环错误或什么?

这几乎是我的循环设置方式:

foreach (string data in new List<string>(Foo.Requests))
{
    // Process the data.

    lock (Foo.Requests)
    {
        Foo.Requests.Remove(data);
    }
}
Run Code Online (Sandbox Code Playgroud)

for (int i = 0; i < Foo.Requests.Count; i++)
{
    string data = Foo.Requests[i];

    // Process the data.

    lock (Foo.Requests)
    {
        Foo.Requests.Remove(data);
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:for*循环是这样的设置,如下所示:

while (running)
{
    // [...]
}
Run Code Online (Sandbox Code Playgroud)

编辑:根据要求添加了有关异常的更多信息.

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]
Run Code Online (Sandbox Code Playgroud)

编辑:锁定的原因是有另一个线程添加数据.此外,最终,多个线程将处理数据(因此,如果整个设置错误,请告知).

编辑:很难找到一个好的答案.

我发现埃里克·利珀特的评论值得,但他并没有真正回答(无论如何都投了他的评论).

Pavel Minaev,Joel Coehoorn和Thorarin都给出了我喜欢和投票的答案.索拉林还花了20分钟写了一些有用的代码.

我可以接受所有这三个并让它分享声誉但唉.

Pavel Minaev是下一个应得的人,所以他获得了荣誉.

感谢帮助好人.:)

Pav*_*aev 13

你的问题是,它的构造函数(List<T>IEnumerable你调用的内容)创建一个新列表对于它的参数是不是线程安全的.会发生什么事情,虽然这个:

 new List<string>(Foo.Requests)
Run Code Online (Sandbox Code Playgroud)

在执行时,另一个线程改变Foo.Requests.你必须在通话期间锁定它.

[编辑]

正如埃里克所指出的那样,另一个问题List<T>并不能保证读者在另一个线程正在改变它时更安全.即并发读者是好的,但并发读者和作者不是.当您将写入锁定为彼此时,您不会将读取锁定在写入上.


Joe*_*orn 5

您的锁定方案已被破坏.您需要锁定Foo.Requests()循环的整个持续时间,而不仅仅是在删除项目时.否则,在"处理数据"操作过程中,该项可能会变为无效,并且枚举可能会在从一个项目移动到另一个项目之间发生变化.这假设您不需要在此间隔期间插入集合.如果是这种情况,您确实需要重新考虑使用适当的生产者/消费者队列.


Tho*_*rin 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

不是问题,但......

话虽如此,我有点怀疑上面是你想要的.如果在处理期间有新请求进入,则当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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这还有一些问题,我将留给读者:

  • 每次处理请求后都应该检查_stopProcessing
  • 如果有多个线程在进行处理,Peek()/ Dequeue()方法将不起作用
  • 封装不足:Foo.Requests是可访问的,但如果您希望处理任何请求,则需要使用Foo.AddRequest添加任何请求.
  • 在多个处理线程的情况下:需要处理循环内的队列为空,因为Count> 0检查周围没有锁定.