我应该在锁定列表之前和之后仔细检查吗?



我有一个应用程序内服务,它允许我从各种来源向它提供消息,这些消息将被放入一个简单的列表中。该服务在其自己的线程中运行,将定期将列表中的所有消息处理到各种文件中;每个源对应一个文件,然后管理其大小。

我的问题是关于检查消息的正确方法,并围绕访问列表的代码执行锁定。只有两个地方可以访问该列表;一个是将消息添加到列表的位置,另一个是将消息从列表转储到处理列表中的位置。

将消息添加到列表:

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,因为它是线程安全的(即它在内部处理并发)。

最新更新