Должен ли я перепроверить до и после блокировки списка?

У меня есть сервис в приложении, который позволяет мне кормить его сообщениями из разных источников, которые будут помещены в простой список. Служба, работающая в своем собственном потоке, будет периодически обрабатывать все сообщения в списке в различные файлы; один файл для каждого источника, который затем управляется по размеру.

Мой вопрос касается правильного способа проверки сообщений и выполнения блокировки кода, который обращается к списку. Есть только два места, которые получают доступ к списку; один - это место, где сообщение добавляется в список, а второй - место, где сообщения сбрасываются из списка в список обработки.

Добавление сообщения в список:

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...

Мои вопросы: должен ли я выполнить двойную проверку при обработке списка, см. Ниже? И почему? Или почему нет? И есть ли опасности в доступе к свойству count списка за пределами блокировки? Один из методов лучше или эффективнее? И почему? Или почему нет?

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, в отличие от предыдущего примера, в котором использовался тип вместе с некоторыми другими метаданными.

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 будет идеальным.


Что касается списка BlockingCollection, следующий пример работает аналогично приведенному выше.

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

Сама простота…

1 ответ

Решение

Теория:

Как только поток получает _SyncLockObject заблокируйте все остальные потоки, повторно входящие в этот метод, придется ждать снятия блокировки.

Поэтому проверка до и после блокировки не имеет значения. Другими словами, это не будет иметь никакого эффекта. Это также не безопасно, потому что вы не используете параллельный список.

Если один поток случится проверить Count в первом тесте, пока другой очищает или добавляет в коллекцию, вы получите исключение с Collection was modified; enumeration operation may not execute., Кроме того, вторая проверка может выполняться только одним потоком за раз (поскольку она синхронизирована).

Это относится и к вашему методу Add. Хотя блокировка принадлежит одному потоку (то есть поток выполнения достиг этой строки), никакие другие потоки не смогут обработать или добавить в список.

Вы должны быть осторожны, чтобы также заблокировать, если вы просто читаете из списка в некоторых других местах в вашем приложении. Для более сложных сценариев чтения / записи (таких как настраиваемая одновременная коллекция) я рекомендую использовать ReaderWriterLockSlim,

Практика:

Используйте BlockingCollection, так как он является потокобезопасным (то есть он обрабатывает параллелизм внутри).

Другие вопросы по тегам