質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.59%

【C#】非同期メソッドの作り方について悩んでいます。

解決済

回答 3

投稿

  • 評価
  • クリップ 1
  • VIEW 2,885

score 40

前提

・VS2019
・Windows Forms で実行していると仮定

質問したい事

ライブラリの一部に、戻り値にTaskを持ったメソッドを作ろうとしています。
どのように非同期メソッドを作るのが正しいのかが知りたいです。

悩んでいる事

ライブラリ内のメソッドで、Task.Run を使ってタスクを開始してもよいものなのでしょうか。

気にしている事として、幾つかのサイトをみてみた所Task.Run(or start, Task.FactoryStart)と書いてタスクを開始するのはユーザーが決めることでありライブラリが決めることではない、という文言が書いてありライブラリが勝手にスレッドプールを埋めてしまわないように配慮するのは確かにその通りなのかなと感じました。
https://www.infoq.com/jp/articles/Async-API-Design/

ただ、ライブラリのメソッド内でTask.Runを書かないという制約を守りつつ非同期メソッドを作るにはどうするのが正しい書き方なのかがわかりません。

具体的な例

非常に簡易的ですが、以下のような非同期メソッドをライブラリに組み込もうとするとします。

public static Task<object> HogeAsync()
{
    var tcs = new TaskCompletionSource<object>();
    new Task<object>(() =>
    {
        //something
        return "hoge";
    }).ContinueWith(t =>
    {
        try
        {
            if (t.IsFaulted)
            {
                tcs.TrySetException(t.Exception);
            }
            else if (t.IsCanceled)
            {
                tcs.TrySetCanceled();
            }
            else
            {
                tcs.TrySetResult(t.Result);
            }
        }
        catch (Exception e)
        {
            tcs.TrySetException(e);
        }
    });

    return tcs.Task;
}

これを呼び出す際には、以下の様になります。

var hoge = await HogeAsync();


しかし、これを Windows Forms 上で実行するととトークンをキャンセルするまで。処理がどこかへ行ってしまいます。
処理がどこかへいってしまう理由として、Taskが開始されていない状態でTaskを実行しようとしてしまっているせいだと考えています。なので先ほどのメソッドを以下の様に書き換えれば思った通りの挙動をしてくれます。

