What would be an efficient way to share a pool of

2019-05-10 08:35发布

Imagine several tasks trying to use a pool of resources concurrently. A single resource from the pool can only be used by an specific number of tasks at time; the number can be one.

In a synchronous environment, it seems to me that WaitHandle.WaitAny & Semaphore is the way to go.

var resources = new[] { new Resource(...), new Resource(...) }; // 'Resource' custom class wrapers the resource
var semaphores = new[] { new Semaphore(1, 1), new Semaphore(1, 1) };
... 
var index = WaitHandle.WaitAny(semaphores);

try
{
    UseResource(resources[index]);
}
finally
{
    semaphores[index].Release();
}

But what should we do in an asynchronous environment?

4条回答
Viruses.
2楼-- · 2019-05-10 09:20

I generally recommend that developers separate the "pooling" logic from the "using" logic. One nice side benefit of that separation is that only the pooling logic requires synchronization.

In the real scenario the number of resources would be run-time known; more precisely it would be at the initialization of the application (i.e. configuration).

Each port on the server accepts only one client at time and each server has only one port available.

So, you have a finite set of resources, and each resource can only be used by one thread at a time.

Since you can't create new resources on-demand, you'll need a signal to know when one is available. You can do this yourself, or you can use something like a BufferBlock<T> to act as an async-ready queue.

Since each resource can only be used by one thread at a time, I recommend using the common IDisposable technique for freeing the resource back to the pool.

Putting these together:

public sealed class Pool
{
  private readonly BufferBlock<Resource> _block = new BufferBlock<Resource>();

  public Pool()
  {
    _block.Post(new Resource(this, ...));
    _block.Post(new Resource(this, ...));
  }

  public Resource Allocate()
  {
    return _block.Receive();
  }

  public Task<Resource> AllocateAsync()
  {
    return _block.ReceiveAsync();
  }

  private void Release(Resource resource)
  {
    _block.Post(resource);
  }

  public sealed class Resource : IDisposable
  {
    private readonly Pool _pool;
    public Resource(Pool pool, ...)
    {
      _pool = pool;
      ...
    }

    public void Dispose()
    {
      _pool.Release(this);
    }
  }
}

Usage:

using (var resource = Pool.Allocate())
    UseResource(resource);

or:

using (var resource = await Pool.AllocateAsync())
    await UseResourceAsync(resource);
查看更多
Anthone
3楼-- · 2019-05-10 09:21
  1. Encapsulate the "WaitAny" style logic into a helper. That makes the code feel natural again. This untangles the mess. Usually, async code can look structurally identical to the synchronous version thanks to await.
  2. Regarding performance, this should perform better than synchronous wait handles because those require kernel mode transitions and context switches. Make sure not to depend on exceptions for control flow (for cancellation) because those are hideously slow (like 0.1us per exception).

Any concerns remaining? Leave a comment.

查看更多
甜甜的少女心
4楼-- · 2019-05-10 09:21

I'd probably go with a mutex or a lock on an object. Either can force the thread to wait until the lock or mutex is released.

查看更多
SAY GOODBYE
5楼-- · 2019-05-10 09:35

Following the asynchronous version Task.WhenAny & SemaphoreSlim.

var resources = new[] { new Resource(...), new Resource(...) }; // 'Resource' custom class wrapers the resource
var semaphores = new[] { new SemaphoreSlim(1, 1), new SemaphoreSlim(1, 1) };
...
var waits = new[] { semaphores[0].WaitAsync(), semaphores[1].WaitAsync() };

var index = Array.IndexOf(waits, await Task.WhenAny(waits));

// The wait is still running - perform compensation.
if (index == 0)
    waits[1].ContinueWith(_ => semaphores[1].Release());
else if (index == 1)
    waits[0].ContinueWith(_ => semaphores[0].Release());

try
{
    await UseResourceAsync(resources[index]);
}
finally
{
    semaphores[index].Release();
}
查看更多
登录 后发表回答