LoginSignup
1

More than 3 years have passed since last update.

PromiseとTaskCompletionSource

Last updated at Posted at 2020-06-25

非同期処理に使う JavaScript の Promise と .NET Framework (C#) の TaskCompletionSource を比較します。

概要

Promise や TaskCompletionSource のどちらか片方の知識から、比較によってもう一方の取っ掛かりにするのが狙いです。

JavaScript のコードは以下の記事から一部引用します。Promise の概要もこちらで説明しています。

Promise は await での利用に限定して、.then() は利用しません。

JavaScript は Deno を想定してトップレベルで await を使用します。ただし最後の Web Speech API の例は、ブラウザで動かす必要があるため async 関数で囲みます。

待機

定番の setTimeoutPromise でラップしたものです。

JavaScript
function wait(timeout) {
    return new Promise((resolve, reject) => setTimeout(resolve, timeout));
}

await wait(1000);
console.log(1);
await wait(2000);
console.log(2);
await wait(3000);
console.log(3);

C# に移植します。用意されている Task.Delay() を使うだけです。

C#
using System;
using System.Threading.Tasks;

class Test
{
    static async Task Main()
    {
        await Task.Delay(1000);
        Console.WriteLine(1);
        await Task.Delay(2000);
        Console.WriteLine(2);
        await Task.Delay(3000);
        Console.WriteLine(3);
    }
}
実行結果
1
2
3

TaskCompletionSource

.NET Framework で JavaScript の Promise に仕様が近いのは TaskCompletionSource です。

resolve

resolve に相当するのは SetResult です。簡単な例で比較します。

JavaScript
let p = new Promise((resolve, reject) => resolve(1));
console.log(await p);
C#
using System;
using System.Threading.Tasks;

class Test
{
    static async Task Main()
    {
        var tcs = new TaskCompletionSource<int>();
        tcs.SetResult(1);
        Console.WriteLine(await tcs.Task);
    }
}
実行結果
1

※ この例では非同期の嬉しさがありませんが、後で SetResult が遅延する例を示します。👉音声合成

SetResult の名前のニュアンスは「await で取り出すための結果をセットする」という感じでしょうか。個人的には分かりやすい名前だと思います。

reject

reject に相当するのは SetException です。簡単な例で比較します。

JavaScript
function test(f) {
    return new Promise((resolve, reject) => {
        if (f) resolve(1); else reject(new Error());
    });
}

try {
    console.log(await test(true));
    console.log(await test(false));
} catch (e) {
    console.log(e);
}
C#
using System;
using System.Threading.Tasks;

class Test
{
    static Task<int> test(bool f)
    {
        var tcs = new TaskCompletionSource<int>();
        if (f) tcs.SetResult(1); else tcs.SetException(new Exception());
        return tcs.Task;
    }

    static async Task Main()
    {
        try
        {
            Console.WriteLine(await test(true));
            Console.WriteLine(await test(false));
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}
実行結果
1
System.Exception: 種類 'System.Exception' の例外がスローされました。
   場所 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   場所 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   場所 System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   場所 Test.<Main>d__1.MoveNext()

わざと例外を発生させています。

TaskCompletionSource の模倣

コールバックの引数を外に出せば TaskCompletionSource が模倣できます。

JavaScript
class TaskCompletionSource {
    constructor() {
        this.Task = new Promise((resolve, reject) => {
            this.SetResult    = resolve;
            this.SetException = reject;
        });
    }
}

function test(f) {
    let tcs = new TaskCompletionSource();
    if (f) tcs.SetResult(1); else tcs.SetException(new Error());
    return tcs.Task;
}

try {
    console.log(await test(true));
    console.log(await test(false));
} catch (e) {
    console.log(e);
}

Promise の模倣

SetResultSetException をコールバックに引数で渡せば Promise が模倣できます。

C#
using System;
using System.Threading.Tasks;

class Promise<T>
{
    private TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
    public static implicit operator Task<T>(Promise<T> p) => p.tcs.Task;
    public Promise(Action<Action<T>, Action<Exception>> action) =>
        action(tcs.SetResult, tcs.SetException);
}

class Test
{
    static Task<int> test(bool f)
    {
        return new Promise<int>((resolve, reject) => {
            if (f) resolve(1); else reject(new Exception());
        });
    }

    static async Task Main()
    {
        try
        {
            Console.WriteLine(await test(true));
            Console.WriteLine(await test(false));
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

同じようなことを試みた記事です。

.then().catch() を模倣しようとすると、かなり面倒なことになるようです。

キャンセル

TaskCompletionSource ではキャンセルもサポートされます。(SetCanceled

Promise には対応するものがないため、キャンセルを実装するには工夫が必要です。

比較は省略します。

音声合成

コールバックで終了やエラーの通知が来るタイプとして、Web Speech API の例を WinRT に移植して比較します。

JavaScript
function speak(lang, text) {
    return new Promise((resolve, reject) => {
        let u = new SpeechSynthesisUtterance(text);
        u.lang = lang;
        u.onend = resolve;
        u.onerror = reject;
        speechSynthesis.speak(u);
    });
}

(async function () {
    try {
        await speak("en", "Hello, world!");
        await speak("fr", "Bonjour, monde !");
        await speak("ja", "こんにちは、世界!");
    } catch (e) {
        console.log(e);
    }
})();
C#
using System;
using System.Linq;
using System.Threading.Tasks;
using Windows.Media.Core;
using Windows.Media.SpeechSynthesis;
using Windows.Media.Playback;

class Program
{
    static Task Speak(string lang, string text)
    {
        var tcs = new TaskCompletionSource<int>();
        try
        {
            var voice = SpeechSynthesizer.AllVoices.First(v => v.Language.StartsWith(lang));
            var synthesizer = new SpeechSynthesizer();
            var player = new MediaPlayer();
            synthesizer.Voice = voice;
            var stream = synthesizer.SynthesizeTextToStreamAsync(text).AsTask().Result;
            player.Source = MediaSource.CreateFromStream(stream, stream.ContentType);
            player.MediaEnded += (sender, o) => tcs.SetResult(0);
            player.Play();
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
        return tcs.Task;
    }

    static async Task Main()
    {
        try
        {
            await Speak("en", "Hello, world!");
            await Speak("fr", "Bonjour, monde !");
            await Speak("ja", "こんにちは、世界!");
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

JavaScript では API に任せている部分を WinRT では記述しているためやや混み入っています。MediaEnded のイベントハンドラの中から SetResult を呼んでいるのがポイントです。await Speak() は再生が終了するまで待ちます。

WinRT での音声合成については以下の記事を参照してください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1