public static Task<object> HogeAsync()
{
    var tcs = new TaskCompletionSource<object>();
    //この部分をTask.Runにしてタスクを開始する
    Task.Run<object>(() =>


しかしこうすると、ライブラリのメソッド内でTask.Runを書かないという制約を守れなくなります。
一体、どうすれば…。

ほかに調べた事

ネイティブなメソッドでは、なぜawaitキーワードで上手く待てるのかを調査してみました。
オーソドックスに、HttpClientクラスのGetAsyncメソッドを追っていってみたところ、Task.Runが使われている箇所がありました。使ってもよい場面があるのでしょうか。。

try
{
  HttpWebRequest prepareWebRequest = this.CreateAndPrepareWebRequest(request);
  state.webRequest = prepareWebRequest;
  cancellationToken.Register(HttpClientHandler.s_onCancel, (object) prepareWebRequest);
  if (ExecutionContext.IsFlowSuppressed())
  {
    IWebProxy webProxy = (IWebProxy) null;
    if (this._useProxy)
      webProxy = this._proxy ?? WebRequest.DefaultWebProxy;
    if (this.UseDefaultCredentials || this.Credentials != null || webProxy != null && webProxy.Credentials != null)
      this.SafeCaptureIdenity(state);
  }
  //ここでTask.Runつかってるじゃん
  Task.Run((Action) (() => this._startRequest((object) state)));
}
catch (Exception ex)
{
  this.HandleAsyncException(state, ex);
}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • gentaro

    2019/06/18 17:20

    > オーソドックスに、HttpClientクラスのGetAsyncメソッドを追っていってみたところ、Task.Runが使われている箇所

    たぶんこれSendAsyncだと思いますが…。(検索してみたらまったく同じコードが引っかかったため)
    https://github.com/microsoft/referencesource/blob/e0bf122d0e52a42688b92bb4be2cfd66ca3c2f07/System/net/System/Net/Http/HttpClientHandler.cs#L871

    コメント見たらヘルパータスクが云々…って書いてあるから、本筋の処理じゃないからOKって意味なんだろか?(全体像まで追いきるのはしんどいそうなので諦めましたが)

    キャンセル

  • OXamarin

    2019/06/18 19:01

    指摘ありがとうございます。
    確かに、Task.Runが記述されているのはSendAsync内ですね。

    ステップ実行をして確認したんですが、GetAsync内の
    base.SendAsync(request, linkedCts.Token).ContinueWithStandard<HttpResponseMessage>((Action<Task<HttpResponseMessage>>) (task =>

    から呼ばれていましたので、「GetAsyncメソッドを追ったら」という表現をしていました。

    キャンセル

回答 3

checkベストアンサー

+4

こんにちは。

これは根本的に勘違いしている部分があって、「本質的に非同期でない処理を Task として提供するな」というだけの話なんですね。
内部に非同期的な処理が含まれるなら、それを実行するメソッドも非同期になるし、非同期的な処理が含まれないなら、それは同期メソッドになるべきなのです。
その同期メソッドを非同期実行するかどうかはユーザが勝手に選べ、というのがライブラリ API 設計のベストプラクティスだと言う意味です。
「戻り値にTaskを持ったメソッドを作ろうとしています。」というのがそもそも見当違いなのです。戻り値の Task は「そうせざるを得ない」でなるものです。

質問のメソッドを見てみましたが、Task.Run で包もうとしているのが同期処理なのであれば、「その同期処理をそのまま提供してください。」
非同期処理なら「非同期メソッドにして、Task.Run は使わないでください。」
TaskCompletionSource は、イベント処理などの非同期スケジュールを実装しなければならない場合に使うものなので、質問のコードの場合ではおそらく不適当です。

もう少し詳細な要件や事情に踏み込んだサンプルがあれば、ピンポイントな話ができると思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/06/18 12:03

    なるほど。誤解があったかもしれません。読み直してみます。

    キャンセル

  • 2019/06/18 19:18

    tamotoさん
    Zuishinさん
    gentaroさん

    回答と多くの補足説明をありがとうございます。
    ここまで多くのレスがつくとは思っておらず、非常にびっくりしました。

    >「戻り値にTaskを持ったメソッドを作ろうとしています。」というのを「(中身が同期処理のみで構成されたものを) 戻り値を Task にしたメソッドを作ろうとしています。」と読み取ったことに拠ります。
    私がやろうとしていたことはこの通りでございます。

    ここまでの回答をまとめさせて頂きますと、
    ・同期処理しかしないのに Task として提供するな。
    ・なるべくしてTaskを返すメソッドとなるはず(メソッド内で非同期処理が呼ばれている)
    ・C# の非同期フローは、構築された Task が「開始済み」であることを暗黙的に要求している

    つまり、私が実装しようとしていた事自体が非同期ライブラリのポリシーに反していたということですね。作りながら違和感を覚えていたので、根本的な間違いに気付けてよかったです。ありがとうございました!

    キャンセル

  • 2019/06/18 20:12

    私も途中混乱してしまいましたが、考えを整理するのに役立ちました。ありがとうございます。

    中身が非同期処理なのであれば当然の結果としてTaskが戻り値になるし、同期処理だけをラップしたTaskの(つまりTask.Run()の結果を返す)場合は、目的はおそらく非ブロッキング処理にしたい、とか並列実行したい、というものでしょうけど、それは利用者側の責務であってライブラリでやるべきじゃない、という事ですね。元記事の内容もなんとなく理解できた気がします。

    キャンセル

+3

参考にされている記事の意味がよくわかっていませんので外れかもしれませんが・・・

Task.Run を使わない => ライブラリの利用者に非同期を強要しない・・・という意味ではなかろうかと思います。

その理解で正しければ、どうすれば良いかは、同期・非同期を選択できる余地を与えると言うことになるかと思います。

具体的には、例えば、

(1) 同期版・非同期版の両方のメソッドを実装する、または

(2) 同期版のみ実装してユーザーが非同期にしたい場合は、ユーザーに Task.Run を使ってもらう。

・・・で、少なくとも記事に書いてあった懸念はなくなると思うのですが。

HttpClient には非同期版しか実装してないようなのが解せませんが (隠し拡張メソッドの同期版とかがある? 参考にされている記事は 6 年も前のものなので、状況が変わった?)

以上、個人的意見です。外れでしたらすみません。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/06/18 09:15

    この記事、直訳っぽくてぱっと読んだだけでは頭に入ってこないのでなかなか理解できていないですけど、少なくとも(1)は記事の内容と矛盾しません?

    > 責任あるライブラリ開発者になる必要があります。メソッドが本当に同期処理を行うなら、非同期バージョンを提供せずに、同期バージョンだけを提供します。同様に本当に非同期だったら、非同期バージョンだけを提供します。

    まぁ非同期版のメソッドをWaitでラップした同期版を作るな、という文脈ではありますが…。

    キャンセル

  • 2019/06/18 10:15 編集

    > 少なくとも(1)は記事の内容と矛盾しません?

    どうでしょう? 

    記事の“ライブラリ内でTask.Runを使わない”のセクションに書いてあるのは、非同期メソッドを使うと (a) スレッドプールからスレッドを取得するのに時間がかかるという問題がある、(b) スレッドプールの枯渇の問題がある・・・と言う 2 点です。

    自分はここだけ見て回答に書きました、Task.Run を使わない => ライブラリの利用者に非同期を強要しない・・・という意味ではなかろうかと思ったわけです。

    そういう意味にとらえれば矛盾はしないと思いますけど。

    (a), (b) の問題点を知らないライブラリの利用者が、使うべきでないのに深く考えずに(マルチスレッドで処理が早くなるというような勘違いをして)非同期版を使ってしまうという話はあるかもしれませんが、それはまた違う問題でしょう。

    実際、IO バウンドを行う非同期版・同期版メソッド両方を実装した Microsoft のライブラリはありますし、サービス参照で非同期版・同期版のメソッド両方のメソッドが自動生成されます。

    キャンセル

+1

TaskCompletionSource を使うからでは?

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.59%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る