非同期型関数を呼び出すコンテキストについて

Katsuhiro Mihara 41 評価のポイント
2025-05-08T07:25:47.7566667+00:00

C# を学習していて、ふと気になることがあります。

async void 型関数は戻り値を返したり例外を投げたりできません。しかし、それらの関数はオブジェクトに副作用を残すことができます。

そこで考えました。もし async void 型関数から戻った後で副作用を戻り値として返すか例外として投げたら?

サンプルコードを書きました。このコードはデッドロックしないと考えています。

クラス

using System.Runtime.ExceptionServices;

namespace AFW // The acronym of "AsyncFuncWrapper"
{
    class AsyncFuncWrapper
    {
        /// <summary>
        /// Call the async function on the current context and return a value.
        /// </summary>
        /// <typeparam name="TResult">Type of the return value.</typeparam>
        /// <param name="func">async function</param>
        /// <returns>The return value of the async function.</returns>
		public static TResult? Call<TResult>(Func<Task<TResult>> func)
		{
			var box = new TResult?[1];
			var exceptionBox = new Exception?[1];

			CallAsyncOnCurrentContext(func, box, exceptionBox);

			var exception = exceptionBox[0];
			if (exception != null)
			{
				ExceptionDispatchInfo.Capture(exception).Throw();
			}

			return box[0];
		}

		private static async void CallAsyncOnCurrentContext<TResult>(
			Func<Task<TResult>> func, TResult?[] box, Exception?[] exceptionBox)
		{
			exceptionBox[0] = null;

			try {
				box[0] = await func();
			}
			catch (Exception e)
			{
				exceptionBox[0] = e;
			}
        }
    }
}

main 関数と周辺

using AFW;

static async Task<int> AsyncInt(int value)
{
    await Task.CompletedTask; // Gimic to make this function async.
    return value;
}

static async Task<int> AsyncIntThrowsException(int value)
{
    await Task.CompletedTask; // Gimic to make this function async.
    throw new Exception("Exception from AsyncIntThrowsException");
}

var result1 = AsyncFuncWrapper.Call(() => AsyncInt(42));
Console.WriteLine($"AsyncInt(42) returns {result1}");

try {
    var result2 = AsyncFuncWrapper.Call(() => AsyncIntThrowsException(42));
}
catch (Exception e)
{
    Console.WriteLine($"AsyncIntThrowsException(42) throws exception: {e.Message}");
}

出力

AsyncInt(42) returns 42
AsyncIntThrowsException(42) throws exception: Exception from AsyncIntThrowsException

すると async void 型関数は知られているより強い能力を有しており、私たちは性能低下を代償として非同期型関数をコールスタックの任意の箇所で呼び出せることになります。

これは、どこか誤解しているでしょうか。

開発者テクノロジ | C#
開発者テクノロジ | C#

C 言語ファミリをルーツとし、コンポーネント指向プログラミングのサポートを含む、オブジェクト指向およびタイプセーフのプログラミング言語。

0 件のコメント コメントはありません

質問作成者が受け入れた回答
gekka 14,551 評価のポイント MVP ボランティア モデレーター
2025-05-08T08:44:53.8733333+00:00

async voidは待ち合わせができず呼び出し元の処理が進んでしまうので、結果及びエラーの取得が間に合うことが保証できません。

using System;
using System.Threading.Tasks;

public class Program
{
    public static async Task Main(string[] args)
    {
        async Task<int> AsyncInt(int value)
        {
            await Task.Delay(TimeSpan.FromSeconds(1));//これは時間まで待つための切り替えが発生する
            await Task.CompletedTask; //これは完了済みのタスクと同等なので切り替えが発生しない
            return value;
        }

        async Task<int> AsyncIntThrowsException(int value)
        {
            await Task.Delay(TimeSpan.FromSeconds(1)); //これは時間まで待つための切り替えが発生する
            await Task.CompletedTask; //これは完了済みのタスクと同等なので切り替えが発生しない
            throw new Exception("Exception from AsyncIntThrowsException");
        }

        var result1 = AsyncFuncWrapper.Call(() => AsyncInt(42));
        Console.WriteLine($"AsyncInt(42) returns {result1}");

        try
        {
            var result2 = AsyncFuncWrapper.Call(() => AsyncIntThrowsException(42));

            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Callで呼ばれた処理の待ち合わせできてないよ");
        }
        catch (Exception e)
        {
            Console.WriteLine($"AsyncIntThrowsException(42) throws exception: {e.Message}");
        }
        await Task.Delay(TimeSpan.FromSeconds(1));

        Console.WriteLine("終わり");
    }
}

class AsyncFuncWrapper
{
    public static TResult? Call<TResult>(Func<Task<TResult>> func)
    {
        var box = new TResult?[1];
        var exceptionBox = new Exception?[1];

        CallAsyncOnCurrentContext(func, box, exceptionBox); //ここはTaskの完了待ちがされない

        //ここはfuncのTaskにて切り替えが発生すると次の処理に進んでしまう

        var exception = exceptionBox[0]; //
        if (exception != null)
        {
            System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception).Throw();
        }
        return box[0];
    }

    private static async void CallAsyncOnCurrentContext<TResult>(Func<Task<TResult>> func, TResult?[] box, Exception?[] exceptionBox)
    {
        exceptionBox[0] = null;

        try
        {
            box[0] = await func(); //ここはTaskの完了待ちが発生する
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} 処理が終わった");
        }
        catch (Exception e)
        {
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} 処理がエラー");
            exceptionBox[0] = e;
        }
    }     
}

あと、戻り値と例外を取得するのはTaskCompletionSource<T>でできて、Taskに変換もできます。

class AsyncFuncWrapper
{
    public static Task<TResult> Call<TResult>(Func<Task<TResult>> func)
    {
        System.Threading.Tasks.TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
        CallAsyncOnCurrentContext(func, tcs);
        return tcs.Task;
    }

    private static async void CallAsyncOnCurrentContext<TResult>(Func<Task<TResult>> func, System.Threading.Tasks.TaskCompletionSource<TResult> tcs)
    {
        try
        {
            var result = await func();         
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} 処理が終わった");
            tcs.TrySetResult(result);
        }
        catch (Exception e)
        {
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} 処理がエラー");
            tcs.SetException(e);
        }
    }
}

この回答は役に立ちましたか?

1 人がこの回答が役に立ったと思いました。

1 件の追加の回答

並べ替え方法: 最も役に立つ
  1. 削除済み

    この回答は当社の行動規範に違反したため削除されました。 アクションを実行する前にこの回答を手動で報告したか、自動検出機能により特定しました。 詳細については、当社の行動規範を参照してください。


    コメントはオフになっています。 詳細情報

お客様の回答

質問作成者は回答に "承認済み"、モデレーターは "推奨" とマークできます。これにより、ユーザーは作成者の問題が回答によって解決したことを把握できます。