代码之家  ›  专栏  ›  技术社区  ›  Fake Code Monkey Rashid

为什么foreach循环在某些情况下不能工作?

  •  4
  • Fake Code Monkey Rashid  · 技术社区  · 16 年前

    我使用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]
    

    编辑:锁定的原因是有另一个线程在添加数据。而且,最终会有多个线程在处理数据(因此,如果整个设置错误,请给出建议)。

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

    我觉得埃里克·利珀特的评论是值得的,但他并没有真正回答(无论如何都投了赞成票)。

    帕维尔·米纳耶夫、乔尔·科霍恩和图拉林都给出了我喜欢的答案,而且都投了赞成票。Thorarin还花了20分钟来编写一些有用的代码。

    我可以接受这三个,并让它分拆声誉,但遗憾的是。

    帕维尔·米纳耶夫是下一个当之无愧的人,所以他得到了荣誉。

    谢谢你的帮助,好人。:)

    9 回复  |  直到 16 年前
        1
  •  13
  •   Pavel Minaev    16 年前

    你的问题是 List<T> 从中创建新列表 IEnumerable (这就是你所说的)就其论点而言,是不安全的。当以下情况发生时:

     new List<string>(Foo.Requests)
    

    正在执行,另一个线程更改 Foo.Requests . 在通话期间,您必须将其锁定。

    [编辑]

    正如埃里克指出的,另一个问题 列表& T; 当另一个线程正在改变它时,也不能保证读者可以安全地阅读。也就是说,并发读卡器可以,但并发读卡器和并发写卡器不行。当你把你写的东西锁在一起的时候,你不会把你读的东西锁在你写的东西上。

        2
  •  5
  •   Joel Coehoorn    16 年前

    你的锁定方案被破坏了。你需要锁上 Foo.Requests() 在循环的整个持续时间内,而不仅仅是在移除项时。否则,在“处理数据”操作和枚举之间可能会在从一个项移动到另一个项之间发生变化的过程中,该项可能会变得无效。这假设您在这个时间间隔内也不需要插入集合。如果是这样的话,您真的需要重新考虑使用适当的生产者/消费者队列。

        3
  •  5
  •   Thorarin    16 年前

    在看到您的异常之后,我发现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();
            }
        }
    }
    

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

    • 在每次处理请求之后,都应该检查停止处理
    • 如果有多个线程正在进行处理,peek()/dequeue()方法将不起作用。
    • 封装不足:foo.requests是可访问的,但如果您希望处理请求,则需要使用foo.addrequest添加任何请求。
    • 在多个处理线程的情况下:需要处理循环中的队列为空,因为计数>0检查周围没有锁。
        4
  •  2
  •   DigitalZebra    16 年前

    说实话,我建议重构一下。您正在从对象中删除项,同时还要对其进行迭代。在处理完所有项之前,循环实际上可以退出。

        5
  •  2
  •   Yvo    16 年前

    三件事:
    -我不会把它们锁在for(each)语句中,而是锁在它之外。
    -我不会锁定实际集合,而是锁定本地静态对象
    -不能修改正在枚举的列表/集合

    有关详细信息,请检查:
    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);
    }
    
        6
  •  2
  •   chris166    16 年前

    问题是表达式

    new List<string>(Foo.Requests)
    

    前臂里面,因为它没有锁。我假设当.NET将您的请求集合复制到一个新列表中时,该列表被另一个线程修改。

        7
  •  1
  •   Amy B    16 年前
    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

    • 线程1开始处理列表。
    • thread2调用list构造函数,它为要创建的数组取一个计数。
    • thread1更改列表中的项目数。
    • 线程2的项目数错误。

    您的锁定方案错误。在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.request的人都必须参与锁定方案。

        8
  •  0
  •   Valters Vingolds jpkroehling    16 年前

    在遍历列表时,您正在尝试从列表中删除对象。(好吧,从技术上讲,你不是这样做的,但这是你想要达到的目标)。

    以下是正确操作的方法:在迭代时,构造另一个要删除的条目列表。只需构造另一个(临时)列表,将要从原始列表中删除的所有条目放入临时列表。

    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 );
    

    使用列表类的“removeall”方法。

        9
  •  0
  •   John Saunders    16 年前

    我知道这不是你要求的,只是为了我自己的理智,以下是否代表了你的代码的意图:

    private object _locker = new object();
    
    // ...
    
    lock (_locker) {
        Foo.Requests.Clear();
    }