14

I am preparing myself for an interview and I came across the followign question. I tried but I could not find anything which can create a class containing thread safe collection without "lock". If know any solution then please help.

Create a C# class derived from Object and implements the following methods:

  • AddString – This method should add a given string to an internal collection
  • ToString – Override this method and return a single, comma-delimited string containing all the strings in the internal collection

Requirements:

  • Must be thread-safe
  • Must support multiple concurrent readers
  • Must not use any pre-existing thread-safe collections
  • Bonus: don’t use any type of lock
Oded
  • 489,969
  • 99
  • 883
  • 1,009
Mayur
  • 347
  • 1
  • 4
  • 10
  • 1
    This question asks for the ability to mimic the functionality of `lock`, are you sure you will be interviewing for a position where that is required? – H H May 20 '12 at 17:25

5 Answers5

11

Here’s a way of achieving lock-free modification of a collection by working on a local copy and then attempting to atomically swap it with the global collection whilst checking for races:

public class NonLockingCollection
{
    private List<string> collection;

    public NonLockingCollection()
    {
        // Initialize global collection through a volatile write.
        Interlocked.CompareExchange(ref collection, new List<string>(), null);
    }

    public void AddString(string s)
    {
        while (true)
        {
            // Volatile read of global collection.
            var original = Interlocked.CompareExchange(ref collection, null, null);

            // Add new string to a local copy.
            var copy = original.ToList();
            copy.Add(s);

            // Swap local copy with global collection,
            // unless outraced by another thread.
            var result = Interlocked.CompareExchange(ref collection, copy, original);
            if (result == original)
                break;
        }
    }

    public override string ToString()
    {
        // Volatile read of global collection.
        var original = Interlocked.CompareExchange(ref collection, null, null);

        // Since content of global collection will never be modified,
        // we may read it directly.
        return string.Join(",", original);
    }
}

Edit: Since using Interlocked.CompareExchange to implicitly perform volatile reads and writes has given rise to some confusion, I’m posting below the equivalent code with Thread.MemoryBarrier calls instead.

public class NonLockingCollection
{
    private List<string> collection;

    public NonLockingCollection()
    {
        // Initialize global collection through a volatile write.
        collection = new List<string>();
        Thread.MemoryBarrier();
    }

    public void AddString(string s)
    {
        while (true)
        {
            // Fresh volatile read of global collection.
            Thread.MemoryBarrier();
            var original = collection;
            Thread.MemoryBarrier();

            // Add new string to a local copy.
            var copy = original.ToList();
            copy.Add(s);

            // Swap local copy with global collection,
            // unless outraced by another thread.
            var result = Interlocked.CompareExchange(ref collection, copy, original);
            if (result == original)
                break;
        }
    }

