17

My original method looks like:

string DoSomeWork();

Method DoSomeWork starts some work on another thread and returns execution ID (just random string). Later on I can query results by the returned 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 a few options. First, use an out parameter, second, 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 create a class that derives Task:

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

Does this look ok? Or is it better to decide other options?

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

UPDATE, seems I was not able to define this clear enough. But I need access to ExecutionID before the job completes so I cannot use Task.Result.

4

3 に答える 3

17

私は個人的に拡張するのではなく、代わりにそれを 作成します。そうすれば、返されるだけのAPIについて心配する必要はありません。タスクをラップするだけです。基になるタスクを公開するプロパティを持つことができ、C#5非同期の目的で、独自の型にawaiterパターンを実装できますが、独自の派生型を作成すると、利益よりも害が大きくなる可能性が高いように感じます。しかし、それはほとんど腸の感覚です。Task<T>Task<T>

もう1つのオプションは、逆の方法で作業することです。追加の状態をTask.AsyncStateプロパティに保存します。結局のところ、それが目的です。そうすれば、論理的にその一部である実行コンテキストを失うことなく、タスクを簡単に渡すことができます。

于 2012-02-13T18:02:07.783 に答える
12

Task<T>タスクの結果に他の情報を「埋め込む」ことができるため、代わりに使用することをお勧めします。

たとえば、あなたの場合、次のようなものが理にかなっているかもしれません:

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" };
     });
}

コメントに応じて編集:

タスクが完了する「前に」データが必要で、他の目的でこれにアクセスしようとしている場合は、タスクと他のデータを含むクラスを作成し、それを返すことをお勧めします。

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>
}

Taskこれにより、プロセスと/に関する情報にアクセスできますTask<T>

于 2012-02-13T17:55:18.137 に答える
4

またはから継承することにし場合、タスクの実際の作業を提供するまたはデリゲートは、Task 派生オブジェクトの構築時に指定する必要があり、後で変更できないというフラストレーションに遭遇する可能性があります。これは、基本クラスのコンストラクターが新しく作成されたタスクを実行しない場合でも当てはまります。実際、タスクが開始されたとしても、かなり後で開始される可能性があります。TaskTask<TResult>Action<Object>Func<Object,TResult>Start()

Taskこれにより、最終的な作業の完全な詳細が利用可能になる前にインスタンスを作成する必要がある状況で、派生クラスを使用することが難しくなります。

例として、アドホックなTask<TResult>方法で互いのResultプロパティにアクセスできるように、共通の目標に取り組んでいるよく知られたノードの非定型ネットワークが考えられます。ネットワーク内の任意のノードでできることを保証する最も簡単な方法は、それらのいずれかを開始する前に、それらすべてを事前に構築することです。これにより、作業グラフの依存関係を分析しようとする問題がうまく回避され、実行時の要因によって、いつ、どのような順序で値が要求されるかを判断できるようになります。Wait()Result

ここでの問題は、ノードによっては、構築時に作業を定義する機能を提供できない場合があることです。必要なラムダ関数を作成するためResultに、ネットワーク内の他のタスクからの値を閉じる必要がある場合、必要なTask<TResult>を提供する はResultまだ構築されていない可能性があります。また、構築前の段階で以前に構築されていたとしても、まだ構築Start()されていない他のノードへの依存関係が組み込まれている可能性があるため、まだ呼び出すことはできません。ネットワークを事前に構築することの要点は、このような複雑さを回避することだったことを思い出してください。

これだけでは不十分であるかのように、目的の関数を提供するためにラムダ関数を使用する必要があるのが不便な理由は他にもあります。引数としてコンストラクターに渡されるため、関数はthis最終的なタスク インスタンスのポインターにアクセスできません。これは、特にラムダが必然的にスコープの下で定義されていることを考えると、醜いコードになります。無関係なthisポインター。

先に進むこともできますが、結論としては、派生クラスで拡張機能を定義するときに、ランタイム クロージャの肥大化やその他の煩わしさに耐える必要はありません。それはポリモーフィズムの全体的なポイントを見逃していませんか? 派生クラスの作業デリゲートを通常の方法で定義する方がエレガントTaskです。つまり、基本クラスの抽象関数です。

方法は次のとおりです。秘訣は、独自の引数の 1 つを閉じるプライベート コンストラクターを定義することです。(チェーンされた) 呼び出し先によって渡される引数はnull、プレースホルダー変数として機能します。これを閉じて、Task基本クラスに必要なデリゲートを作成できます。コンストラクター本体に入ると、「this」ポインターが使用可能になるため、実際の関数ポインターを閉じた引数に置き換えることができますnull。外部デリゲートがまだ呼び出されていないため、これを行うのに「遅すぎる」ことはないことに注意してください。

「タスク」から派生する場合:

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

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

    protected abstract void action();
};

「Task<TResult>」から派生する場合:

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

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

    protected abstract TResult function();
};

構築されたTaskインスタンスの他の使用法と同様に、 は構築時に自動的に開始されないことに注意してください。そのため、この手法では、後で明示的に呼び出す必要がTaskありますStart()。もちろん、上で説明したように、ここが要点です。

最後に、プライベート コンストラクターが常にベース コンストラクターnullstate引数を渡すようにしたこと、およびこれにより、読み取り専用プロパティが有用な値にTask設定されることが実質的に防止されることに注意してください。AsyncState必要に応じて、このような値のパススルーを含めるようにこれを変更できますが、ここでも重要な点は、起動データが事前に決定されているという要件を排除することです。を呼び出す前の任意の時点で、関連するインスタンス データを設定する独自の派生クラス全体がある場合、論理的に無関係な時点で、おそらく非常に事前に、1 つの「特別な" データ パラメーターを使用して、タスクの最終的な有用な作業の詳細を表します。Start

于 2016-02-15T23:24:56.980 に答える