3

I have some async methods:

// not ideal
private TaskCompletionSource<A> _tcsA;
private TaskCompletionSource<A> _tcsB;
private TaskCompletionSource<A> _tcsC;
...

public Task<A> GetAAsync() {
  _currentTask = TaskType.A;
  _tcsA = new TaskCompletionSource<A>();
  // some complex non-sync task, using http/events that ends with Complete();
  // QueueRequest?.Invoke(this, taskInfo); // raise request -- this does not return
  return _tcsA.Task;
}

public Task<B> GetBAsync() {
  _currentTask = TaskType.B;
  _tcsB = new TaskCompletionSource<B>();
  // some complex non-sync task, using http/events that ends with Complete();
  // QueueRequest?.Invoke(this, taskInfo); // raise request -- this does not return
  return _tcsB.Task;
}

public Task<C> GetCAsync() {
  _currentTask = TaskType.C;
  _tcsC = new TaskCompletionSource<C>();
  // some complex non-sync task, using http/events that ends with Complete();
  // QueueRequest?.Invoke(this, taskInfo); // raise request -- this does not return
  return _tcsC.Task;
}

// called by an external source, a http listener / processor
// this should complete the async call by the client and return results
public void Complete(Result result) {
  switch (_currentTask) {
    case TaskType.A:
      _tcsA.SetResult(new A());
      break;
    case TaskType.B:
      _tcsB.SetResult(new B());
      break;
    case TaskType.C:
      _tcsC.SetResult(new C());
      break;
  }

  _currentTask = TaskType.None;
}

The above is semi-pseudo code for simplicity. I call one of the methods like so:

A a = await service.GetAAsync();

Now the problem is the TaskCompletionSource<T> is generic, if I have a 100 methods in this way I would have to create a variable for each return type. But since only one method can be called at once it would be nice to use a single TaskCompletionSource, yet not type it to object (TaskCompletionSource<object>).

I don't want to do:

object a = await service.GetAAsync();

Because that will need casting by the client. So the best solution would be to have a single TaskCompletionSource, but have it typed somehow. Or alternatively be able to have a Dictionary of TaskCompletionSource. Both of which seem impossible to me.

How should I solve this?

Update:

For a background on my situation have a look at: Wrap synchronous code into async await in disconnected scenario

sprocket12
  • 5,368
  • 18
  • 64
  • 133
  • Take a look at [generic type parameters](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-type-parameters) they might help you out – MindSwipe Mar 29 '19 at 09:48
  • 1
    There isn't any non-generic API for this, so indeed the best option you have is to use `object` for the TCS. Sorry. – Marc Gravell Mar 29 '19 at 09:50
  • Have a look at this [thread](https://stackoverflow.com/questions/11969208/non-generic-taskcompletionsource-or-alternative) and approved answer – Pavel Anikhouski Mar 29 '19 at 09:52
  • @PavelAnikhouski that's not quite the same thing; in *that* case, the **result** was non-generic (`Task`). In *this* case, the results are typed - `Task`, `Task` etc, and the problem is that OP only wants one field for the TCS (since it isn't expected to handle multiple concurrent operations), but a TCS is inherently typed/generic – Marc Gravell Mar 29 '19 at 09:53
  • @MarcGravell Or... I would also be happy with the ability to add the TCS objects to a collection, keeping type safety. I know that's kind of a similar issue. – sprocket12 Mar 29 '19 at 09:55
  • @sprocket12 what are you trying to do? Why use a tcs at all and why 100 of them, stored in *fields*? Those TCSs aren't valid outside the methods. Those methods could use `Task.FromResult()` or `Task.FromException()` and avoid storing the TCSs out of scope. – Panagiotis Kanavos Mar 29 '19 at 09:59
  • @PanagiotisKanavos usually, that's because the completion isn't synchronous - that's something that happens later, perhaps due to a response from an out-of-process call, and the async IO handler wants somewhere to say "and the answer is...". Not everything fits cleanly into an `async` method implementation. – Marc Gravell Mar 29 '19 at 10:01
  • @PanagiotisKanavos unfortunately that is the only way. I am communicating with a server, which calls me, I send back my request in a response, then, wait, then wait more, finally I am called with a result using events. – sprocket12 Mar 29 '19 at 10:02
  • @sprocket12 you have to explain that in the question itself. The *actual* problem is how to convert 100 different events to tasks. You still can't store them in fields, they have no meaning outside individual method *calls*. One TCS instance can represent only one trigerred event. Once they're made local, the problem becomes how to handle multiple tasks. That's the job of `Task.WhenAll()`, WhenAny, WaitAll, WaitAny. – Panagiotis Kanavos Mar 29 '19 at 10:07
  • @sprocket12 you should explain what your *actual* problem is. There are many ways to handle asynchronous flows, eg with `async/await`, using TPL Dataflow, or Reactive Extensions. Tasks are good when you have individual asynchronous operations. Rx is better for handling *stream* of events – Panagiotis Kanavos Mar 29 '19 at 10:10
  • @PanagiotisKanavos Sorry the comments are getting too many, I have tried to explain my situation in the link and some updates to the OP. – sprocket12 Mar 29 '19 at 10:14
  • @sprocket12 OP means original poster, that's you. You already posted that you want to convert events to tasks. Storing the TCS in a field is a bad idea though and definitely *not* needed. You asked about your *implementation* though, not your actual problem. If you don't expose any TCSs, you won't need to put them in collections and the question goes away. All that's left is handling individual async calls to the methods – Panagiotis Kanavos Mar 29 '19 at 10:18
  • @PanagiotisKanavos sorry I meant Original Post. I think there is some misunderstanding. You see, the call to the outside service is not synchronous, it won't return, so if I don't have a `_tcs` then when I manually call `Complete` from outside I wont have anything to complete the original call with. Hope that makes sense. Ill update my question with a bit more detail. – sprocket12 Mar 29 '19 at 10:24
  • @sprocket12 both in your previous question and this one you keep asking about your *implementation*, not the actual problem. That's why you keep hitting problems. If server responses are received by the queue's subscriber, why not include the TCS itself in the queued message? – Panagiotis Kanavos Mar 29 '19 at 10:28
  • @PanagiotisKanavos Then you have the same problem, you are keeping `_tsc` around, storing it somewhere, waiting until the response comes. Hows that better than what I am doing? – sprocket12 Mar 29 '19 at 10:34
  • @sprocket12 not at all. The TCS would be stored only in the message. It's better because you don't have to handle the TCS at all, apart from having the processor call `.Complete()` on the message's TCS. This means the rest of the code would only need to deal with the tasks – Panagiotis Kanavos Mar 29 '19 at 10:34
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/190904/discussion-between-sprocket12-and-panagiotis-kanavos). – sprocket12 Mar 29 '19 at 10:36

