我有一个应用程序内服务,它允许我从各种来源向它提供消息,这些消息将被放入一个简单的列表中。该服务在其自己的线程中运行,将定期将列表中的所有消息处理到各种文件中;每个源对应一个文件,然后管理其大小。
我的问题是关于检查消息的正确方法,并围绕访问列表的代码执行锁定。只有两个地方可以访问该列表;一个是将消息添加到列表的位置,另一个是将消息从列表转储到处理列表中的位置。
将消息添加到列表:
Public Sub WriteMessage(ByVal messageProvider As IEventLogMessageProvider, ByVal logLevel As EventLogLevel, ByVal message As String)
SyncLock _SyncLockObject
_LogMessages.Add(New EventLogMessage(messageProvider, logLevel, Now, message))
End SyncLock
End Sub
处理列表:
Dim localList As New List(Of EventLogMessage)
SyncLock _SyncLockObject
If (_LogMessages.Count > 0) Then
localList.AddRange(_LogMessages)
_LogMessages.Clear()
End If
End SyncLock
' process list into files...
我的问题是:我应该在处理列表时进行仔细检查吗,见下文?为什么呢?或者为什么不呢?在锁之外访问列表的计数属性是否有任何危险?这两种方法都是更好还是更有效?为什么呢?或者为什么不呢?
Dim localList As New List(Of EventLogMessage)
If (_LogMessages.Count > 0) Then
SyncLock _SyncLockObject
If (_LogMessages.Count > 0) Then
localList.AddRange(_LogMessages)
_LogMessages.Clear()
End If
End SyncLock
End If
' process list into files...
我知道在这种特殊情况下,如果我进行仔细检查可能无关紧要,因为在处理功能之外,列表只能增长。但这是我的工作示例,我正在尝试了解线程的更精细细节。
提前感谢您的任何见解...
经过一些进一步的研究,谢谢你"浣熊",还有一些实验性编程,我有一些进一步的想法。
关于ReaderWriterLockSlim,我有以下示例,它似乎工作正常。它允许我读取列表中的消息数,而不会干扰可能尝试读取列表中消息数或消息本身的其他代码。当我想处理列表时,我可以将我的锁升级到写入模式,将消息转储到处理列表中并在任何读/写锁之外处理它们,从而不会阻塞任何其他可能想要添加或读取更多消息的线程。
请注意,此示例对消息使用更简单的结构,即 String,而不是前面的示例使用 Type 以及其他一些元数据。
Private _ReadWriteLock As New Threading.ReaderWriterLockSlim()
Private Sub Process()
' create local processing list
Dim processList As New List(Of String)
Try
' enter read lock mode
_ReadWriteLock.EnterUpgradeableReadLock()
' if there are any messages in the 'global' list
' then dump them into the local processing list
If (_Messages.Count > 0) Then
Try
' upgrade to a write lock to prevent others from writing to
' the 'global' list while this reads and clears the 'global' list
_ReadWriteLock.EnterWriteLock()
processList.AddRange(_Messages)
_Messages.Clear()
Finally
' alway release the write lock
_ReadWriteLock.ExitWriteLock()
End Try
End If
Finally
' always release the read lock
_ReadWriteLock.ExitUpgradeableReadLock()
End Try
' if any messages were dumped into the local processing list, process them
If (processList.Count > 0) Then
ProcessMessages(processList)
End If
End Sub
Private Sub AddMessage(ByVal message As String)
Try
' enter write lock mode
_ReadWriteLock.EnterWriteLock()
_Messages.Add(message)
Finally
' always release the write lock
_ReadWriteLock.ExitWriteLock()
End Try
End Sub
我看到这种技术的唯一问题是开发人员必须勤奋地获取和释放锁。否则,将发生死锁。
至于这是否比使用SyncLock更有效,我真的不能说。对于这个特定的例子及其用法,我相信任何一个都足够了。我不会做双重检查,因为"浣熊"给出的关于在其他人更改计数时阅读计数的原因。鉴于此示例,SyncLock 将提供相同的功能。然而,在一个稍微复杂的系统中,一个多个源可以读取和写入列表的系统,ReaderWriterLockSlim将是理想的选择。
关于阻止集合列表,以下示例的工作方式与上述示例类似。
Private _Messages As New System.Collections.Concurrent.BlockingCollection(Of String)
Private Sub Process()
' process each message in the list
For Each item In _Messages
ProcessMessage(_Messages.Take())
Next
End Sub
Private Sub AddMessage(ByVal message As String)
' add a message to the 'global' list
_Messages.Add(message)
End Sub
简单本身...
理论:
一旦线程获取了_SyncLockObject
锁,重新进入该方法的所有其他线程都必须等待锁被释放。
因此,锁定前后的检查无关紧要。换句话说,它不会有任何效果。它也不安全,因为您没有使用并发列表。
如果一个线程碰巧在第一次测试中检查Count
,而另一个线程正在清除或添加到集合中,那么您将收到异常,其中包含 Collection was modified; enumeration operation may not execute.
.此外,第二次检查一次只能由一个线程执行(因为它已同步)。
这也适用于您的 Add 方法。虽然锁由一个线程拥有(意味着执行流已到达该行),但其他线程将无法处理或添加到列表中。
如果您只是从应用程序中其他位置的列表读取,则还应小心锁定。对于更复杂的读/写方案(例如自定义并发集合),我建议使用 ReaderWriterLockSlim
。
实践:
使用 BlockingCollection,因为它是线程安全的(即它在内部处理并发)。