4

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?

Community
  • 1
  • 1
drowa
  • 682
  • 5
  • 13
  • 2
    @AndreasNiedermair Are you sure about that? Have you read [A Guide to Code Review for Stack Overflow users](http://meta.codereview.stackexchange.com/questions/5777/a-guide-to-code-review-for-stack-overflow-users)? I personally think this question is better off on Stack Overflow than on Code Review. – Simon Forsberg Jul 26 '15 at 20:27
  • @SimonAndréForsberg I wasn't aware of the *Please do not vote to close with a custom reason that "it belongs on Code Review".*-mantra - thanks for that, I'll retract my vote. For the other points in the table, I am half/half - My intention was: let the community decide... Especially the *doesn't seem as natural* and *any better way* indicates quite a range of suggestions (even if the title is a bit off then ...) which would be perfectly suited for CR. –  Jul 26 '15 at 20:37
  • @drowa: What resources do you have that permit multiple users but not any number of users? – Stephen Cleary Jul 27 '15 at 12:07
  • @drowa: Also, your code shows a compile-time known set of resources, with no capability to add more; is that what you actually need? – Stephen Cleary Jul 27 '15 at 12:23
  • @StephenCleary: 1) Imagine the pool of resources as a cluster of servers and you want to distribute the load; you could even have servers with different capacities, which would result in semaphores with different parameters. 2) The code is just an example. 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). – drowa Jul 27 '15 at 18:59
  • @StephenCleary: I'm wondering how the approach would be if we restrict one task at time per resource (i.e. support for resources that accept more than one consumer at time is free if the solution uses `SemaphoreSlim`). – drowa Jul 27 '15 at 19:10
  • @drowa: Regarding my first question, I was asking what your *actual* use case was. I can *imagine* this, but you shouldn't design for problems you don't actually have. – Stephen Cleary Jul 27 '15 at 23:29
  • @StephenCleary: I've omitted details of my use case in an attempt to avoid getting lost in a rabbit hole discussing details that are not relevant. Having said that, my actual pool of resources is a cluster of servers running a banking core system. It is a very specialized and old school (read mainframe) database system. The protocol is over TCP/IP and it is stateless (like HTTP). Each port on the server accepts only one client at time and each server has only one port available. My application is a kind of proxy/multiplexer that sits between this old system and several modern middlewares. – drowa Jul 28 '15 at 00:20

4 Answers4

7

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);
Community
  • 1
  • 1
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • I didn't know this `System.Threading.Tasks.Dataflow` namespace at all (I will need some time to digest this answer). Another requirement for my application is the ability for certain special tasks (one for each server), to be able to wait for a specific server in the pool (not any server). These special tasks periodically perform a handshake request in order to prevent the server to close the connection due inactivity. Do you know if this `BufferBlock` could provide that? (and yes, the connections from my application to the servers are always open). – drowa Jul 28 '15 at 07:46
  • @drowa: `BufferBlock` does not provide that capability. It's actually more of an async-compatible producer/consumer queue. I would consider having the resource handle heartbeat messages itself, if they can be interleaved with regular messages. – Stephen Cleary Jul 28 '15 at 12:04
  • Unfortunately heartbeat messages cannot be interleaved since they are normal messages (a kind of NOP instruction). The server can only handle one request/response at time. You can only send a new request after receiving the response from the previous request. – drowa Jul 28 '15 at 21:43
  • @drowa: Interesting... and complex... I would model this with streams, where each resource has a stream of data requests, and a separate stream in "front" of them all that acts like a load balancer. You can do this with TPL Dataflow (one BufferBlock linked to a bunch of BufferBlocks); Rx is another option. – Stephen Cleary Jul 29 '15 at 19:05
  • It's not that complex, in my view. If was on a synchronous environment, the `Semaphore` would attend all the requirements. – drowa Jul 31 '15 at 23:06
  • 1
    @drowa: Only if you depend on an undocumented detail of behavior - namely, that `WaitOne` will only change the state of a single synchronization primitive. Making assumptions like that will eventually land you on Raymond Chen's blog. – Stephen Cleary Aug 01 '15 at 00:36
3
  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.

usr
  • 168,620
  • 35
  • 240
  • 369
  • My concern of performance was more on the fact that we are going to acquire locks unnecessarily, which doesn't happen (at least on the surface) with the synchronous version. Another point, if I decide to cancel the other waits in other to make sure the tasks created by `ContinueWith` are completed before I start using the resource, then how can I avoid exceptions for control flow? – drowa Jul 27 '15 at 18:38
  • The cancelled tasks transition to a cancelled state, that's all. Just don't Wait/Result/await them so that nothing throws. If you need to wait for them you can use .ContinueWith(_ => { }, ExecuteSync) to suppress exceptions. I wish the framework has support for waiting without throwing. – usr Jul 27 '15 at 18:43
  • Hm, WaitAsync seems to use exceptions internally. That's likely to result in bad performance. Maybe you need to implement your own semaphore which is not as a hard as it sounds when you know how. The reference source is available. – usr Jul 27 '15 at 19:39
0

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.

nickRise
  • 96
  • 1
  • 6
0

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();
}
Community
  • 1
  • 1
drowa
  • 682
  • 5
  • 13
  • It doesn't seem as natural (and as performant) as the synchronous version. Is there a better way? – drowa Jul 27 '15 at 22:59
  • Quoting a comment from the header of the source code of `SemaphoreSlim`: _"A lightweight semahore class that contains the basic semaphore functions plus some useful functions like interrupt and **wait handle exposing to allow waiting on multiple semaphores**"_. Is this comment from the authors of `SemaphoreSlim` suggesting this approach is not good? – drowa Jul 27 '15 at 23:02