ConcurrentQueue(Of T)VS List(Of T)与多线程应用程序中的Synclock语句

时间:2016-03-04 20:35:54

标签: .net vb.net multithreading list

我有一个Public Shared queItems As Queue(Of String),当线程想要删除并使用Dequeue在队列开头返回字符串时,它被许多后台线程使用;

Public Function NextItem() As String
    Dim _item As String = Nothing
    SyncLock Form1.queItems
        If Form1.queItems.Count = 0 Then Return Nothing
        _item = Form1.queItems.Dequeue()
        Form1.queItems.Enqueue(_item)
    End SyncLock

    Return _item
End Function

我后来介绍了ConcurrentQueue(Of T) Class我使用NextItem() As String制作了下一版Public Shared queItems As ConcurrentQueue(Of String),如下所示:

Public Function NextItem2() As String
    Dim _item As String = Nothing
here:
    If Form1.queItems.Count = 0 Then Return Nothing
    If Not Form1.queItems.TryDequeue(_item) Then
        Thread.Sleep(100)
        GoTo here
    End If

   Return _item
End Function

我的机器中第一个版本比下一个版本快20%左右。

但它们在线程安全方面是否相同?

首选使用哪个版本?

5 个答案:

答案 0 :(得分:1)

With the old queue, you usually do this (pseudocode)

Public class SyncQueue

    private _queue As new Queue(of String)
    private shared _lock As New Object() 

    Public Shared Function Dequeue() As String

        Dim item As String = Nothing
        SyncLock _lock
            If _queue.Count > 0 Then
                _queue.Dequeue(item)
            End If
            Return item
        End SyncLock
    End Function

    Public Shared Sub Enqueu(item as String)
        SyncLock _lock
            _queue.Enqueue(item)
        End SyncLock
    End Sub 

End Class

Whilst with ConcurrentQueue(Of T) all this taken care for you. And you should use Try... methods

If (queue.TryDequeue(item)) Then
    ' do something with the item    
End If

答案 1 :(得分:1)

我愿意猜测第二个版本较慢的原因是你使用不正确。如果您使用TryDequeue,则无需检查Count,它会为您处理。

修复代码:

Public Function NextItem2() As String
    Dim _item As String
    Form1.queItems.TryDequeue(_item)
    Return _item
End Function

从ConcurrentQueue读取计数实际上非常昂贵。

将项添加到队列时,只需查看队列的一端即可。删除项目时,它只会查看另一端。这意味着您可以添加一个线程并删除另一个线程,它们不会相互干扰(除非队列为空)。

当你想要计数时,它实际上必须查看两端并计算它们之间的项目。这反过来要求锁定两端,或连续重试,直到它得到一个干净的读取。 (您可以自己检查代码,但很难理解。)

http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentQueue.cs

这就是他们用于TryDequeue的代码:

 public bool TryDequeue(out T result)
    {
        while (!IsEmpty)
        {
            Segment head = m_head;
            if (head.TryRemove(out result))
                return true;
            //since method IsEmpty spins, we don't need to spin in the while loop
        }
        result = default(T);
        return false;
    }

如您所见,如果队列为空,它会将result参数设置为0 / null。因此我们不需要明确检查函数的返回值。

现在,如果你想要一个不同的默认值,那么你会写:

Public Function NextItem2() As String
    Dim _item As String
    If Not Form1.queItems.TryDequeue(_item) Then _item = [my default]
    Return _item
End Function

答案 2 :(得分:1)

并发集合在内部实现分区以避免必须锁定集合。这意味着在有许多线程排队和出队(高争用)的情况下,集合的一部分可以被“锁定”,而其他部分(想象它像数据块)仍然可以访问。使用简单的SyncLock是不可能的,因为这样做会锁定整个集合,从而阻止其他线程的访问。在只涉及少数线程的情况下,SyncLock和列表可能会更快,因为您绕过了对集合进行分区的开销。在你有8个或更多线程同时竞争同一个集合的情况下,并发集合很可能比list + synclock快几个数量级。

http://www.codethinked.com/net-40-and-system_collections_concurrent_concurrentqueue

编辑:我实际上只是使用以下方法在我的双核机器上测试了这个:

Imports System.Threading

