我有一个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%左右。
但它们在线程安全方面是否相同?
首选使用哪个版本?
答案 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
。这正是它的用途。