astrowalker astrowalker - 2 months ago 36
C# Question

How to combine time base polling with awaitable Task

I implemented already a polling-worker based on Timer. As example you can think of

TryConnect
on the client side -- I call
TryConnect
and it will connect eventually in some time. It handles multiple threads, if connecting is in the process already all subsequent
TryConnect
returns immediately without any extra action. Internally I simply create a timer and in intervals I try to connect -- if the connection fails, I try again. And so on.

Small downside is it is "fire&forget" pattern and now I would like to combine it with "async/await" pattern, i.e. instead calling:

client.TryConnect(); // returns immediately
// cannot tell if I am connected at this point


I would like to call it like this:

await client.TryConnect();
// I am connected for sure


How can I change my implementation to support "async/await"? I was thinking about creating empty
Task
(just for
await
), and then complete it with
FromResult
, but this method creates a new task, it does not complete given instance.

For the record, current implementation looks like this (just a sketch of the code):

public void TryConnect()
{
if (this.timer!=null)
{
this.timer = new Timer(_ => tryConnect(),null,-1,-1);
this.timer.Change(0,-1);
}
}
private void tryConnect()
{
if (/*connection failed*/)
this.timer.Change(interval,-1);
else
this.timer = null;
}

Answer

Lacking a good Minimal, Complete, and Verifiable code example it's impossible to offer any specific suggestions. Given what you've written, it's possible what you're looking for is TaskCompletionSource. For example:

private TaskCompletionSource<bool> _tcs;

public async Task TryConnect()
{
   if (/* no connection exists */)
   {
     if (_tcs == null)
     {
       this.timer = new Timer(_ => tryConnect(),null,-1,-1);
       this.timer.Change(0,-1); 
       _tcs = new TaskCompletionSource<bool>();
     }

     await _tcs.Task;
   }
}

private void tryConnect()
{
   if (/*connection failed*/)
     this.timer.Change(interval,-1);
   else
   {
     _tcs.SetResult(true);
     _tcs = null;
     this.timer = null;
   }
}

Notes:

  • Your original code example would retry the connection logic if TryConnect() is called again after a connection is made. I expect what you really want is to also check for the presence of a valid connection, so I modified the above slightly to check for that. You can of course remove that part if you actually always want to attempt a new connection, even if one already exists.
  • The code sets _tcs to null immediate after setting its result. Note though that any code awaiting or otherwise having stored the Task value for the _tcs object will implicitly reference the current _tcs object, so it's not a problem to discard the field's reference here.
  • There is no non-generic TaskCompletionSource. So for scenarios where you just need a Task, you can use the generic type with a placeholder type, like bool as I've done here or object or whatever. I could've called SetResult(false) just as well as SetResult(true), and it wouldn't matter in this example. All that matters is that the Task is completed, not what value is returned.
  • The above uses the async keyword to make TryConnect() an async method. IMHO this is a bit more readable, but of course does incur slight overhead in the extra Task to represent the method's operation. If you prefer, you can do the same thing directly without the async method:
public Task TryConnect()
{
   if (/* no connection exists */)
   {
     if (_tcs == null)
     {
       this.timer = new Timer(_ => tryConnect(),null,-1,-1);
       this.timer.Change(0,-1); 
       _tcs = new TaskCompletionSource<bool>();
     }

     return _tcs.Task;
   }

   return Task.CompletedTask;
}
Comments