web-dev-qa-db-ja.com

タスクを返すときにタスクをチェーンする適切な方法は何ですか?

C#でタスクを使用するのはそうですが、メソッドからタスクを返そうとすると混乱し、そのメソッドはそれ自体で複数のタスクを実行します。では、メソッドで新しいタスクを起動して、その中ですべてを順番に実行する必要がありますか? .ContinueWith()ですべてを行うことに頭を悩ませるのは難しいです

例:

public Task<string> GetSomeData(CancellationToken token)
{
    return Task.Factory.StartNew(() =>
    {
        token.ThrowIfCancellationRequested();

        var initialData = GetSomeInteger(token).Result;

        return GetSomeString(initialData, token).Result;
    });
}

public Task<int> GetSomeInteger(CancellationToken token)
{
    return Task<int>.Factory.StartNew(() =>
    {
        return 4;
    }, token);
}

public Task<string> GetSomeString(int value, CancellationToken token)
{
    return Task<string>.Factory.StartNew(() =>
    {
        return value.ToString();
    }, token);
}

タスクを正しく使用するためにこのメソッドを他にどのように記述するかわかりません。そこか何かに.ContinueWithがあるべきだと思うだけだと思います。

可能な修正??

public Task<string> GetSomeData(CancellationToken token)
{
    return GetSomeInteger(token).ContinueWith((prevTask) =>
    {
        return GetSomeString(prevTask.Result, token);
    }, token).Unwrap();
}
23
Travyguy9

一般に、タスクベースのメソッドを既に使用している場合は、新しいタスクのスピンアップを回避することをお勧めします。明示的にブロックする代わりにタスクを連鎖させると、ThreadPoolスレッドが拘束されたままになることがないため、システムのオーバーヘッドが削減されます。

そうは言っても、あなたがしているように単にブロックする方が簡単なことがよくあります。

C#5はこれをはるかに簡単にし、両方の長所を提供するAPIを提供することに注意してください。

public async Task<string> GetSomeData(CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    var initialData = await SomeOtherMethodWhichReturnsTask(token);

    string result = await initialData.MethodWhichAlsoReturnsTask(token);

    return result;
};

更新後に編集:

新しいコードを考えると、これをContinueWithで直接チェーンする簡単な方法はありません。いくつかのオプションがあります。 nwrap を使用して、作成するTask<Task<string>>を変換できます。

public Task<string> GetSomeData(CancellationToken token)
{
    Task<Task<string>> task = GetSomeInteger(token)
                               .ContinueWith(t => 
                               {
                                   return GetSomeString(t.Result, token);
                               }, token);
    return task.Unwrap();
}

または、 TaskCompletionSource<T> を使用して、自分でアンラップをエレガントに処理できます。

public Task<string> GetSomeData(CancellationToken token)
{
    var tcs = new TaskCompletionSource<string>();

    Task<int> task1 = GetSomeInteger(token);
    Task<Task<string>> task2 = task1.ContinueWith(t => GetSomeString(t.Result, token));
    task2.ContinueWith(t => tcs.SetResult(t.Result.Result));
    return tcs.Task;
}

これにより、新しいタスク(スレッドプールスレッドを拘束する)を作成したり、ブロックしたりすることなく、プロセス全体を機能させることができます。

キャンセル時に継続を追加し、キャンセルがリクエストされたときに tcs.SetCancelled を使用することもできます。

26
Reed Copsey

これを解決するために私が作成した拡張メソッドを次に示します。 .Net4 +で動作します

public static Task<TNewResult> ContinueWith<T, TNewResult>(this Task<T> task, Func<Task<T>, Task<TNewResult>> continuationFunction, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<TNewResult>();
    task.ContinueWith(t => 
    {
        if (cancellationToken.IsCancellationRequested)
        {
            tcs.SetCanceled();
        }
        continuationFunction(t).ContinueWith(t2 => 
        {
            if (cancellationToken.IsCancellationRequested || t2.IsCanceled)
            {
                tcs.TrySetCanceled();
            }
            else if (t2.IsFaulted)
            {
                tcs.TrySetException(t2.Exception);
            }
            else
            {
                tcs.TrySetResult(t2.Result);
            }
        });
    });
    return tcs.Task;
}
4
Kraig McConaghy

はい、すべてがメインタスク内で順番に実行されます。これは、Resultプロパティを呼び出すと、値が返されるまで現在のスレッドがブロックされるためです。

0
Dave New