Keith,
In addition to the others comments:
From what I've found and what you've told me, I can avoid synclocks on
boolean, int32 and string types if only one thread can modify the
values. Now why exactly is it that I must use synclocks if both threads
can modify those values? Wouldn't it be impossible for more than one
thread to modify one of these values at a time?
If two threads wrote to a common value, the last thread to write would be
the value in that thread. However are the threads incrementing this common
value? If they are incrementing the value then you have a "unit of work" a
read followed by a write. If both threads read the value when it was 10,
thread one would write 11, and thread two would also write 11, if both
threads are suppose to increment the value, clearly it should be 12 after
both threads increment the value! Hence you need proper thread locking!
Don't confuse atomic values with atomic operations. An integer is an atomic
value, in that it can be read or written with out fear of a task switch,
however operations such as adding one to an integer "x += 1" are not atomic!
Remember to increment an integer, you have a read, followed by an add,
followed by a write. A task switch can occur between any of the three
operations.
My number one rule with multi-threaded apps is to use proper Locking! (which
means even for atomic values, most of the time I use thread locking!) It may
be overkill, however I know its safe! As invariable even on simple integer
values the "unit of work" is more then just a single read or a single write,
and often more then one "simple integer" is involved! Note for "simple
integers" I would consider using the Interlocked class described below
instead of a SyncLock.
My second rule is to encapsulate that locking into the object itself, do not
rely on locking external to the object itself. If you are a follower OOP
this one makes sense, if you don't follow OOP this one may be hard to
explain. Which means that "simple integers" would not be "global" variables,
they would be encapsulated in another class with methods that had the proper
locking for that class. By encapsulating the locking in the class you are
ensured that all access following the same locking rules!
A third rule is to not lock on the object itself or even more important lock
on a Type. Create a New Object (literally New Object) as a padlock and use
that.
Private Readonly m_padlock As New Object ' class scope
SyncLock m_padlock
m_value = value
End SyncLock
SyncLock me ' bad!
SyncLock GetType(HashTable) ' bad!
By creating a padlock that is encapsulated in your class you are ensured
that only you are locking on it, and you are not blocking other threads
unnecessarily!
That way if I had to set or get the value of a date, I could avoid
synclocks entirely. Do you think this is overkill?
As I implied above, when dealing with multi-threading apps, proper thread
safety is NEVER overkill! For the most part I do not "avoid" locking if I
know a variable is "atomic", as its too easy to change that variable later
(an Integer to a Long) and suddenly have obscure problems later. In other
words its better safe then sorry!
By thread safety I mean using a SyncLock or one of the other thread locking
mechanisms in the System.Threading namespace. Remember most collections in
System.Collections have a Synchronized method that returns a thread safe
collection, which allows you to get a thread safe Queue for example, and you
don't need to manually protect all of its methods. However if I have a Queue
that is already encapsulated in one of my other classes, I may consider
making my class thread safe (via ReaderWriterLock for example) instead of
using Queue.Synchronized to make a thread safe Queue. The methods on my
class would hide all the details of the thread safety! Also if you are using
CollectionBase or DictionaryBase (which I normally do) you will need to add
your own thread safety. Also by encapsulating the locking in the class
itself I can introduce a ResetEvent (either auto or manual) that will allow
the consumer thread (reader) to "go to sleep" until the producer thread
(writer) is ready for it. Which is more efficient then having one thread
spinning its wheels until the Queue.Count is not zero.
Understand that a SyncLock is implemented in terms of the
System.Threading.Monitor class, most of the time its easier to use the
SyncLock statement instead of the Monitor class.
Some higher forms of locks in the System.Threading namespace are:
- AutoResetEvent
- ManualResetEvent
- Mutex
- ReaderWriterLock
The Interlocked class has a number of shared thread safe methods for dealing
with "atomic" values. I would use this class before
Hope this helps
Jay