bsobaid
bsobaid

Reputation: 975

Basic .NET threading - Most efficient way to synchronize an object for a single reader and single writer thread

I am using c# framework 2.0

I have an arraylist of objects. I need to make it thread safe for a single thread read and another single thread writer scenario. The read happens a few hundred times every second while write (which involve removing or adding elements in the array) happen rarely, if ever, probably once a week from UI.

I can always use a lock() but how can I make this array threadsafe with minimum performance and latency overhead for reader thread.

Upvotes: 1

Views: 1070

Answers (4)

Jon Hanna
Jon Hanna

Reputation: 113392

Edit: Anyone coming across this, I encourage you to take a look at the discussion. The point about the reader thread modifying the objects in the array-list is very important to this whole class of questions about shared collections (and also why in this case I'd probably go with "just put a big lock on the whole thing" rather than the approach I give here without detailed analysis of those modifications. Still, the following can still work with some cases:


For the pattern of reads to writes that you describe, I would do the following.

Let's say the ArrayList is called al. For all reads, I would (mostly, see below) just read from ignoring all thread safety (don't worry, there's method in my madness). To write I would do:

ArrayList temp = new ArrayList(al);//create copy of al
/* code to update temp goes here */
al = temp;//atomic overwrite of al.
Thread.MemoryBarrier();//make sure next read of al isn't moved by compiler or from stale cache value.

The copying of al is safe, because ArrayList is safe for multiple readers. The assignment is safe because we're overwriting the reference, which is atomic. We just need to make sure there's a memory barrier though we could probably skip that in practice with most current system (don't though, theoretically at least it's required and let's not second-guess that).

This would be pretty dreadful for most shared use of an arraylist, because it makes the writes much more expensive than they have to be. However, those are as you say 60,000,000 times less frequent than the reads, so in this case that balance makes sense.

Note on safe reading with this approach:

I had made a false assumption because of thinking about a similar case. The following is safe:

for(object obj in al)
  DoSomething(obj);

Even when DoSomething could take a long time. The reason is, that this calls GetEnumerator() once and then works on an enumerator that will keep relating to the same list even if al changes in the meantime (it'll be stale, but safe).

The following is not safe:

for(int i = 0; i < al.Count; ++i)
  DoSomething(al[i]);//

Count is safe, but it could be stale by the time you get to al[i] which could mean that you call al[43] when al has only 20 items.

It's also safe to call BinarySearch, Clone, Contains, IndexOf and GetRange, but not safe to use the results of BinarySearch, Contains or IndexOf to decide upon a second thing you do with al because it could have changed by that time.

The following is also safe:

ArrayList list = al;
for(int i = 0; i != list.Count; ++i)
  DoSomething(list[i]);
DoSomethingElse(list);
DoSomethingReallyComplicated(list);
for(int i = 0; i != list.Count; ++i)
 SureWhyNotLetsLoopAgain(list[i]);

Note that we are not copying al, just copying the reference, so this is really really cheap. Everything we do with list is safe, because if it become stale, it's still one thread's own version of the old list and can't go wrong.

So. Either do everything in a foreach (the case I was mistakenly assuming), return the results of a single call to the methods noted as safe above (but don't use it to decide whether to do another operation on al), or assign al to a local variable and act on that.


Another really important caveat, is whether the writer thread will actually set any properties or call any non-threadsafe methods on any of the objects contained in the arraylist. If it does, then you've gone from having one thread synchronisation issue to think about, to having dozens!

If this is the case, then you have to worry about not just the arraylist, but each object that can be changed (by changed I mean the way calling .Append("abc") on a StringBuilder changes that object, not replacing with a completely new object the way str = "abc" changes str).

There are four safe possibilities:

  1. You have immutable objects in your arraylist - happy days.
  2. You have mutable objects, but don't mutate them - good too, but you have to be sure.
  3. You have threadsafe objects - okay, but that does mean you've at least one set of threading problems that is more complicated than the one this question deals with.
  4. You lock on each object for both writing and reading - while locks being contested is less likely with such fine locks, this has its own heaviness and really you're no better than the default (and it should be the default) answer of "just lock, locks are cheap when not contested anyway).

Upvotes: 4

Tudor
Tudor

Reputation: 62479

Since there is only reader and one writer, a ReaderWriterLock would not help in this case. Probably the classic lock is the best option in this case.

Having said this, since the reader needs to read very often and thus checks the lock often, a robust solution would be a SpinLock: http://msdn.microsoft.com/en-us/library/system.threading.spinlock.aspx

Edit: Indeed, as pointed out by David Silva, SpinLock is not available in .net 2.0. However, it can be implemented with repeated calls to Interlocked.CompareExchange, but probably going into details is not relevant at this stage. The classic lock is your best option.

Upvotes: 3

csharptest.net
csharptest.net

Reputation: 64248

I went through this process recently. It turns out the performance gain on a lockless version of single-producer/consumer queue is almost meaningless (around 0.0002 ms per queue/dequeue).

My advice is to just use the lock() statement, it works. You may also look at the lockless version i wrote, see Writing a Lockless Queue for a single producer/consumer. The last article in the series has a locking queue you could also use.

Upvotes: 1

Nick Butler
Nick Butler

Reputation: 24433

I would just use a lock. If it's uncontended, it is very fast - certainly capable of millisecond periods.

Upvotes: 2

Related Questions