簡體   English   中英

這是否使用靜態隊列線程安全?

[英]Is this use of a static queue thread-safe?

msdn文檔聲明靜態通用隊列是線程安全的。 這是否意味着以下代碼是線程安全的? 換句話說,當一個線程將一個int排隊並且另一個線程同時使一個int出局時是否存在問題? 我是否必須為線程安全鎖定Enqueue和Dequeue操作?

class Test {
    public static Queue<int> queue = new Queue<int>(10000);

    Thread putIntThread;
    Thread takeIntThread;

    public Test() {
        for(int i = 0; i < 5000; ++i) {
            queue.Enqueue(0);
        }
        putIntThread = new Thread(this.PutInt);
        takeIntThread = new Thread(this.TakeInt);
        putIntThread.Start();
        takeIntThread.Start();
    }

    void PutInt() {
        while(true)
        {
            if(queue.Count < 10000) {//no need to lock here as only itself can change this condition
                queue.Enqueue(0);
            }
        }
    }

    void TakeInt() {
        while(true) {
            if(queue.Count > 0) {//no need to lock here as only itself can change this condition
                queue.Dequeue();
            }
        }
    }

}

編輯:我必須使用.NET 3.5

這絕對不是線程安全的。 來自Queue<T>的文檔。

此類型的公共靜態(在Visual Basic中為Shared)成員是線程安全的。 任何實例成員都不保證是線程安全的。

只要未修改集合, Queue<T>就可以同時支持多個讀取器。 即便如此,通過集合枚舉本質上不是一個線程安全的過程。 為了在枚舉期間保證線程安全,您可以在整個枚舉期間鎖定集合。 要允許多個線程訪問集合以進行讀寫,您必須實現自己的同步。

重讀你的問題,你似乎對短語“這種類型的靜態成員”感到困惑 - 它不是在談論“靜態隊列”,因為沒有這樣的東西。 對象不是靜態的或不是 - 成員是。 當談到靜態成員時,它正在談論像Encoding.GetEncodingQueue<T>實際上沒有任何靜態成員)之類的東西。 實例成員是EnqueueDequeue類的東西 - 與類型實例相關的成員而不是類型本身。

因此,您需要為每個操作使用鎖定,或者如果您使用的是.NET 4,請使用ConcurrentQueue<T>

是的,正如這里所說的,靜態實例的實例成員與靜態成員不同,並且只有后者才能保證線程安全,因此您必須鎖定入隊和出列操作。

如果鎖定被證明是一個瓶頸,那么隊列是以無鎖方式寫入的更簡單的集合之一,只要一個不需要Queue<T>提供的完整ICollection<T>實現:

internal sealed class LockFreeQueue<T>
{
  private sealed class Node
  {
    public readonly T Item;
    public Node Next;
    public Node(T item)
    {
      Item = item;
    }
  }
  private volatile Node _head;
  private volatile Node _tail;
  public LockFreeQueue()
  {
    _head = _tail = new Node(default(T));
  }
#pragma warning disable 420 // volatile semantics not lost as only by-ref calls are interlocked
  public void Enqueue(T item)
  {
    Node newNode = new Node(item);
    for(;;)
    {
      Node curTail = _tail;
      if (Interlocked.CompareExchange(ref curTail.Next, newNode, null) == null)   //append to the tail if it is indeed the tail.
      {
        Interlocked.CompareExchange(ref _tail, newNode, curTail);   //CAS in case we were assisted by an obstructed thread.
        return;
      }
      else
      {
        Interlocked.CompareExchange(ref _tail, curTail.Next, curTail);  //assist obstructing thread.
      }
    }
  }    
  public bool TryDequeue(out T item)
  {
    for(;;)
    {
      Node curHead = _head;
      Node curTail = _tail;
      Node curHeadNext = curHead.Next;
      if (curHead == curTail)
      {
        if (curHeadNext == null)
        {
          item = default(T);
          return false;
        }
        else
          Interlocked.CompareExchange(ref _tail, curHeadNext, curTail);   // assist obstructing thread
      }
      else
      {
        item = curHeadNext.Item;
        if (Interlocked.CompareExchange(ref _head, curHeadNext, curHead) == curHead)
        {
          return true;
        }
      }
    }
  }
#pragma warning restore 420
}

