Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Task<T> for non-live task

I have an Operation class like...

public sealed class Operation
{
  public void DoSomething1(ArgType1 arg) {...}
  public void DoSomething2(ArgType2 arg) {...}
  ...
  public Task<bool> Execute() {...}
}

The DoSomething methods package work to be done, storing the arg parameters and then the Execute() method will start a Task to accomplish this work together in an atomic fashion. The primary effect of the DoSomethings are side effects but some of them make sense to also return a value, my first instinct would be to return a Task as follows...

  public Task<ResultType3> DoSomething3(ArgType3 arg) {...}

but the catch is that that Task wouldn't be 'live' as most tasks are assumed to be. await-ing the result of this Task would be fruitless until Execute() is called to initiate the work, and thus I feel it would be confusing to the consumer. It is as if the return values of DoSomething3() and Execute() are non-independent tasks.

I could wrap the Task<> in a new type, called something like Result<>, and internally it would hold a Task<> and the Operation would hold onto its TaskCompletionSource<> and set the Result at the end of Execute() so that clients, after await-ing the Task returned from Execute, could observe the Result.

public Result<T>
{ 
  internal Result(Task t) { _t = t; }
  public bool IsComplete { get { return _t.IsComplete; } }
  public T Result { get { return _t.Result; } }
  // Perhaps more methods delegating to the underlying Task
}

  public Result<ResultType4> DoSomething4(ArgType4 arg) {...}

The primary motivation of wrapping Task would be to communicate to the consumer that the result of DoSomething3() is not a live Task and to make it difficult / impossible to call...

var result = await op.DoSomething4(x);

as that would likely deadlock the code, since no one fired off the Operation yet. Note the similarity of this Result<> type to Nullable<> with different semantics.

Another approach would be for the method to return some opaque object that would be used as a key to retrieve the actual result from the Operation after Execute() has completed...

var token = op.DoSomething4(x);
...
var succeeded = await op.Execute();
if (! succeeded) return;
var result = op.RetrieveResult(token);

where Retrieve result would have a signature similar to...

public T RetrieveResult(Token<T> token) {...}

I suppose another option would be to add an additional argument that acts as a callback to be executed at the end of Execute() when the actual result is available...

public void DoSomething5(ArcType5 arg, Func<ResultType5,Task> callback) {...}

So as you can see I have several different options without a strong intuition about which is most appropriate. Unfortunately this is probably primarily just a matter of taste, but I would appreciate feedback on the different approaches.

like image 966
John Jones Avatar asked Mar 01 '26 22:03

John Jones


1 Answers

I can't find a reason for you to have different methods that only set values (instead of properties for that matter) and a single method that runs everything.

But if you want to keep this design you can do something very similar to TPL Dataflow's blocks. Have a Completion Task property that completes only when Execute completes and have DoSomething3 be void. This lets the user understand that the entire operation can be awaited (including Execute) instead of just DoSomething3.

public sealed class Operation
{
    private TaskCompletionSource<bool> _tcs
    public Task Completion {get { return _tcs.Task;} }
    public void DoSomething3(ArgType2 arg) {...}
    ...
    public Task<bool> Execute() 
    {
        // ...
        _tcs.SetResult(false);
    }
}

Usage:

operation.DoSomething3(arg);
await operation.Completion;
like image 180
i3arnon Avatar answered Mar 03 '26 10:03

i3arnon