Locking for Reads and Writes in C#.NET - ' Lock Codes ' (
Page 2 of 2 )
Coding the ReaderWriterLock
These different threads, of course, will only work together provided they actually make use of the locking mechanism. And that's where the ReaderWriterLock class comes in. Here's how it works:
ADVERTISEMENT
First, your code must create a single instance of ReaderWriterLock, which is shared among threads. This, of course, must only be done once for each object you are protecting. (That is, if you have two lists you want to protect, you'll create two instances of ReaderWriterLock, one for each list.)
The best place to store this instance is as a static (or shared in VB.NET) member of a class. And the best way to create this object is in static initialization. (You could, in theory, wait to create the ReaderWriterLock instance until you need it. But the problem is you're going to have a chicken-and-egg problem. What if two threads simultaneously want to create the single instance? You would somehow have to protect the lock… And you can see what a mess that can result in. Technically, it is possible, but the best way here is to use static initialization.)
Here's a class that includes such a static initialization:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace ReaderWriterSample
{
class MyClass
{
private static ReaderWriterLock listlock =
new ReaderWriterLock();
}
class Program
{
static void Main(string[] args) {
}
}
}
This code creates a single instance of ReaderWriterLock. (This class is a member of the System.Threading namespace, which I'm including at the top of the code.) Notice, however, that no object or anything is actually associated with the lock. You don't pass any object into the constructor or anything. Rather, it's up to you, as the programmer, to keep track of which ReaderWriterLock instances are used to protect which objects. That's where good variable naming comes in. In this case I'm protecting a list (which I haven't coded yet), so I'm calling it listlock.
The list is easiest to initialize through static initialization as well, so that's what we'll do. Here's the expanded code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace ReaderWriterSample
{
class MyClass
{
private static ReaderWriterLock listlock =
new ReaderWriterLock();
private static List<string> mylist = new List<string>();
}
class Program
{
static void Main(string[] args) {
}
}
}
Now the code is ready for some different threads to access the list. In an ASP.NET application, you don't need to manually create any threads. But in this sample, I'll manually create them. I'll write two routines, one that wants to modify the list, and another that wants to read the list. Then I'll create several threads that randomly call these routines. (Actually it won't exactly be random; I'll make the calls somewhat arbitrary for demonstration purposes.)
Here's the whole program. Look closely at the two routines DoSomeWriting (which acquires a writer lock) and DoSomeReading (which acquires a reader lock).
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace ReaderWriterSample
{
class MyClass
{
private static ReaderWriterLock listlock =
new ReaderWriterLock();
private static List<string> mylist = new List<string>();
public void DoSomeWriting() {
listlock.AcquireWriterLock(-1);
Console.WriteLine("Writing...");
mylist.Add(DateTime.Now.Ticks.ToString());
Console.WriteLine("Count is " + mylist.Count.ToString());
listlock.ReleaseWriterLock();
}
public void DoSomeReading() {
listlock.AcquireReaderLock(-1);
Console.WriteLine("Reading...");
Console.Write("List: (count " + mylist.Count.ToString()
+ ")" );
foreach (string item in mylist) {
Console.Write(item);
Console.Write("-");
}
Console.WriteLine();
listlock.ReleaseReaderLock();
}
}
class Program
{
static void Main(string[] args) {
MyClass inst = new MyClass();
Random rnd = new Random();
for (int i = 0; i < 20; i++ ) {
Console.WriteLine(i);
if (i % 4 == 0) {
Thread t = new Thread(inst.DoSomeWriting);
t.Start();
}
else {
Thread t = new Thread(inst.DoSomeReading);
t.Start();
}
}
}
}
}
The code that does the writing requests a writer lock. If any threads are currently reading or writing, the code will wait until the lock is available. Here's the call that makes that happen:
listlock.AcquireWriterLock(-1);
The parameter is the number of milliseconds to wait before timing out. In this case, -1 means to wait indefinitely. When you're finished with the writer lock, it's imperative that you release it so other threads waiting can run. Here's the code:
listlock.ReleaseWriterLock();
Reader locks use similar calls:
listlock.AcquireReaderLock(-1);
and
listlock.ReleaseReaderLock();
Although the output isn't particularly pretty, you can see from it that when a thread is writing to the list, no other threads can access the list. But when a thread wants to read from the list, any other thread can read (but not write to) the list. (Also, the output is a little messy because the read threads can fun simultaneously, and as such can simultaneously write to the console, which causes their lines to get mixed together. But this is just for demonstration purposes. In a real application where you're writing a log file, this Reader/Writer lock would be an ideal helper to keep the log file in order!)
Here's the output, if you want to look through it carefully (but remember, it's messy).
The ReaderWriterLock class is one of those highly-specialized classes that's incredibly easy to use, and, although specialized, very useful. I've made us of it in many of my ASP.NET applications when any number of people can access your website simultaneously. When writing to data files, I simply acquire a writer lock. When reading them, I acquire a reader lock. (Just don't forget to relinquish the locks when you're done, or your application will freeze up!) Use it wisely, and as always, have fun!