2 Answers2

4

I don't want to do object a = await service.GetAAsync() because that will need casting by the client.

Not necessarily. Keep the same signature, but add a call to ContinueWith to cast to the appropriate type:

public Task<A> GetAAsync() {
  _currentTask = TaskType.A;
  _tcs = new TaskCompletionSource<object>();
  // some complex task that ends with Complete();
  return _tcs.Task.ContinueWith(t => (A)t.Result);
}

Or you can do the same thing using async/await:

public async Task<A> GetAAsync() {
  _currentTask = TaskType.A;
  _tcs = new TaskCompletionSource<object>();
  // some complex task that ends with Complete();
  return (A)await _tcs.Task;
}
Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • Wow, this may be the cleanest solution to this issue! Ill try it out and mark as answered. – sprocket12 Mar 29 '19 at 09:59
  • @sprocket12 an even cleaner solution would be to not use a TCS at all. Outside that method the TCS has no meaning, which means it *shouldn't* be stored in a field. You can use `Task.FromResult()` instead of a tcs – Panagiotis Kanavos Mar 29 '19 at 10:01
  • @PanagiotisKanavos Task.FromResult is viable only if `some complex task that ends with Complete()` is synchronous. That said, I agree that the tcs shouldn't be stored in a field and instead passed along to the async workflow – Kevin Gosse Mar 29 '19 at 10:02
  • @KevinGosse that's one of the most common scenarios for TCSs. The other is converting EAP/APM-style calls to tasks. – Panagiotis Kanavos Mar 29 '19 at 10:04
  • 1
    @sprocket12 this might look cleaner, but it is much less efficient; you're essentially creating twice as many tasks as you need by doing this – Marc Gravell Mar 29 '19 at 10:04
  • @MarcGravell thats unfortunate, but it still seems like the best solution. I can use a single `TaskCompletionSource` and still return the correct types to client. – sprocket12 Mar 29 '19 at 10:16
  • @sprocket12 I guess it depends on the call frequency, then; since you're going off-box, it presumably isn't high volume enough that a few allocation will hurt, so... you'll probably be fine with a few extra `Task` instances :) – Marc Gravell Mar 29 '19 at 10:19
  • 1
    The first answer uses `ContinueWith` in a dangerous way. Using `async`/`await` would be better from a semantic standpoint. – Stephen Cleary Apr 01 '19 at 19:11
0

This is easily accomplished with the TaskCompletionSourceDictionary <TKey>:

Objectives:

  • any type can be used for the key - I used your TaskType enum for this example
  • if there is only one instance per type, there's simpler a non-generic version
  • dynamic # of producers and consumers
  • completion of the result can be observed by multiple consumers
  • type safety is enforced - producer and consumer must specify same type, otherwise an cast exception describes the type difference
  • producer can set result before consumer asks for a task
  • consumer can ask for the task before producer sets the result

We can accomplish all of this with the GetOrAdd method on this new class:

TaskCompletionSource<TValue> GetOrAdd<TValue>(TKey key)

Example usage:

private readonly TaskCompletionSourceDictionary<TaskType> _tasks = new TaskCompletionSourceDictionary<TaskType>();

public Task<A> GetAAsync() =>
    _tasks.GetOrAdd<A>(TaskType.A).Task;

public Task<B> GetBAsync() =>
    _tasks.GetOrAdd<B>(TaskType.B).Task;

public Task<C> GetCAsync() =>
    _tasks.GetOrAdd<C>(TaskType.C).Task;

// called by an external source, a http listener / processor
// this should complete the async call by the client and return results
public void Complete(Result result)
{
    switch (_currentTask)
    {
        case TaskType.A:
            _tasks.GetOrAdd<A>(TaskType.A).TrySetResult(new A());
            break;
        case TaskType.B:
            _tasks.GetOrAdd<B>(TaskType.B).TrySetResult(new B());
            break;
        case TaskType.C:
            _tasks.GetOrAdd<C>(TaskType.C).TrySetResult(new C());
            break;
    }

    _currentTask = TaskType.None;
}

In addition to GetOrAdd, there's also TryAdd and TryRemove. Removing can be dangerous though - remove for the last consumer.

George Tsiokos
  • 1,890
  • 21
  • 31