非同期というのは並列処理の一種。
例えば、コンビニで例えると店員二人で客の対応を行うのがマルチスレッド。
対して、お弁当温めてくださいと言われたときに電子レンジに温めるのを任せ、他の客の対応を行うのが非同期。
コードで例を見てみよう。
その前に準備
準備:Stopwatchクラス
C#にはデフォルトで時間を測るStopwatchクラスが存在する。
stopWatch.Startが時間計測スタート。
using Newtonsoft.Json;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Diagnostics;
using System.Threading;
namespace ConsoleApp9
{
    class Program
    {
        static void Main()
        {
            var stopWatch = new Stopwatch();
            stopWatch.Start();
            Thread.Sleep(3000);
            Console.WriteLine(stopWatch.Elapsed);
        }
    }
}
実行結果
00:00:03.0010707
async await
メソッドにasyncをつけるとそのメソッドは非同期メソッドになる。
awaitをつけると非同期メソッドが終了するまで待機する。
Task.Delay(1)はダミー行。asyncメソッドの中でawaitがないと同期で実行されてしまうため入れている。
戻り値のTaskというのはjavascriptでいうところのresolve。
Taskは非同期メソッドの状態を返す。
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        static void Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            Exe1();
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task Exe1()
        {
            await Task.Delay(1);
            Thread.Sleep(3000);
        } 
    }
}
実行結果
00:00:00.0495786
これでawait Task.Delay(1)の行がなければ同期実行されるので3秒待つことになる。
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        static void Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            Exe1();
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task Exe1()
        {
            Thread.Sleep(3000);
        } 
    }
}
実行結果
00:00:03.0207121
awaitをつけるとasyncメソッドが終わるまで待つ
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        async static Task Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            await Exe1();
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task Exe1()
        {
            await Task.Delay(1);
            Thread.Sleep(3000);
        } 
    }
}
実行結果
00:00:03.0207121
後で待機することも可能。
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        async static Task Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            var task = Exe1();
            Console.WriteLine("Hello");
            await task;
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task Exe1()
        {
            await Task.Delay(1);
            Thread.Sleep(3000);
            Console.WriteLine("Hello2");
        } 
    }
}
実行結果
Hello
Hello2
00:00:03.0551079
複数のTaskを待機する。
Thread.SleepとTask.Delayはどちらも待機なのでTask.Delayを使っていく。
Task.WhenAll(list)はlist内のtaskが全て終了するまで待機する。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        async static Task Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            var task = Exe1();
            var list = new List<Task>();
            for(var i = 0; i < 5; i++)
            {
                list.Add(Exe1());
            }
            await Task.WhenAll(list);
            Console.WriteLine("Hello");
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task Exe1()
        {
            await Task.Delay(3000);
            Console.WriteLine("Hello2");
        } 
    }
}
WPFでの非同期
WPFのイベントで非同期メソッドを使うことによって、UIを妨害することなくイベントの実行ができる
まずは非同期を使わない場合
<Window x:Class="WpfApp13.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp13"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Button Content="ボタン" Width="100" Height="100" Click="Button_Click"></Button>
    </Grid>
</Window>
using System.Threading;
using System.Windows;
namespace WpfApp13
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Thread.Sleep(3000);
            MessageBox.Show("Buttonをクリックしました。");
        }
    }
}
実行すると、3秒経った後メッセージボックスが表示される、3秒の間はWPFを操作することはできない。
このように重い処理がイベントに入ってしまっている場合ユーザーの利用を阻害してしまうことがあるので、その場合非同期を使うのが有効となる。
非同期を使った場合。
using System.Threading.Tasks;
using System.Windows;
namespace WpfApp13
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            await Task.Delay(3000);
            MessageBox.Show("Buttonをクリックしました。");
        }
    }
}
この場合メッセージを出すのはボタンをクリックしてから3秒後になるが、その間操作はできる。
戻り値がTaskではなくvoidになっているのはメソッドの登録を行うときにvoidでないといけないから。
Waitメソッドによる待機
実はawaitを使わなくても待機する方法がある。
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        static void Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            Exe1().Wait();
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task Exe1()
        {
            await Task.Delay(3000);
            Console.WriteLine("Hello2");
        } 
    }
}
実行結果
Hello2
00:00:03.0553411
しかし、このメソッドにはデメリットがありWPFなどで使うとデットロックが発生し動かなくなってしまう。
using System.Threading.Tasks;
using System.Windows;
namespace WpfApp13
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            WaitWrapMethod().Wait();
            MessageBox.Show("Buttonをクリックしました。");
        }
        private async Task WaitWrapMethod()
        {
            await Task.Delay(1000);
        }
    }
}
実行して、Button_Clickイベントと紐づいているボタンをクリックすると、デットロックを起こし動かなくなる。
Task.Delay(1000).ConfigureAwait(false)というようにConfigureAwait(false)をしてあげるとデットロックは起こらない。
using System.Threading.Tasks;
using System.Windows;
namespace WpfApp13
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            WaitWrapMethod().Wait();
            MessageBox.Show("Buttonをクリックしました。");
        }
        private async Task WaitWrapMethod()
        {
            await Task.Delay(1000).ConfigureAwait(false);
        }
    }
}
実行してボタンをクリックしてみるとデットロックは起こらない。
複数のTaskの待機
WaitAllメソッドを使うと複数のTaskをawaitなしで待機することができる。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        static void Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            var list = new List<Task>();
            for(var i = 0; i < 5; i++)
            {
                list.Add(Exe1());
            }
            Task.WaitAll(list.ToArray());
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task Exe1()
        {
            await Task.Delay(3000);
            Console.WriteLine("Hello2");
        } 
    }
}
実行結果
Hello2
Hello2
Hello2
Hello2
Hello2
00:00:03.0633685
非同期メソッドの戻り値
Genericを使うことによって戻り値を指定することができる。
例えば、stringが戻り値である場合非同期メソッドの時はTaskとすればよい。
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        async static Task Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            var t = await Exe1();
            Console.WriteLine(t);
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task<string> Exe1()
        {
            await Task.Delay(3000);
            return "Hello";
        } 
    }
}
実行結果
Hello
00:00:03.0480318
Resultによる非同期メソッドの戻り値
Task型の変数があったとき、Resultフィールドによって値を見ることができる。
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace ConsoleApp9
{
    class Program
    {
        static void Main()
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            var t = Exe1();
            var val = t.Result;
            Console.WriteLine(val);
            Console.WriteLine(stopwatch.Elapsed);
        }
        async static Task<string> Exe1()
        {
            await Task.Delay(3000);
            return "Hello";
        } 
    }
}
実行結果
Hello
00:00:03.0586377