How to serialise access to a single device from multiple async tasks?

50 views Asked by At

I have single external resource - named pipe. I need to be able to do the following:

  • multiple async tasks can be spawn nearly simultaneously (by EXCEL-DNA function, ran in many cells)
  • tasks will try to aquire access to the pipe (there can be only one pipe per app) in arbitrary manner
  • the winning task will send request and wait for the response, then release the pipe to next waiting task
  • a task waiting for too long should continue after timeout (return "#err" value)
  • I am not yet sure whether tasks will be fired from different threads (I am new to excel-dna)

I can't quite figure out an elegant way to do it (one thing comes to mind - a r/w lock with timeout).

Is there a pure async solution for such problem?

1

There are 1 answers

3
Christophe Devos On

It seems like you can use a queue for this, just like Dave suggests in the comments.

With the help of a TaskCompletionSource<TResult>, you can create your own tasks that you can complete with our own logic.

The following is a simple incomplete example (not tested and just typed here) of how you can implement something like this:

public class PipeAccess
{
  private readonly ConcurrentQueue<ScheduledTask> _queue

  public Task<string> GetResult(string input) {
    var schedule = new ScheduledTask(input);
    _queue.Enqueue(schedule);
    return schedule.Completion.Task;
  }

  private sealed class ScheduledTask
  {
    private CancellationTokenSource _timeout;
    private IDisposable _timeoutRegistration;

    public ScheduleTask(string input)
    {
      Input = input;
      Completion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
      _timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
      _timeoutRegistration = _timeout.Token.Register(() => Completion.TrySetCancelled());
    }

    public string Input { get; }

    public TaskCompletionSource<string> Completion { get; }

    public bool StartProcessing()
    {
      _timeoutRegistration.Dispose();
      _timeout.Dispose();
      return !Completion.Task.IsCancelled;
    }
  }
}

In this example I haven't included code how you read the next task from the queue but you start with calling StartProcessing() so you stop the timeout. If this returns a false, then this task is already timed out and you can just go to the next item in the queue. Otherwise you do your thing with the pipe, and the result you can set to the Completion.TrySetResult(), any errors that occured can be set to Completion.TrySetException().

I have included the TaskCreationOptions.RunContinuationsAsynchronously because otherwise the code that is awaiting the PipeAccess.GetResult() will continue on this thread, making your PipeAccess wait until that code is done before it can continue to the next queued task (and increasing the likelyhood of a timeout).