此隊列只有EnqueueTryDequeue (如果隊列為空,則返回false)方法。 使用互鎖增量和減量添加Count屬性是微不足道的(確保在實際屬性中易於讀取count字段),但除此之外,添加任何無法寫入委托給其中一個成員的內容變得相當棘手已經定義,或者在構造期間發生(在這種情況下,你將只有一個線程在那時使用它,除非你做了一些非常奇怪的事情)。

實現也是無等待的,好像一個線程的動作不會阻止另一個線程進行(如果一個線程在第二個線程嘗試這樣做時進入排隊過程的一半,第二個線程將完成第一個線程)線程的工作)。

盡管如此,我還是要等到鎖定實際證明是一個瓶頸(除非你只是在嘗試;與異國情調一起玩,與熟悉的人一起工作)。 實際上,在許多情況下,這將證明比鎖定Queue<T>更昂貴,特別是因為它不太擅長將項目保持在內存中彼此靠近,所以你會發現接近連續的大量操作不太適合那個原因。 鎖定通常非常便宜,只要沒有頻繁的鎖定爭用。

編輯:

我現在有時間補充說明上述方法的工作原理。 我通過閱讀其他人的相同想法的版本來寫這篇文章,為自己寫這個想法來復制這個想法,然后與我之后閱讀的版本進行比較,並發現這是一個非常有用的練習。

讓我們從非鎖定免費實現開始。 這是一個單獨的鏈表。

internal sealed class NotLockFreeYetQueue<T>
{
  private sealed class Node
  {
    public readonly T Item;
    public Node Next{get;set;}
    public Node(T item)
    {
      Item = item;
    }
  }
  private Node _head;
  private Node _tail;
  public NotLockFreeYetQueue()
  {
    _head = _tail = new Node(default(T));
  }
  public void Enqueue(T item)
  {
    Node newNode = new Node(item);
    _tail.Next = newNode;
    _tail = newNode;
  }
  public bool TryDequeue(out T item)
  {
      if (_head == _tail)
      {
          item = default(T);
          return false;
      }
      else
      {
        item = _head.Next.Item;
        _head = _head.Next;
        return true;
      }
  }
}

到目前為止關於實施的一些注釋。

ItemNext可以合理地為字段或屬性。 因為它是一個簡單的內部類,一個必須readonly而另一個是“啞”讀寫(在getter或setter中沒有邏輯),這里真的沒什么可供選擇的。 我做了Next的屬性在這里純粹是因為那是不會以后工作了,我想談的是,當我們到達那里。

_head_tail開始指向一個sentinel而不是null可以通過不必為空隊列設置特殊情況來簡化事情。

因此,在成為新尾部之前,排隊將創建一個新節點並將其設置為_tailNext屬性。 出列將檢查是否為空,如果它不為空,則從頭節點獲取值並將head設置為舊頭的Next屬性的節點。

此時需要注意的另一件事是,由於新節點是根據需要而不是在預先分配的數組中創建的,因此在正常使用中它將比Queue<T>具有更低的性能。 這不會變得更好,事實上我們現在要做的每件事都會使單線程性能變差。 同樣,只有在激烈的爭論中,這才會擊敗鎖定的Queue<T>

讓我們排隊無鎖。 我們將使用Interlocked.CompareExchange() 這將第一個參數與第三個參數進行比較,如果它們相等,則將第一個參數設置為第二個參數。 在任何情況下,它都返回舊值(無論是否覆蓋)。 比較和交換是作為一個原子操作完成的,因此本身就是線程安全的,但是我們需要做更多的工作來使這些操作的組合也是線程安全的。

CompareExchange和其他語言中的等價物有時縮寫為CAS(用於Compare-And-Swap)。

使用它們的常用方法是循環,我們首先獲取通過正常讀取重寫的值(請記住.NET讀取的32位值,較小的值和引用類型始終是原子的)並嘗試覆蓋它如果它沒有改變,循環直到我們成功:

private sealed class Node
{
  public readonly T Item;
  public Node Next;
  public Node(T item)
  {
    Item = item;
  }
}
/* ... */
private volatile Node _tail;
/* ... */
public void Enqueue(T item)
{
  Node newNode = new Node(item);
  for(;;)
  {
    Node curTail = _tail;
    if(Interlocked.CompareExchange(ref curTail.Next, newNode, null) == null)
    {
      _tail = newNode;
      return;
    }
  }
}

