tixture55’s diary

主にプログラミング関係の日記です。

C#で非同期でダウンロードする処理を書く

C#で非同期処理を書く際の文法やルールをまとめてみました。非同期処理にはasync/awaitを使いますが、これが使えるようになったのはC#5.0からです。

 

同期処理でファイルをダウンロードする処理

 
using System.IO;
using System.Net;

public static string ReadFromUrl(Uri url)
{
      using (WebClient webClient = new WebClient())
      {
             using (Stream stream = webClient.OpenRead(url))
             {
                    TextReader tr = new StreamReader(stream, Encoding.UTF8, true);
                    string body = tr.ReadToEnd();
 
                    return body;
              }
       }
}

public static void Download()
{
        Uri url = new Uri("https://github.com/hoge/master/README.md");
        string body = ReadFromUrl(url);
        Console.WriteLine(body);
}

StreamReaderで文字列として読み取ります。非同期に書き換えると、下記のようになります。

 

 
 

using System.IO;
using System.Net;
using System.Threading.Tasks;

public static async Task<string> ReadFromUrlAsync(Uri url)
{
    using (WebClient webClient = new WebClient())
    {

       using (Stream stream = await webClient.OpenReadTaskAsync(url))
       {
           TextReader tr = new StreamReader(stream, Encoding.UTF8, true);
           string body = await tr.ReadToEndAsync();

 

           return body;
        }
             }
}

public static async Task DownloadAsync()
{
           Uri url = new Uri("https://github.com/hoge/master/README.md");
           string body = await ReadFromUrlAsync(url);
           Console.WriteLine(body);
}

 

意外と非同期に書き換えるやり方は単純で「非同期だよ」と書いてやることがまずやることみたいです。それは前提として、非同期処理は呼び出し元から末端まで、「全て非同期メソッドで実装する必要がある」と言う事です。

 

非同期処理の挙動

非同期処理を行うメソッドはそれが実行中でもUI側は動かすことができます。例えば、非同期メソッドでwaitする処理が走っても、UI側まで待機になって固まることはないわけですね。

 

非同期処理を書く際のポイント

  • メソッドの中で、awaitキーワードを使うためには、そのメソッド定義時にasyncキーワードを付けてやる必要があります。
  • 同期させたい処理の戻り値がstringなら、それに合わせて非同期処理の戻り値はTask<string>にする必要がある。
  • Task=非同期ではない。非同期処理ではないメソッドをTaskを使って書くこともできる。
  • 非同期メソッドの戻り値は、Task型かTask<T>型かvoid型にしてやる必要がある。voidはほぼ使わない。
  • 返されたTask<T>をawaitすると、目的の文字列が得られる。

awaitキーワードの効果は、「指定したTaskの完了を待つ」「そして、その結果を取り出す」です。 

 

非同期処理の怖い所

 非同期処理で扱うTaskはただの処理の集合体で、ポリシーとしては「書かれている処理が順番どおり実行できればOK」なので、どのスレッドが実行したかとかお構いなしなわけです。というかTaskはただの作業手順書なので、そもそもスレッドの存在など気にしていません。

 なので、デッドロックやスレッドの実行順序するプログラムが別途必要となります。

 

デッドロックになるパターン

そもそもawaitとは「〜を待つ」という意味で、このキーワードが付いたメソッドは呼び出し元のメソッドの完了を待ってから実行されます。なので、呼び出される側でTask.Wait()をした場合、呼び出し側を待機中にしてしまうので結果、デッドロックとなってしまいます。

 

デッドロック対策

  • 今後非同期処理に改修する可能性があるならTask.Wait()は使わない
  • とにもかくにも、awaitを使う際には、必ずConfigureAwait(false)を書く。
 
 
private async void Page_Load(object sender, EventArgs e)
{
          await HeavyProcessingAsync().ConfigureAwait(false);
}

 

Task.Wait()使わなくても、WhileループやResultプロパティの参照とかで余裕でデッドロックになるみたいです。(Result プロパティは、タスクが終了するまで呼び出し元のスレッドをブロックします。)

ConfigureAwait(false)をつければ、特定のスレッドに戻らなくても良いので、デッドロックを防げます。しかし、ListなどのUI処理では特定のスレッドに戻る必要があるので、そこだけ留意すればよいらしいです。

 

参考サイト

qiita.com

qiita.com

方法: タスクから値を返す | Microsoft Docs