    public override string ToString()
    {
        // Fresh volatile read of global collection.
        Thread.MemoryBarrier();
        var original = collection;
        Thread.MemoryBarrier();

        // Since content of global collection will never be modified,
        // we may read it directly.
        return string.Join(",", original);
    }
}
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • Not quite; it’s a commonly-used way of enforcing a volatile read. Similar to an implicit `Thread.MemoryBarrier()`. – Douglas May 20 '12 at 17:40
  • Your code seems not very interested in the results of `CompareExchange()`. It could 'fail' you know. – H H May 20 '12 at 17:42
  • 2
    @HenkHolterman: In case of failure, the reference-equality check `result == original` would return `false`, causing the entire loop to be re-attempted. – Douglas May 20 '12 at 17:43
  • `collection` may _never_ be `null`. `CompareExchange(ref location1, value, comparand)` writes `value` to `location1` if and only if the current value of `location1` is equal to `comparand`. In the `CompareExchange(ref collection, null, null)` call, the value of `collection` would never be equal to `null`, and would thus never be replaced with `null`. – Douglas May 20 '12 at 17:59
  • 1
    Yes, I had to read it 2x. This stuff always make my head hurt. I now think it might work, but I wouldn't touch it with a 3m pole. And of course the ToList() will quickly make this far worse than the heaviest Sync object. – H H May 20 '12 at 18:02
  • @HenkHolterman: It’s to be expected that lock-free updates would be much more complex to implement. And no, the performance would actually be superior to a locking implementation if the frequency of readers is significantly higher than the frequency of writers (which was presumably the intent behind this question). – Douglas May 20 '12 at 18:18
  • @BrianGideon: Thanks! To be honest, I drew the idea for this answer from your [Copy-Modify-Swap Pattern](http://stackoverflow.com/a/10557130/1149773), replacing the `lock` with `Interlocked.CompareExchange` to eliminate blocking in all scenarios. – Douglas May 21 '12 at 17:04
  • @Douglas: Thanks...but give yourself some credit. The `CompareExchange` in a loop is a big leap! – Brian Gideon May 21 '12 at 17:29
  • 1-st: as many commenters wrote - clone list in loop is worst idea for frequent Add operation - it should be O(m), but becomes at least O(n^m), where m - threads (bigger list - more time to clone - bigger chance that your last check will cause next loop). 2-nd: you would got NullReferenceException if second thread tryed to add string while first cloning list. And 3-rd: if you look here http://msmvps.com/blogs/jon_skeet/archive/2010/09/02/don-t-let-this-get-away.aspx - you'll make sure that your constructor is thread-safe and no method will be executed before collection initialized. – David Levin Oct 24 '13 at 21:48
  • @EvgenyLevin: 1-st: Where are the many commenters? Yes, the `AddString` operation might cause the list to be cloned multiple times, but [as I already mentioned](http://stackoverflow.com/questions/10675400/threadsafe-collection-without-lock/10675650?noredirect=1#comment13852364_10675650), I am assuming that the question was intended for scenarios where “the frequency of readers is significantly higher than the frequency of writers”. 2-nd: You're mistaken. 3-rd: Issue described by Skeet does not apply to my code, since I don't call any virtual methods from my constructor. – Douglas Oct 24 '13 at 23:44
  • 1
    FWIW, `.ToList` performs quite well when used on an existing `List` due to long-standing micro-optimizations. It effectively uses Array.Copy which uses an external native implementation which benefits from such optimizations. The "logical" complexity of a problem is not the same as the performance on a real-world system since different operations yield dramatically different execution times, so don't rely too much on strictly theoretical Big-O notation. For an example: https://dzone.com/articles/performance-of-array-vs-linked-list-on-modern-comp – TheXenocide Feb 22 '19 at 15:49
2

Based on the question you should be able to add a concurrent collection inside your object that will handle the thread safety requirements for you. They did not specify what type of internal collection to use.

You should be able to implement one of the collections from the concurrentcollection namespace and achieve this.

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

tsells
  • 2,751
  • 1
  • 18
  • 20
1

You could create a non blocking linked list. For example something like this.

The .net framework provides methods like CompareExchange(Object, Object, Object) that allow you to write safe code without locking the whole list.

Fox32
  • 13,126
  • 9
  • 50
  • 71
1

My solution. Basically mimic locking using Interlocked.Exchange and an AutoResetEvents. Did some simple tests and it seems working.

    public class SharedStringClass
    {
        private static readonly int TRUE = 1;
        private static readonly int FALSE = 0;

        private static int allowEntry;

        private static AutoResetEvent autoResetEvent;

        private IList<string> internalCollection;

        public SharedStringClass()
        {
            internalCollection = new List<string>();
            autoResetEvent = new AutoResetEvent(false);
            allowEntry = TRUE;
        }

        public void AddString(string strToAdd)
        {
            CheckAllowEntry();

            internalCollection.Add(strToAdd);

            // set allowEntry to TRUE atomically
            Interlocked.Exchange(ref allowEntry, TRUE);
            autoResetEvent.Set();
        }

        public string ToString()
        {
            CheckAllowEntry();

            // access the shared resource
            string result = string.Join(",", internalCollection);

            // set allowEntry to TRUE atomically
            Interlocked.Exchange(ref allowEntry, TRUE);
            autoResetEvent.Set();
            return result;
        }

        private void CheckAllowEntry()
        {
            while (true)
            {
                // compare allowEntry with TRUE, if it is, set it to FALSE (these are done atomically!!)
                if (Interlocked.CompareExchange(ref allowEntry, FALSE, TRUE) == FALSE)
                {
                    autoResetEvent.WaitOne();
                }
                else
                {
                    break;
                }
            }
        }
    }
sean717
  • 11,759
  • 20
  • 66
  • 90
0

The easiest solution is having a field of type string[]. Whenever a caller wants to add a string, create a new array with the new item appended and swap it for the old one.

This model does not require synchronization. It does not tolerate concurrent writers but it allows for concurrent reading.

usr
  • 168,620
  • 35
  • 240
  • 369