簡體   English   中英

線程和 IEnumerable; “收藏已修改”-異常

[英]Threading and IEnumerable; “Collection Was Modified”-exception

想象一下,在下面的 class 中,一個線程獲取 IEnumerable-object 並開始對元素進行迭代。 在迭代過程中,另一個線程出現並通過 Add 方法向 library_entries 添加一個新條目。 迭代中是否會拋出“集合已修改”異常? 或者鎖會阻止元素的添加,直到迭代完成? 或者兩者都不是?

謝謝!

public static class Library
{
    private static List<string> library_entries = new List<string>(1000000);

    public static void Add(string entry)
    {
        lock (library_entries)
            library_entries.Add(entry);
    }

    public static IEnumerable<string> GetEntries()
    {
        return library_entries.Where(entry => !string.IsNullOrEmpty(entry));
    }
}

不,您不會遇到異常,您使用的是 Linq 查詢。 更糟糕的是,它將無法預料地失敗。 最典型的結果是同一個項目被枚舉了兩次,盡管當 List 在 Add() 調用期間重新分配其內部存儲時,任何事情都是可能的,包括 IndexOutOfRangeException。 每周一次,給予或接受。

調用 GetEntries() 並使用枚舉器的代碼也必須獲取鎖。 鎖定 Where() 表達式還不夠好。 除非您創建列表的副本。

鎖定根本沒有幫助,因為迭代不使用鎖定。 我建議重寫GetEntries() function 以返回副本。

public static IEnumerable<string> GetEntries()
{
    lock(lockObj)
    {
        return library_entries.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
    }
}

請注意,這會返回一致的快照。 即迭代時它不會返回新添加的對象。

而且我更喜歡鎖定私有 object ,其唯一目的是鎖定,但由於列表是私有的,這不是真正的問題,只是一個風格問題。

您也可以像這樣編寫自己的迭代器:

int i=0;
bool MoveNext()
{
  lock(lockObj)
  {
      if(i<list.Count)
          return list[i];
      i++;
  }
}

如果這是一個好主意,取決於您的訪問模式、鎖爭用、列表的大小……您可能還想使用讀寫鎖來避免許多讀取訪問的爭用。

static GetEntries方法不對 static library_entries集合執行任何鎖定 => 它不是線程安全的,並且來自多個線程的任何並發調用都可能中斷。 您已鎖定 Add 方法這一事實很好,但枚舉不是線程安全操作,因此如果您打算同時調用 GetEntries 方法,您也必須鎖定它。 此外,因為此方法返回一個IEnumerable<T> ,它不會在實際列表上執行任何操作,直到您開始枚舉可能在GetEntries方法之外的內容。 因此,您可以在 LINQ 鏈接的末尾添加一個.ToList()調用,然后鎖定整個操作。

是的,會拋出一個異常——你沒有鎖定一個普通的 object。 此外, GetEntries方法中的鎖是沒有用的,因為該調用會立即返回。 迭代時必須發生鎖定。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM