C# Properties && Collections

N

NvrBst

Is there a way to make Collections (Like SortedDictionary<T,K>) thread
safe with properties? For value types (IE the primative types) doing
the following makes them thread safe I think

private ReaderWriterSlim myLock = new ReaderWriterSlim();
private string myString;
public string MyString {
get { myLock.EnterReadLock(); try { return myString; } finally
{ myLock.ExitReadLock(); } }
set { myLock.EnterWriteLock(); try { myString = value; } finally
{ myLock.ExitWriteLock(); } }
}


For reference types (like collections) I'm unsure. If we changed the
above example from "string" to "SortedDictionary<string, string>" then
we call "MyString.Add(...)" the get property would be called but, if
my understanding is correct, the lock would unlock when the reference
to "myString" is returned, and then the "Add" method would be called?

If I want to not worry about locks while programing (with a collection
thats accessed by multiple threads) do I have to create a wrapper
classes that override all the methods I use from the collection class,
or is there a more elegant way?

It just seems like something that should be very common, and wondering
if there is a better solution than the one I see. If wrapping is what
I have to do, IE when making "myThreadSafeSortedDictionary", what
would I override exactly? ContainsKey / Add / [] / Count are what I
usally use? Does [] automatically call Add? Are there hidden ones I
need to do (Equals?)? Does properties, like Count, need to be done?

Thanks :) I know this post is a little long. I'm welcome to any
comments or suggestions even if its just on 1 part. Or if anyone
knows some websites I should be reading that answers some of my
questions I'd be greatful.

NB
 
M

Marc Gravell

(minor: string isn't a value-type)

Method level therad safety isn't actually all that helpful with
collections. There is no point having Add() and Contains()
*themselves* thread-safe when a very typical use case is:

if(!foo.Contains(bar)) {
foo.Add(bar);
}

If there are two threads doing this, then one can definitely blow up;
the /caller/ is the only person who can define the logical scope of a
(composite) operation.

Personally I'd decide on a sync-object (perhaps simply the collection
itself, perhaps a public property) and have the callers do something
like:

lock(foo) {
if(!foo.Contains(bar)) {
foo.Add(bar);
}
// whatever else is part of this logical transaction
}

I'd also start simple with a Monitor ("lock") rather than a
ReaderWriterLock; in most cases this is more than adequate.

Marc
 
M

Marc Gravell

Actually I should add that member-level locking also isn't always useful for
standard entities... consider two threads:

[A] (summing the value)
decimal lineValue = line.Quantity * line.UnitPrice;
(updating a line item)
line.Quantity = 3;
line.UnitPrice = 1.03;

There is a thread race there... thread A can get an inconsistent state where
one (but not both) properties has been updated, and report a lineValue that
has *never* been true.

As another thought on collections - consider also that the result of
GetEnumerator() outlives the individual method, but should still be
considered part of the unit of work.

Marc
 
N

NvrBst

Actually I should add that member-level locking also isn't always useful for
standard entities... consider two threads:

[A] (summing the value)
decimal lineValue = line.Quantity * line.UnitPrice;
(updating a line item)
line.Quantity = 3;
line.UnitPrice = 1.03;

There is a thread race there... thread A can get an inconsistent state where
one (but not both) properties has been updated, and report a lineValue that
has *never* been true.

As another thought on collections - consider also that the result of
GetEnumerator() outlives the individual method, but should still be
considered part of the unit of work.

Marc


Ahh silly strings. Thanks for the reply. I knew of the race problems
before (I was a little more conserned with crashing than using an old
values); enumeration - contains/Add would of been possible problems
though (can also use the [] instead of add to kinda fix the contains/
add thing, in most situations). But, I realize now its probably
better/cleaner to just wrap what I'm using in the "lock" statments so
its a little higher level.

Reasion I use the "ReaderWriterLockSlim(..)" instead of "lock" is
because 99% of the times I'm only reading from the collections but
there is the odd ocasion the user might call a method that writes; so
I thought it'd be better using the "EnterReadLock" all the time
instead of the "lock".


One more questions though. I already use a snipplet that I made, to
make using the "EnterReadLock, try, do, finally" and "EnterWriteLock,
try, do, finally" constructs, but my code is a little bloated. Is
there any way to make something like my own "lock(xxx) { }" or
"using(xxx) { }" (since lock is basically identical to what I'm doing
except with Monitors). Its also basically just like a snipplet but
its hiding the implementation. IE, so I could call

rLock(TheReaderWriterObject) { if(list[3] == 5) return 3; }; ((and a
wLock(...) { } construct to go with it)). And have it convert to the
following at compile time:

TheReaderWriterObject.EnterReadLock(); try { if(list[3] == 5) return
3; } finally { TheReaderWriterObject.ExitReadLock(); }

Just so my codes a little more read-able? Or something like this
isn't possible?

Thanks
 
M

Marc Gravell

Is there any way to make something like my own "lock(xxx) { }" or
"using(xxx) { }"

To borrow Jon's idea about a "using" construct for lock-tokens, and
applying extension methods (from C# 3 [*]), how about the following
(seems to work):

ReaderWriterLockSlim syncLock = new
ReaderWriterLockSlim();
using (syncLock.Read())
{
// foo
}
using (syncLock.Write())
{
// bar
}
* = without C# 3, you'd probably need to use a static helper method,
for example: using(LockHelper.Read(syncLock)) {...}

Necessary support code (easily extended to similar locks, such as a
ReaderWriterLock, etc):

public static class LockExtensions
{
abstract class LockToken<T> : IDisposable where T : class
{
private T lockObject;
protected LockToken(T lockObject)
{
if (lockObject == null) throw new
ArgumentNullException();
this.lockObject = lockObject;
Enter(lockObject);
}
protected abstract void Enter(T lockObject);
protected abstract void Exit(T lockObject);
public void Dispose()
{
if (lockObject != null)
{
Exit(lockObject);
lockObject = null;
}
}
}
sealed class ReaderWriterLockSlimReadLock :
LockToken<ReaderWriterLockSlim>
{
public ReaderWriterLockSlimReadLock(ReaderWriterLockSlim
lockObject)
: base(lockObject) { }
protected override void Enter(ReaderWriterLockSlim
lockObject)
{
lockObject.EnterReadLock();
}
protected override void Exit(ReaderWriterLockSlim
lockObject)
{
lockObject.ExitReadLock();
}
}
sealed class ReaderWriterLockSlimWriteLock :
LockToken<ReaderWriterLockSlim>
{
public ReaderWriterLockSlimWriteLock(ReaderWriterLockSlim
lockObject)
: base(lockObject) { }
protected override void Enter(ReaderWriterLockSlim
lockObject)
{
lockObject.EnterWriteLock();
}
protected override void Exit(ReaderWriterLockSlim
lockObject)
{
lockObject.ExitWriteLock();
}
}
public static IDisposable Read(this ReaderWriterLockSlim
lockObject)
{
return new ReaderWriterLockSlimReadLock(lockObject);
}
public static IDisposable Write(this ReaderWriterLockSlim
lockObject)
{
return new ReaderWriterLockSlimWriteLock(lockObject);
}
}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top