Public Class Form1

    Private _lock As New Object

    Private _queue As New Queue(Of Integer)

    Private _concurrentQueue As New Concurrent.ConcurrentQueue(Of Integer)


    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load

        Dim sw As New Stopwatch()

        Dim lstResQueue As New List(Of Integer)
        Dim lstResConcurrent As New List(Of Integer)

        For i = 1 To 10
            Dim t As New Thread(AddressOf TestLockedQueue)

            sw.Start()
            t.Start()
            While t.IsAlive : Thread.Sleep(0) : End While
            sw.Stop()

            lstResQueue.Add(sw.ElapsedMilliseconds)

            sw.Reset()

            t = New Thread(AddressOf TestConcurrentQueue)

            sw.Start()
            t.Start()
            While t.IsAlive : Thread.Sleep(0) : End While
            sw.Stop()

            lstResConcurrent.Add(sw.ElapsedMilliseconds)
        Next

        MessageBox.Show(String.Format("Average over 10 runs: " & vbCrLf & _
                                      "Queue(Of Integer) with lock: {0}" & vbCrLf & _
                                      "ConcurrentQueue(Of Integer): {1}",
                                      lstResQueue.Average, lstResConcurrent.Average))
    End Sub


    Private Sub TestLockedQueue()
        Parallel.For(0, 5000000,
                     New ParallelOptions() With {.MaxDegreeOfParallelism = 16},
                     Sub(i)
                         Dim a = 0

                         SyncLock _lock
                             Try
                                 _queue.Enqueue(i)
                             Catch ex As Exception
                             End Try
                         End SyncLock

                         SyncLock _lock
                             Try
                                 a = _queue.Dequeue()
                             Catch ex As Exception
                             End Try
                         End SyncLock

                         Dim b = a
                     End Sub)
    End Sub

    Private Sub TestConcurrentQueue()
        Parallel.For(0, 5000000,
                     New ParallelOptions() With {.MaxDegreeOfParallelism = 16},
                     Sub(i)
                         Dim a = 0

                         Try
                             _concurrentQueue.Enqueue(i)
                         Catch ex As Exception
                         End Try

                         _concurrentQueue.TryDequeue(a)

                         Dim b = a
                     End Sub)
    End Sub

End Class

ConcurrentQueue的速度总是快两倍。在更强大的8核处理器上看到结果会很有趣。我的结果是10秒而不是4.5秒。我猜测ConcurrentQueue的扩展性不会超过2个核心,而ConcurrentBag将会一直扩展。

答案 3 :(得分:1)

我正在尝试破译你的代码。有很多奇怪的事情在发生。

在第一个块中,您正在一个名为Shared的实例方法中从Form1类访问NextItem()队列。目前尚不清楚NextItem()课程或其他地方是否定义了Form1。通过这种设计,您似乎预期多个Form1实例(或定义了NextItem()的类)共享单个队列。这有点奇怪。

我将假设您可以为队列使用实例变量。

此外,您在出列后立即入队。这似乎也错了。

所以考虑到这一点,我认为你的第一个方法看起来应该像线程安全一样:

Private _queItems As Queue(Of String)
Private _queItemsLock As New Object()

Public Function NextItem() As String
    SyncLock _queItemsLock
        If _queItems.Count = 0 Then
            Return _queItems.Dequeue()
        Else
            Return Nothing
        End If
    End SyncLock
End Function

在你的第二段代码中,你有很多事情会让它有点混乱,但这就是我认为应该是这样的:

Private _queItems As ConcurrentQueue(Of String)

Public Function NextItem2() As String
    Dim _item As String = Nothing
    If _queItems.TryDequeue(_item) Then
        Return _item
    Else
        Return Nothing
    End If
End Function

现在,尽管如此,我认为除了Queue(Of T)ConcurrentQueue(Of String)之外,还有更好的选择。类BufferBlock(Of String)(在System.Threading.Tasks.Dataflow名称空间中)可能更容易使用。

您可以定义此代码:

Private _bufferBlock As New BufferBlock(Of String)()

'Blocking call, but immediately returns with value or `Nothing`
Public Function NextItem3() As String
    Dim _item As String = Nothing
    If _bufferBlock.TryReceive(_item) Then
        Return _item
    Else
        Return Nothing
    End If
End Function

'Blocking call - waits for value to be available
Public Function NextItem4() As String
    Return _bufferBlock.Receive()
End Function

'Awaitable call
Public Async Function NextItem5() As Task(Of String)
    Return Await _bufferBlock.ReceiveAsync()
End Function

...然后你可以像这样使用它:

Async Sub Main
    Dim t1 = Task.Factory.StartNew(Function () NextItem4())
    Dim t2 = Task.Factory.StartNew(Function () NextItem4())
    _bufferBlock.Post("Alpha")
    _bufferBlock.Post("Beta")
    _bufferBlock.Post("Gamma")
    _bufferBlock.Post("Delta")
    Dim x = Await NextItem5()
    Dim y = Await _bufferBlock.ReceiveAsync()
    Console.WriteLine(t1.Result)
    Console.WriteLine(t2.Result)
    Console.WriteLine(x)
    Console.WriteLine(y)
End Sub

......这让我:

Alpha
Beta
Gamma
Delta

注意:有时结果的顺序会发生变化,因为此代码的异步性质意味着t1& t2可以按任何顺序排列。

就我个人而言,我认为调用Await _bufferBlock.ReceiveAsync()的能力最有可能简化您的代码。

在一天结束的时候,我会看看你提出的任何方法,并及时确定哪些方法更快。我们可以为您的代码计时,因为我们没有它。

答案 4 :(得分:1)

在不知道发生了什么的情况下,我怀疑第二个更慢,因为它故意睡了100毫秒。很难从中了解这种情况经常发生的情况。拿出来再次尝试测试。我敢打赌,20%的差异会消失。

如果您要同时进行排队和出队,我会使用ConcurrentQueue。这正是它的用途。