2

I have a challenging question for you big experts. This has not yet a practical usage in my code, but comes from an idea I just had.

If I have an IList<T>, how do I implement an enumerator that randomly walks the list and that can be used by multiple threads simultaneously?

For example, if I have elements A, B, C, D, E, F and two concurrent threads performing a for-each loop on the list with a ReaderLock acquired (so I'm sure nobody else will touch the list thus causing an exception), I would like their respective cycles to return, for example, B, E, C, D, A, F and E, B, D, C, A, F.

The reason why I need this is because I need to place locks on List<SslStream> elements to send data to clients, because SslStream is not thread-safe. Picking elements randomly (but making sure I pick them all) reduces the lock conflict probabilities and is supposed to improve I/O-bound operations performance.

Please keep in mind that even if I told you why I need such an enumerator, I still like challenge. There can be other ways of sending the same data to multiple clients, but my question remains the same :) :)

usr-local-ΕΨΗΕΛΩΝ
  • 26,101
  • 30
  • 154
  • 305
  • It's okay that you like challenges. Though, there's a possibility that no one comes with a good solution with these constraints. Be ready for that :) – zneak Nov 01 '10 at 20:10

3 Answers3

5

Something like this (obviously needs to be productionized):

class RandomList<T> : IEnumerable<T> {
     private readonly IList<T> list;
     private readonly Random rg;
     private readonly object sync = new Object();
     public RandomList(IList<T> list) : this(list, new Random()) { }

     public RandomList(IList<T> list, Random rg) {
         Contract.Requires<ArgumentNullException>(list != null);
         Contract.Requires<ArgumentNullException>(rg != null);
         this.list = list;
         this.rg = rg;
     }

     public IEnumerator<T> GetEnumerator() {
         List<int> indexes;
         // Random.Next is not guaranteed to be thread-safe
         lock (sync) {
             indexes = Enumerable
                 .Range(0, this.list.Count)
                 .OrderBy(x => this.rg.Next())
                 .ToList();
         }
         foreach (var index in indexes) {
             yield return this.list[index];
         }
     }
}
      IEnumerator IEnumerable.GetEnumerator() {
          return GetEnumerator();
      }
}
jason
  • 236,483
  • 35
  • 423
  • 525
  • I truly believe that your code, while still valid for my purposes, shows the same problem you highlighted in the post by thecoop. While LINQs are simpler to read and write, they don't perform O(1). But this is still a good starting point – usr-local-ΕΨΗΕΛΩΝ Nov 01 '10 at 20:26
  • 1
    @djechelon: Huh? This can not possibly spin forever. I randomly order the indexes. – jason Nov 01 '10 at 20:30
  • I think I said wrong. Anyway I never meant FOREVER. I just meant that I don't trust LINQ performance yet enough. You read just one statement, but it hides at least two for cycles, that was all. – usr-local-ΕΨΗΕΛΩΝ Nov 01 '10 at 20:40
  • @djechelon, you shouldn't be so wary of Linq performance... it's not significantly slower than a manual loop – Thomas Levesque Nov 01 '10 at 20:41
  • Thomas you hit the point!! ;) LINQ is not even FASTER as it may look like, so this is why I avoid linking to System.Core for now :-) – usr-local-ΕΨΗΕΛΩΝ Nov 01 '10 at 20:46
3

Create an array of the same size as your list, initially populate it as a[i] = i and then shuffle using a Fisher Yates algorithm.

Your enumerator can then iterate over this array, returning elements from your source list at the random index provided.

Luke Hutteman
  • 1,921
  • 13
  • 11
  • 1
    You definitely convinced me :) congrats you won my challenge. Believe me or, not, when I was posting my previous comment I was about to rethink my two-deck approach with an in-place approach, *almost* reinventing Fisher-Yates without ever having heard about it :) Big thanks for the answer, it works (with some attention) even in my case where C# regular List is implemented with a static array and allows you to remove elements thus leaving "holes" (actually the deck will be made by only valid indexes). I'll try the code :) – usr-local-ΕΨΗΕΛΩΝ Nov 01 '10 at 20:44
0

This should do it:

public static IEnumerable<T> YieldRandom<T>(this IList<T> items) {
    Random random = new Random();
    HashSet<int> yielded = new HashSet<int>(items.Count);

    for (int i=0; i<items.Count; i++) {
        // find an index we haven't yielded yet
        int yieldIndex;
        do {
            yieldIndex = random.Next(items.Count);
        }
        while (yielded.Contains(yieldIndex));

        yielded.Add(yieldIndex);
        yield return items[yieldIndex];
    }
}

I'm sure more LINQ could be used in places :)

thecoop
  • 45,220
  • 19
  • 132
  • 189
  • I can implement my own IList if I need ;), don't worry for the extension – usr-local-ΕΨΗΕΛΩΝ Nov 01 '10 at 20:11
  • This can spin for a long time as you get near the "end" of the enumeration. – jason Nov 01 '10 at 20:16
  • That's going to perform very poorly as your yielded HashSet grows and you keep having to retry random entries. – Luke Hutteman Nov 01 '10 at 20:21
  • @Jason: yea, I have read about that in another article about shuffling. I'm starting to consider a two-deck approach -- @thecoop: I don't think LINQ is smarter than a regular sequential for-each loop when I put locks into it – usr-local-ΕΨΗΕΛΩΝ Nov 01 '10 at 20:23
  • @djechelon: Two-deck approach? – jason Nov 01 '10 at 20:27
  • I was thinking about cards... if you need to shuffle them and you can only grab a random card at once, you can put each random card you get from the first unshuffled deck to the second deck. In programming, this will mean holding a list of "unused" values along which to shuffle. The problem is to maintain a non-fragmented list of "remaining values" which is possible only with dynamic linked list, but has horrible performance with them because it takes O(n/2) to walk a double-linked list. With static arrays you have too many "holes" and it takes some time to reorder the array. – usr-local-ΕΨΗΕΛΩΝ Nov 01 '10 at 20:32