Matt Wilko
Matt Wilko

Reputation: 27322

How to make an iteration of a list thread safe

I have a class that contains a Dictionary that is loaded from the DB when the LoadFromDb method is called, this can be called many times from any number of threads.

I then have an Employees property which may be called by any thread.

As an exmaple:

Public Class Employees
    Private _employeesDictionary As New SortedDictionary(Of Int32, Employee)

    Public ReadOnly Property Employees As IList(Of Employee)
        Get
            Return _employeesDictionary.Values.ToList
        End Get
    End Property

    Public Sub loadFromDb()
        Static rnd As New Random
        _employeesDictionary = New SortedDictionary(Of Int32, Employee)

        'generate a random number of employees
        Dim numEmpsToAdd = rnd.Next(101)
        For i = 0 To numEmpsToAdd
                _employeesDictionary.Add(i + 1, New Employee With {.ClockNo = i + 1, .Name = $"Name:{i + 1}"})
        Next

    End Sub
End Class

I want to ensure that LoadFromDb can only be called once at any one time. Additional calls should abort if there is a lock.

However I want to also lock the Employees property from being read at this point, but when LoadFromDb is not being called I want to allow multiple threads to read the values.

This is basically the solution, but I want to know how I can achieve this using SyncLock, Mutex, Semaphore, or something else which will ensure thread safety. I initially tried a SyncLock, but this blocks multiple threads from accessing the Employees property when LoadFromDb is not being called:

Public Class Employees
    Private _employeesDictionary As New SortedDictionary(Of Int32, Employee)
    Private locked As Boolean
    Public ReadOnly Property Employees As IList(Of Employee)
        Get
            Do While locked
                'wait for lock to be released
            Loop
            Return _employeesDictionary.Values.ToList
        End Get
    End Property

    Public Sub loadFromDb()
        Static rnd As New Random
        If locked Then Exit Sub
        locked = True
        Try
            _employeesDictionary = New SortedDictionary(Of Int32, Employee)

            'generate a random number of employees
            Dim numEmpsToAdd = rnd.Next(101)
            For i = 0 To numEmpsToAdd
                _employeesDictionary.Add(i + 1, New Employee With {.ClockNo = i + 1, .Name = $"Name:{i + 1}"})
            Next
        Catch ex As Exception
            Throw ex
        Finally
            locked = False
        End Try
    End Sub
End Class

Upvotes: 0

Views: 60

Answers (1)

dbasnett
dbasnett

Reputation: 11773

Using Threading.Monitor methods might be the best approach. Not tested for obvious reasons. You do know that there is a possibility of generating zero records because of how the rnd.Next is called.

Public Class Employees
    Private _employeesDictionary As New SortedDictionary(Of Int32, Employee)
    Private locked As New Object
    Public ReadOnly Property Employees As IList(Of Employee)
        Get
            Dim rv As New List(Of Employee)
            Threading.Monitor.Enter(locked)
            'wait for lock to be released
            rv = _employeesDictionary.Values.ToList
            Threading.Monitor.Exit(locked)
            Return rv
        End Get
    End Property

    Private Shared rnd As New Random 'only one needed
    Public Sub loadFromDb()
        Try
            If Threading.Monitor.TryEnter(locked) Then
                _employeesDictionary = New SortedDictionary(Of Int32, Employee)

                'generate a random number of employees
                Dim numEmpsToAdd As Integer = rnd.Next(101)
                For i As Integer = 0 To numEmpsToAdd
                _employeesDictionary.Add(i + 1, New Employee With {.ClockNo = i + 1, .Name = $"Name:{i + 1}"})
                Next
                Threading.Monitor.Exit(locked)
            End If
        Catch ex As Exception
            Throw ex
        Finally
            Threading.Monitor.Exit(locked)
        End Try
    End Sub
End Class

edit:

Public Class Test
    Private _employeesDictionary As New SortedDictionary(Of Int32, Int32)
    Public ReadOnly Property Employees As IList(Of Int32)
        Get
            Dim rv As New List(Of Int32)
            'wait for a Semaphore to be released
            mySemaPhore.WaitOne()
            rv = _employeesDictionary.Values.ToList
            mySemaPhore.Release()
            Return rv
        End Get
    End Property

    Const maxThreads As Integer = 4
    Private mySemaPhore As New Threading.Semaphore(maxThreads, maxThreads)
    Private locked As New Object
    Private Shared rnd As New Random 'only one needed

    Public Sub loadFromDb()
        Try
            Dim tempDict As New SortedDictionary(Of Int32, Int32)
            'generate a random number of employees
            Dim numEmpsToAdd As Integer = rnd.Next(6)
            For i As Integer = 0 To numEmpsToAdd
                tempDict.Add(i + 1, i + 1)
            Next
            If Threading.Monitor.TryEnter(locked) Then
                'get all semaphores
                For x As Integer = 1 To maxThreads
                    mySemaPhore.WaitOne()
                Next
                _employeesDictionary = New SortedDictionary(Of Int32, Int32)(tempDict)
                'release all semaphores
                For x As Integer = 1 To maxThreads
                    mySemaPhore.Release()
                Next
                Threading.Monitor.Exit(locked)
            End If
            tempDict.Clear()
        Catch ex As Exception
            Throw ex
        Finally
            If Threading.Monitor.IsEntered(locked) Then
                Threading.Monitor.Exit(locked)
            End If
        End Try
    End Sub
End Class

Upvotes: 2

Related Questions