2

I have a fixed number of "browsers", each of which is not thread safe so it must be used on a single thread. On the other hand, I have a long list of threads waiting to use these browsers. What I'm currently doing is have an AutoResetEvent array:

public readonly AutoResetEvent[] WaitHandles;

And initialize them like this:

WaitHandles = Enumerable.Range(0, Browsers.Count).Select(_ => new AutoResetEvent(true)).ToArray();

So I have one AutoResetEvent per browser, which allows me to retrieve a particular browser index for each thread:

public Context WaitForBrowser(int i)
{
    System.Diagnostics.Debug.WriteLine($">>> WILL WAIT: {i}");
    var index = WaitHandle.WaitAny(WaitHandles);
    System.Diagnostics.Debug.WriteLine($">>> ENTERED: {i}");
    return new Context(Browsers[index], WaitHandles[index]);
}

The i here is just the index of the thread waiting, since these threads are on a list and have a particular order. I'm just passing this for debugging purposes. Context is a disposable that then calls Set on the wait handle when disposed.

When I look at my Output I see all my ">>> WILL WAIT: {i}" messages are on the right order, since the calls to WaitForBrowser are made sequentially, but my ">>> ENTERED: {i}" messages are on random order (except for the first few), so they're not entering on the same order they arrive at the var index = WaitHandle.WaitAny(WaitHandler); line.

So my question is, is there any way to modify this so that threads enter on the same order the WaitForBrowser method is called (such that ">>> ENTERED: {i}" messages are also ordered)?

Juan
  • 15,274
  • 23
  • 105
  • 187
  • Possible duplicate of [Is there a synchronization class that guarantee FIFO order in C#?](https://stackoverflow.com/questions/961869/is-there-a-synchronization-class-that-guarantee-fifo-order-in-c) – John Wu Oct 18 '17 at 19:14
  • @JohnWu Not the same question (see question body) – Juan Oct 18 '17 at 19:46

2 Answers2

1

Since there doesn't seem to be an out-of-the-box solution, I ended up using a modified version of this solution:

public class SemaphoreQueueItem<T> : IDisposable
{
    private bool Disposed;
    private readonly EventWaitHandle WaitHandle;
    public readonly T Resource;

    public SemaphoreQueueItem(EventWaitHandle waitHandle, T resource)
    {
        WaitHandle = waitHandle;
        Resource = resource;
    }

    public void Dispose()
    {
        if (!Disposed)
        {
            Disposed = true;
            WaitHandle.Set();
        }
    }
}

public class SemaphoreQueue<T> : IDisposable
{
    private readonly T[] Resources;
    private readonly AutoResetEvent[] WaitHandles;
    private bool Disposed;
    private ConcurrentQueue<TaskCompletionSource<SemaphoreQueueItem<T>>> Queue = new ConcurrentQueue<TaskCompletionSource<SemaphoreQueueItem<T>>>();

    public SemaphoreQueue(T[] resources)
    {
        Resources = resources;
        WaitHandles = Enumerable.Range(0, resources.Length).Select(_ => new AutoResetEvent(true)).ToArray();
    }

    public SemaphoreQueueItem<T> Wait(CancellationToken cancellationToken)
    {
        return WaitAsync(cancellationToken).Result;
    }

    public Task<SemaphoreQueueItem<T>> WaitAsync(CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<SemaphoreQueueItem<T>>();
        Queue.Enqueue(tcs);

        Task.Run(() => WaitHandle.WaitAny(WaitHandles.Concat(new[] { cancellationToken.WaitHandle }).ToArray())).ContinueWith(task =>
        {
            if (Queue.TryDequeue(out var popped))
            {
                var index = task.Result;

                if (cancellationToken.IsCancellationRequested)
                    popped.SetResult(null);
                else
                    popped.SetResult(new SemaphoreQueueItem<T>(WaitHandles[index], Resources[index]));
            }
        });

        return tcs.Task;
    }

    public void Dispose()
    {
        if (!Disposed)
        {
            foreach (var handle in WaitHandles)
                handle.Dispose();

            Disposed = true;
        }
    }
}
Juan
  • 15,274
  • 23
  • 105
  • 187
0

Have you considered using Semaphore instead of array of AutoResetEvent ?

Problem with order of waiting threads (for semaphore) was discussed here: Guaranteed semaphore order?

  • I have, but then I don't know how to get an index (the `WaitOne` method returns a `bool`). – Juan Oct 15 '17 at 19:38