7
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ひとりでAdvent Calendar 2019

Day 14

C# 非同期

Last updated at Posted at 2019-12-13

非同期というのは並列処理の一種。
例えば、コンビニで例えると店員二人で客の対応を行うのがマルチスレッド。
対して、お弁当温めてくださいと言われたときに電子レンジに温めるのを任せ、他の客の対応を行うのが非同期。

コードで例を見てみよう。
その前に準備

###準備:Stopwatchクラス

C#にはデフォルトで時間を測るStopwatchクラスが存在する。
stopWatch.Startが時間計測スタート。

Program.cs
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は非同期メソッドの状態を返す。

Program.cs
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秒待つことになる。

Program.cs
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メソッドが終わるまで待つ

Program.cs
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

###後で待機することも可能。

Program.cs
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が全て終了するまで待機する。

Program.cs
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を妨害することなくイベントの実行ができる
まずは非同期を使わない場合

MainWindow.xaml
<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>
MainWindow.xaml.cs
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を操作することはできない。
このように重い処理がイベントに入ってしまっている場合ユーザーの利用を阻害してしまうことがあるので、その場合非同期を使うのが有効となる。

非同期を使った場合。

MainWindow.xaml.cs
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を使わなくても待機する方法がある。

Program.cs
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などで使うとデットロックが発生し動かなくなってしまう。

MainWindow.xaml.cs
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)をしてあげるとデットロックは起こらない。

MainWindow.xaml.cs
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なしで待機することができる。

Program.cs
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とすればよい。

Program.cs
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フィールドによって値を見ることができる。

Program.cs
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
7
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?