Is this ok to derive from TPL Task to return more

2019-02-08 13:31发布

My original method looks like:

string DoSomeWork();

Method DoSomeWork starts some work on other thread and returns execution ID (just random string). Later on I can query results by given execution ID. Main point is to make execution ID available before job will complete.

Now I want to change signature to return Task, so user can wait if he want to.

Task DoSomeWork();

At the same time I still need to return execution ID (for tracing purposes for example) and I see few options. First if out parameter, second is to return tuple with both execution ID and task(in C# this looks like not a best option), and third about which I actually want to ask.

What if I will create class that will derive from Task class:

public class ExtendedTask : Task
{
     public string ExecutionID {get; set;}
}

Does this looks ok? Or it's better to decide other options?

P.S. In BCL there are some derived from Task classes.

UPDATE, seems I was not able to define this clear enouthg. But I need access to ExecutionID before job will complete and so I cannot use Task.Result.

4条回答
再贱就再见
2楼-- · 2019-02-08 14:01

If you do decide to inherit from Task or Task<TResult>, you might encounter the frustration that the Action<Object> or Func<Object,TResult> delegate that provides the actual work for the task must be specified at the time your Task-derived object is constructed, and cannot be changed later. This is true even though the base class constructor(s) do not Start() the newly created task, and in fact it may not be started until much later, if ever at all.

This makes it difficult to use a Task-derived class in situations where instances must be created prior to the full details of its eventual work being available.

An example might be a amorphous network of well-known Task<TResult> nodes working on a shared goal such that they access each other's Result properties in an ad-hoc manner. The simplest way to guarantee that you can Wait() on any arbitrary node in the network is to pre-construct all of them prior to starting any of them. This neatly avoids the problem of trying analyze work graph dependencies, and allows runtime factors to determine when, if, and in what order Result values are demanded.

The problem here is that, for some of the nodes, you may not be able to provide the function that defines the work at construction time. If creating the necessary lambda function requires closing over Result values from other tasks in the network, the Task<TResult> which provides the Result we want might not have been constructed yet. And even if it happens to have been constructed earlier during the pre-construction phase, you can't call Start() on it yet since it might incorporate dependencies on other nodes which have not. Remember, the whole point of pre-constructing the network was to avoid complexities like these.

As if this weren't enough, there are other reasons it's inconvenient to have to use a lambda function to provide the desired function. Because it's passed into the constructor as an argument, the function can't access the this pointer of the eventual task instance, which makes for ugly code, especially considering the lambda is necessarily defined under the scope of--and possibly inadvertent closure over--some unrelated this pointer.

I could go on, but the bottom line is that you shouldn't have to endure runtime closure bloat and other hassles when defining extended functionality in a derived class. Doesn't that miss the whole point of polymorphism? It would be more elegant to define the work delegate of a Task-derived class in the normal way, namely, an abstract function in the base class.

Here's how to do it. The trick is to define a private constructor which closes over one of its own arguments. The argument, initially set to null, acts as a placeholder variable which you can close over to create the delegate required by the Task base class. Once you're in the constructor body, the 'this' pointer is available, so you can patch in the actual function pointer.

For deriving from 'Task':

public abstract class DeferredActionTask : Task
{
    private DeferredActionTask(DeferredActionTask _this)
        : base(_ => ((Func<DeferredActionTask>)_)().action(),
              (Func<DeferredActionTask>)(() => _this))
    {
        _this = this;
    }
    protected DeferredActionTask() : this(null) { }

    protected abstract void action();
};

For deriving from 'Task<TResult>':

public abstract class DeferredFunctionTask<TResult> : Task<TResult>
{
    private DeferredFunctionTask(DeferredFunctionTask<TResult> _this)
        : base(_ => ((Func<DeferredFunctionTask<TResult>>)_)().function(),
              (Func<DeferredFunctionTask<TResult>>)(() => _this))
    {
        _this = this;
    }
    protected DeferredFunctionTask() : this(null) { }

    protected abstract TResult function();
};

[Edit: Simplified]

These simplified versions further reduce extraneous closures by closing directly over the derived instances' action or function method. This also frees up the AsyncState in the base class in case you want to use it. It hardly seems necessary since you have your own entire derived class now; accordingly, AsyncState isn't passed into the work function. If you need it, you can always just grab it from the property on the base class. Finally, the various optional parameters can now be passed through to the Task base class.

For deriving from 'Task':

public abstract class DeferredActionTask : Task
{
    private DeferredActionTask(Action _a, Object state, CancellationToken ct, TaskCreationOptions opts)
        : base(_ => _a(), state, ct, opts)
    {
        _a = this.action;
    }

    protected DeferredActionTask(
            Object state = null,
            CancellationToken ct = default(CancellationToken),
            TaskCreationOptions opts = TaskCreationOptions.None)
        : this(default(Action), state, ct, opts)
    {
    }

    protected abstract void action();
};

For deriving from 'Task<TResult>':

public abstract class DeferredFunctionTask<TResult> : Task<TResult>
{
    private DeferredFunctionTask(Func<TResult> _f, Object state, CancellationToken ct, TaskCreationOptions opts)
        : base(_ => _f(), state, ct, opts)
    {
        _f = this.function;
    }

    protected DeferredFunctionTask(
            Object state = null,
            CancellationToken ct = default(CancellationToken),
            TaskCreationOptions opts = TaskCreationOptions.None)
        : this(default(Func<TResult>), state, ct, opts)
    {
    }

    protected abstract TResult function();
};
查看更多
【Aperson】
3楼-- · 2019-02-08 14:07

I wouldn't personally extend Task<T>, I'd compose it instead. That way you don't need to worry about any APIs which only return Task<T> - you can just wrap the task. You can have a property which exposes the underlying task, and for the C# 5 async purposes you can implement the awaiter pattern on your own type - but it feels to me like creating your own derived type is likely to do more harm than good. It's mostly a gut feeling though.

Another option is to work the other way round: store your extra state in the Task.AsyncState property; that's what it's there for, after all. That way you can easily pass the task around without losing the execution context it's logically part of.

查看更多
爷、活的狠高调
4楼-- · 2019-02-08 14:16

I would recommend using Task<T> instead, as it allows you to "embed" the other information in the Task's Result.

For example, in your case, it might make sense to have something like:

class ExecutionResult
{
     public int ExecutionID { get; set; }
     public string Result { get; set; }
     // ...
}


public Task<ExecutionResult> DoSomeWork()
{
     return Task.Factory.StartNew( () =>
     {
          // Replace with real work, etc...
          return new ExecutionResult { ExecutionID = 0, Result = "Foo" };
     });
}

Edit in response to comments:

If you need the data "before" the Task completes, and are trying to access this for other purposes, I would recommend making a class that contains the Task and the other data, and returning it, ie:

class ExecutionResult
{
     public int ExecutionID { get; private set; }
     public Task<string> Result { get; private set; }
     // ... Add constructor, etc...
}


public ExecutionResult DoSomeWork()
{
     var task = Task.Factory.StartNew( () =>
     {
          // Replace with real work, etc...
          return "Foo";
     });

     return new ExecutionResult(1, task); // Make the result from the int + Task<string>
}

This will still let you access the information about your process, and the Task/Task<T>.

查看更多
神经病院院长
5楼-- · 2019-02-08 14:22
 private async DeferredFunctionTask<int> WaitForStart(CancellationTokenSource c, string  serviceName)
    {


        var t = await Task.Run<int>(() =>
        {
            int ret = 0;
            for (int i = 0; i < 500000000; i++)
            {


                //ret += i;
                //if (i % 100000 == 0)
                //    Console.WriteLine(i);

                if (c.IsCancellationRequested)
                {
                    return ret;
                }
            }

            return ret;

        });


        return t;
    }

Error CS1983 The return type of an async method must be void, Task or Task

查看更多
登录 后发表回答