21

Say you have an in-memory list of strings, and a multi-threaded system, with many readers but just one writer thread.

In general, is it possible to implement this kind of system in C#, without using a lock? Would the implementation make any assumptions about how the threads interact (or place restrictions on what they can do, when)?

blueberryfields
  • 45,910
  • 28
  • 89
  • 168
  • 4
    "Just one" is not relevant, you'll have to make it zero to skip locking. – Hans Passant Oct 01 '13 at 16:35
  • 1
    You can use a `ReaderWriterLock` so that readers don't lock out other readers, but writers will lock out readers. – heavyd Oct 01 '13 at 16:38
  • What is it that you want to achieve? Can't your readers use a copy for the duration of the read? – Charles Prakash Dasari Oct 01 '13 at 16:38
  • 1
    There is a lock-free IList implementation, check this answer [http://stackoverflow.com/a/5123081/1988244][1] [1]: http://stackoverflow.com/a/5123081/1988244 – PashaPash Oct 01 '13 at 16:41
  • @PashaPash that lock-free implementation answers my question - please post as an answer, and i'll edit/fill in some of the details that were relevant to me – blueberryfields Oct 01 '13 at 16:44
  • 1
    Also see this example of a lock-free queue: http://www.drdobbs.com/parallel/writing-lock-free-code-a-corrected-queue/210604448 (written by one Herb Sutter... you might have heard his name before ;) It's in C++ but you can write the same algorithm in C# – Matthew Watson Oct 01 '13 at 16:45

5 Answers5

25

Yes. The trick is to make sure the list remains immutable. The writer will snapshot the main collection, modify the snapshot, and then publish the snapshot to the variable holding the reference to the main collection. The following example demonstrates this.

public class Example
{
  // This is the immutable master collection.
  volatile List<string> collection = new List<string>();

  void Writer()
  {
    var copy = new List<string>(collection); // Snapshot the collection.
    copy.Add("hello world"); // Modify the snapshot.
    collection = copy; // Publish the snapshot.
  }

  void Reader()
  {
    List<string> local = collection; // Acquire a local reference for safe reading.
    if (local.Count > 0)
    {
      DoSomething(local[0]);
    }
  }
}

There are a couple of caveats with this approach.

  • It only works because there is a single writer.
  • Writes are O(n) operations.
  • Different readers may be using different version of the list simultaneously.
  • This is a fairly dangerous trick. There are very specific reasons why volatile was used, why a local reference is acquired on the reader side, etc. If you do not understand these reasons then do not use the pattern. There is too much that can go wrong.
  • The notion that this is thread-safe is semantic. No, it will not throw exceptions, blow up, or tear a whole in spacetime. But, there are other ways in which this pattern can cause problems. Know what the limitations are. This is not a miracle cure for every situation.

Because of the above constraints the scenarios where this would benefit you are quite limited. The biggest problem is that writes require a full copy first so they may be slow. But, if the writes are infrequent then this might be tolerable.

I describe more patterns in my answer here as well including one that is safe for multiple writers.

Community
  • 1
  • 1
Brian Gideon
  • 47,849
  • 13
  • 107
  • 150
  • 1
    I don't understand how this is actually helpful, because the data isn't partitioned amongst readers. This solves a different problem than the classic reader-writer problem. – Hans Jan 18 '15 at 20:21
  • @Hans: It is useful because readers never have to take a lock. That means they can, all things being equally, fully saturate all CPUs simultaneously if necessary. – Brian Gideon Jan 19 '15 at 00:12
  • Suggested solution will work with multiple writers as well when the multiple writers completely rewrite the data and do not depend on the existing data.I have a scenario where the updates happen based on a separate data source, and it involves a completely rewrite each time. In such a scenario, even though we have multiple writers, this solution still maintains correctness and has high performance in terms of speed at the cost of extra memory (due the extra copy). – user3613932 Jun 22 '18 at 23:19
8

That is a fairly common request for a threading library to fulfill - that sort of lock is generally just called a "reader-writer lock", or some variation on that theme. I haven't ever needed to use the C# implementation specifically, but there is one: http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

Of course, you run into the issue that if readers will always be reading, you'll never be able to get the writer in to write. You'll have to handle that yourself, I believe.

(Ok, so it's still technically a "lock", but it's not the C# "lock" construct, it's a more sophisticated object specifically designed for the purpose stated in the question. So I guess whether it's a correct answer depends somewhat on semantics and on why he was asking the question.)

neminem
  • 2,658
  • 5
  • 27
  • 36
8

To avoid locks, you might want to consider Microsoft's concurrent collections. These collections provide thread safe access to collections of objects in both ordered and unordered forms. They use some neat tricks to avoid locking internally in as many instances as possible.

mnme
  • 620
  • 6
  • 19
wagesj45
  • 171
  • 9
6

You can also use Microsoft's new Immutable Collections library: http://blogs.msdn.com/b/bclteam/archive/2012/12/18/preview-of-immutable-collections-released-on-nuget.aspx

Note: this is completely separate from the Concurrent Collections.

RobSiklos
  • 8,348
  • 5
  • 47
  • 77
1

A singly-linked-list approach can be used without locks provided the writer only inserts/deletes at either the head or the tail. In either case, if you construct the new node beforehand, you only need a single atomic operation (head = newHead; or tail.next = newTail) to make the operation visible to the readers.

In terms of performance, insertions and deletions are O(1), while length calculation is O(n).

Cyanfish
  • 3,907
  • 1
  • 14
  • 12