Sub QueueWork()
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf FirstWorkItem))
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf SecondWorkItem))
ThreadPool.QueueUserWorkItem(New WaitCallback(Sub()
Console.
WriteLine _
("Third work item")
End Sub))
End Sub
Private Sub FirstWorkItem(ByVal state As Object)
Console.WriteLine("First work item")
End Sub
Private Sub SecondWorkItem(ByVal state As Object)
Console.WriteLine("Second work item")
End Sub
Sub PoolInfo()
Dim workerThreads As Integer
Dim completionPortThreads As Integer
ThreadPool.GetAvailableThreads(workerThreads,
completionPortThreads)
Console.WriteLine("Available threads: {0}, async I/O: {1}",
workerThreads, completionPortThreads)
Console.ReadLine()
End Sub
ThreadPool.SetMaxThreads(2000, 1500)
Threads Synchronization
Until now you saw how to
create and rung new threads of execution to split big operations across
multiple threads. This is useful but there is a problem: Imagine you
have multiple threads accessing the same data source simultaneously;
what happens to the data source and how are threads handled to avoid
errors? This is a problem that is solved with the so-called thread
synchronization. Basically the idea is that when a thread accesses a
resource, this resource is locked until required operations are
completed to prevent other threads from accessing that resource. Both
Visual Basic and the .NET Framework respectively provide keywords and
objects to accomplish threads synchronization, as covered in the next
subsections.
The SyncLock..End SyncLock Statement
The Visual Basic language offers the SyncLock..End SyncLock
statement that is the place where you can grant access to the specified
resource to only one thread per time. For example, imagine you have a
class where you define a list of customers and a method for adding a new
customer to the list, as demonstrated by the following code snippet:
Private customers As New List(Of String)
Sub AddCustomer(ByVal customerName As String)
SyncLock Me
customers.Add(customerName)
End SyncLock
End Sub
Basically the preceding code
locks the entire enclosing class, preventing other threads from
accessing the instance until the requested operation completes. By the
way, locking an entire class is not always the best idea, because it can
be expensive in terms of resources and performances, and other threads
cannot also access other members. Unfortunately you cannot directly lock
the resource; the MSDN documentation in fact states that you need to
declare a lock object that you can use as follows:
Private customers As New List(Of String)
Private lockObject As New Object()
Sub AddCustomer(ByVal customerName As String)
SyncLock lockObject
customers.Add(customerName)
End SyncLock
End Sub
The lock object is typically a System.Object. Using an object like this can ensure that the code block executed within SyncLock..End SyncLock will not be accessible by other threads. Another approach is using GetType
instead of the lock object, pointing to the current type where the
synchronization lock is defined. The following code demonstrates this:
Class Customers
Inherits List(Of String)
Public Sub AddCustomer(ByVal customerName As String)
SyncLock GetType(Customers)
Me.Add(customerName)
End SyncLock
End Sub
End Class
The SyncLock..End SyncLock
statement is typical of Visual Basic language grammar. By the way, the
statement is translated behind the scenes into invocations to the System.Threading.Monitor class as is described in next section.
Synchronization with the Monitor Class
The System.Threading.Monitor class is the support object for the SyncLock..End SyncLock statement, and the compiler translates SyncLock blocks into invocations to the Monitor class. You use it as follows:
Sub AddCustomer(ByVal customerName As String)
Dim result As Boolean
Try
Monitor.Enter(lockObject, result)
customers.Add(customerName)
Catch ex As Exception
Finally
Monitor.Exit(lockObject)
End Try
End Sub
Tip
Monitor.Enter now has an overload that takes a second argument of type Boolean, passed by reference, indicating if the lock was taken. This is new in .NET Framework 4.0.
Basically Monitor.Enter locks the object whereas Monitor.Exit unlocks it. It is fundamental to place Monitor.Exit in the Finally part of the Try..Catch block so that resources will be unlocked anyway. At this point you might wonder why use Monitor instead of SyncLock..End SyncLock because they produce the same result. The difference is that Monitor also exposes additional members, such as the TryEnter method that supports timeout, as demonstrated here:
Monitor.TryEnter(lockObject, 3000, result)
This code attempts to obtain the lock on the specified object for three seconds before terminating.
Read/Write Locks
A frequent scenario is when you
have a shared resource that multiple reader threads need to access. In a
scenario like this, you probably want to grant writing permissions just
to a single thread to avoid concurrency problems. The .NET Framework
provides the System.Threading.ReaderWriterLockSlim class, which provides a lock enabled for multiple threads reading and exclusive access for writing.
The .NET Framework still provides the ReaderWriterLock
class, as in its previous versions, but it is complex and used to
handle particular multithreading scenarios. Instead, as its name
implies, the ReaderWriterLockSlim class is a light-weight object for reading and writing locks.
|
Generally an instance of this
class is declared as a shared field and is used to invoke both methods
for reading and writing. The following code demonstrates how you enable a
writer lock:
Private Shared rw As New ReaderWriterLockSlim
Sub AddCustomer(ByVal customerName As String)
Try
rw.EnterWriteLock()
customers.Add(customerName)
Catch ex As Exception
Finally
rw.ExitWriteLock()
End TrThe
End Sub
This ensures that only one
thread can write to the customers’ collection. The next code snippet
shows instead how you can enable a reader lock:
Sub GetInformation()
Try
rw.EnterReadLock()
Console.WriteLine(customers.Count.ToString)
Catch ex As Exception
Finally
rw.ExitReadLock()
End Try
End Sub
ReaderWriterLockSlim
is an object you should use if you expect more readers than writers; in
other cases you should consider custom synchronization locks
implementations.