Reputation: 975
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
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:
Upvotes: 4
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
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
Reputation: 24433
I would just use a lock
. If it's uncontended, it is very fast - certainly capable of millisecond periods.
Upvotes: 2