Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What would be an efficient way to share a pool of resources in an async enviromment?

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?

like image 398
drowa Avatar asked Feb 09 '23 11:02

drowa


2 Answers

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);
like image 144
Stephen Cleary Avatar answered Feb 12 '23 01:02

Stephen Cleary


  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.

like image 32
usr Avatar answered Feb 12 '23 02:02

usr