我們想要添加到尾部的Next只有它是null - 如果沒有寫入它的另一個線程。 所以,我們做一個只有在這種情況下才會成功的CAS。 如果是,我們將_tail設置為新節點,否則我們再試一次。

接下來必須改為成為一個工作領域,我們不能用屬性來做。 我們還使_tail volatile以便_tail在所有CPU緩存中都是新鮮的( CompareExchange具有易失性語義,因此它不會因為缺乏波動性而被打破,但它可能會經常旋轉而不是必要,我們將會做更多的事情_tail也)。

這是無鎖的,但不是等待的。 如果一個線程到CAS,但還沒有寫入_tail,然后暫時沒有任何CPU時間,那么所有其他嘗試入隊的線程都將繼續循環,直到它被調度並設法這樣做。 如果線程被中止或暫停很長時間,這將導致一種永久的活鎖。

因此,如果我們處於CAS失敗的狀態,我們就處於這種狀況。 我們可以通過執行其他線程的工作來解決這個問題:

  for(;;)
  {
    Node curTail = _tail;
    if(Interlocked.CompareExchange(ref curTail.Next, newNode, null) == null)
    {
      Interlocked.CompareExchange(ref _tail, newNode, curTail);   //CAS in case we were assisted by an obstructed thread.

      return;
    }
    else
    {
      Interlocked.CompareExchange(ref _tail, curTail.Next, curTail);  //assist obstructing thread.
    }
  }

現在,在大多數情況下,寫入curTail.Next的線程會將新節點分配給_tail - 但是如果它已經完成則通過CAS。 但是,另一個線程無法寫入curtail.Next它可以嘗試將curTail.Next分配給_tail來完成第一個線程的工作並繼續使用它。

所以,一個無鎖,無等待的入隊。 是時候出發了。 首先讓我們考慮一下我們不懷疑隊列是空的情況。 就像入隊一樣,我們將首先獲得我們感興趣的節點的本地副本; _head_tail_head.Next (對於空隊列,再次不使用_head.Next或尾部會使生活更輕松;這意味着在任何狀態下讀取_head.Next都是安全的)。 與入隊一樣,我們將依賴於波動性,這次不僅僅是_tail ,而是_head ,因此我們將其更改為:

private volatile Node _head;

我們將TryDequeue更改為:

  public bool TryDequeue(out T item)
  {
      Node curHead = _head;
      Node curTail = _tail;
      Node curHeadNext = curHead.Next;
      if (_head == _tail)
      {
          item = default(T);
          return false;
      }
      else
      {
        item = curHeadNext.Item;
        if (Interlocked.CompareExchange(ref _head, curHeadNext, curHead) == curHead)
          return true;
      }
  }

空隊列的情況現在不正確,但我們會回過頭來看。 將item設置為curHeadNext.Item是安全的,就好像我們沒有完成操作一樣,我們將再次覆蓋它,但我們必須使操作寫入_head原子並保證僅在_head沒有改變時才發生。 如果沒有,那么_head已被另一個線程更新,我們可以再次循環(不需要為該線程工作,它已經完成了所有會影響我們的事情)。

現在考慮如果_head == _tail會發生什么。 可能它是空的,但可能_tail.Next (與curHeadNext相同)由enqueue寫入。 在這種情況下,我們更可能想要的不是空quque的結果,而是我們將部分排隊的項目出列的結果。 所以,我們協助該線程並再次繼續循環:

if (curHead == curTail)
{
    if (curHeadNext == null)
    {
        item = default(T);
        return false;
    }
    else
        Interlocked.CompareExchange(ref _tail, curHeadNext, curTail);
}

最后,剩下的唯一問題是我們不斷收到420個警告,因為我們將volatile字段傳遞給byref方法。 這通常會阻止volatile語義(因此警告),但不會使用CompareExchange (因此我們這樣做)。 我們可以禁用警告,包括注釋來解釋我們為什么這樣做(我嘗試永遠不會在沒有正當評論的情況下禁用警告)並且我們已經提供了之前給出的代碼。

請注意,我們在GC支持框架中執行此操作非常重要。 如果我們不得不處理釋放,那將會變得更加復雜。

MSDN聲明的是Queue的靜態方法是線程安全的,而不是靜態實例的實例方法是線程安全的。

是的,你必須像MSDN所說的那樣鎖定

要允許多個線程訪問集合以進行讀寫,您必須實現自己的同步。

暫無
暫無

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

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