Help us understand the problem. What is going on with this article?

PromiseとTaskCompletionSource

非同期処理に使う 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 での音声合成については以下の記事を参照してください。

7shi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした