none
Async Awaitを用いて同期処理をした時に、Awaitせずにフォームが終了してしまう。 RRS feed

  • 質問

  • お世話になります。

    .Net4.6
    VS2015です。

    2つのForm、「FormTest1」「FormTest2」を用意し
    FormTest1からFormTest2をShowDialogにて呼び出しています。

    呼び出す前にFormTest1で定義した関数をFormTest2に渡し、
    FormTest2が起動したら処理を行い、処理が終わったらFormTest2が閉じるというものを作ろうと思っています。

    ですがこのとき、渡す関数の中でもAwaitを使っていると、FormTest2でAwaitされずに終了してしまいます。

    FormTest1にはButton1とButton2を設置し、2つの呼び出し方を試しています。

    【Button1クリック】:(OK)FormTest2は処理が終わってから終了する。
    ・HeavyFuncをFormTest2に渡す。

    【Button2クリック】:(NG)FormTest2がすぐ終了してしまう。
    ・CallSubをFormTest2に渡す。
    ・CallSub内でさらに別タスクにてHeavySubを呼び出しAwaitする。

    なぜButton2クリックではきちんと待機しないのでしょうか?
    Awaitしている関数内でさらにAwaitすることはできないのでしょうか?

    またどのようにしたら問題なく処理を終えることができるのでしょうか。ご教授ください。

    以下に試したプログラムを記載します。

    ☆FormTest1

    Public Class FormTest1 Private Sub FormTest1_Load(sender As Object, e As EventArgs) Handles MyBase.Load End Sub Public Async Sub CallSub(str As String, St As Integer, Fn As Integer) Console.WriteLine("----------CallSub Called------------") Await Task.Run(Sub() HeavySub(str, St, Fn) End Sub) Console.WriteLine("----------CallSub End------------") End Sub Public Sub HeavySub(str As String, St As Integer, Fn As Integer) Dim cnt As Integer Console.WriteLine(str) Parallel.For(Of Integer)(St, Fn, Function() St, Function(i, loopState, localVal) '重い処理のつもり Dim v As Double For j = 0 To 100000000 v = (i + 0.000000001) / (j + 0.99999999) * (j) Next localVal += 1 Console.WriteLine("HeavyMainAction") Return localVal End Function, Sub(localVal) Console.WriteLine("HeavyEndAction") Threading.Interlocked.Add(cnt, localVal) End Sub) Console.WriteLine("Cnt:{0} Parallel.For End", cnt) End Sub Public Sub WriteLine(str As String) Console.WriteLine(str) End Sub Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Dim frmTst2 As New FormTest2 frmTst2.StartAction = AddressOf HeavySub frmTst2.EndAction = AddressOf WriteLine Console.WriteLine("frmTst2 Show") frmTst2.ShowDialog() Console.WriteLine("frmTst2 Closed") End Sub Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click Dim frmTst2 As New FormTest2 frmTst2.StartAction = AddressOf CallSub frmTst2.EndAction = AddressOf WriteLine Console.WriteLine("frmTst2 Show") frmTst2.ShowDialog() Console.WriteLine("frmTst2 Closed") End Sub End Class

    ☆FormTest2

    Public Class FormTest2
        Public Property StartAction As Action(Of String, Integer, Integer)
        Public Property EndAction As Action(Of String)
    
    
        Private Async Sub FormTest2_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Console.WriteLine("-------------------------------------------------")
            Await Task.Run(Sub()
                               StartAction.Invoke("StartActionCalled", 0, 150)
                           End Sub)
            Console.WriteLine("-------------------------------------------------")
            Console.WriteLine("StartActionEnd")
            EndAction.Invoke("EndAction")
            Me.Close()
        End Sub
    End Class



    • 編集済み xxTKCxx 2017年1月27日 8:48
    2017年1月27日 8:37

回答

  • まず、FormTest2で非同期で処理を行い待機するという仕様が決まっているのであれば、Func<Task>型で渡すことを前提にしていいのではないですか。

    その上で渡すほうがそのインターフェースにあわせて渡し方を変えればいいと思いますが、やってみたらぐちゃぐちゃになってしまった気もする…

    Public Class FormTest1
    
        '------------
        '省略
        '------------
        Public Async Function CallSub(str As String, St As Integer, Fn As Integer) As Task
            Await Task.Run(Sub()
                               HeavySub(str, St, Fn)
                           End Sub)
        End Function
    
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Dim frmTst2 As New FormTest2
            frmTst2.StartAction = Function(str, i1, i2)
                                      Return Task.Run(Sub()
                                                          HeavySub(str, i1, i2)
                                                      End Sub)
                                  End Function
            frmTst2.EndAction = AddressOf WriteLine
            frmTst2.ShowDialog()
        End Sub
    
        Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
            Dim frmTst2 As New FormTest2
            frmTst2.StartAction = AddressOf CallSub
            frmTst2.EndAction = AddressOf WriteLine
            frmTst2.ShowDialog()
        End Sub
    End Class
    
    Public Class FormTest2
        Public Property StartAction As Func(Of String, Integer, Integer, Task)
        Public Property EndAction As Action(Of String)
    
    
        Private Async Sub FormTest2_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Await StartAction.Invoke("StartActionCalled", 0, 150)
            EndAction.Invoke("EndAction")
            Me.Close()
        End Sub
    End Class

    • 回答としてマーク xxTKCxx 2017年2月8日 7:47
    2017年1月30日 14:00
    モデレータ

すべての返信

  • こんにちは。

    Button2の場合はTask.Runの中でさらにasync voidの処理を実行しているので待機せずに終わっているのだと思います。

    サンプルから、何をやりたいのかがよくわからないのですが、
    そもそもForm2でTask.Runする必要あるのでしょうか。
    Func<Task>デリゲートを渡して直接Awaitすれば済むような気もします。

    2017年1月27日 8:55
    モデレータ
  • Tak1wa様

    回答いただきありがとうございます。

    >Func<Task>デリゲートを渡して直接Awaitすれば済むような気もします。

    というのは、どういうことでしょうか??私のサンプルプログラムでいう、FormTest2_Loadの中で、

            Await Task.Run(Sub()
                               StartAction.Invoke("StartActionCalled", 0, 150)
                           End Sub)

    としている部分を

            ’Await Task.Run(Sub()
                               StartAction.Invoke("StartActionCalled", 0, 150)
                           ’End Sub)

    というふうにして、直接呼び出すということでよろしいですか?

    実際にやってみましたが、これだと、Button2イベントが終わってからしかHeavyFuncが実行されず、また、HeavyFuncを実行する前にFormTest2は閉じてしまいます。

    実際にやりたいこととしては自作のプログレスバーFormの表示です。

    • メインフォーム(FormTest1)でボタンをクリック→処理を開始
    • プログレスバーFormをモーダル表示(メインフォームの操作を禁じたい)
    • プログレスバーFormには進捗更新のための画面表示と、処理のキャンセル操作ができるようにしたい
    • 処理の終了後に自動的にプログレスバーFormを閉じたい

    という内容です。

    以上よろしくお願いいたします。


    2017年1月30日 0:56
  • 違います。

    単純に、Task<Func>の型で

    Await StartFunc(xxx, xxx)

    のようにすれば待機してくれるはずですが。

    上記についてよくわかりにくいのであれば、
    WindowsFormsの場合はBackgroundWorkerを利用することでその用途の実装を簡単に行うことが出来ますので
    そちらも試されてはどうでしょうか。

    2017年1月30日 1:09
    モデレータ
  • Tak1wa様

    回答ありがとうございます。

    仰っていることの意図はなんとなくわかりましたが、具体的にどのように記述すればよいのかわかりません。
    恐れ入りますがこのサンプルにおいてどのように記述すればよいか教えていただけないでしょうか。
    疑問はそのままにしたくないので。。。

    今回の件が理解できた上でBackGroundWorkerについてはまた別途試してみたいと思います。情報ありがとうございます。

    宜しくお願い致します。


    • 編集済み xxTKCxx 2017年1月30日 5:40
    2017年1月30日 5:28
  • まず、FormTest2で非同期で処理を行い待機するという仕様が決まっているのであれば、Func<Task>型で渡すことを前提にしていいのではないですか。

    その上で渡すほうがそのインターフェースにあわせて渡し方を変えればいいと思いますが、やってみたらぐちゃぐちゃになってしまった気もする…

    Public Class FormTest1
    
        '------------
        '省略
        '------------
        Public Async Function CallSub(str As String, St As Integer, Fn As Integer) As Task
            Await Task.Run(Sub()
                               HeavySub(str, St, Fn)
                           End Sub)
        End Function
    
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Dim frmTst2 As New FormTest2
            frmTst2.StartAction = Function(str, i1, i2)
                                      Return Task.Run(Sub()
                                                          HeavySub(str, i1, i2)
                                                      End Sub)
                                  End Function
            frmTst2.EndAction = AddressOf WriteLine
            frmTst2.ShowDialog()
        End Sub
    
        Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
            Dim frmTst2 As New FormTest2
            frmTst2.StartAction = AddressOf CallSub
            frmTst2.EndAction = AddressOf WriteLine
            frmTst2.ShowDialog()
        End Sub
    End Class
    
    Public Class FormTest2
        Public Property StartAction As Func(Of String, Integer, Integer, Task)
        Public Property EndAction As Action(Of String)
    
    
        Private Async Sub FormTest2_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Await StartAction.Invoke("StartActionCalled", 0, 150)
            EndAction.Invoke("EndAction")
            Me.Close()
        End Sub
    End Class

    • 回答としてマーク xxTKCxx 2017年2月8日 7:47
    2017年1月30日 14:00
    モデレータ
  • Tak1wa様

    返信遅くなり申し訳ございません。
    お教えいただいたやり方で希望の動作ができました。

    Taskを返すというのがどうするのかわからなかったのですが、Awaitが入っていると暗黙的に返されるのですね。

    このたびは大変ありがとうございました。

    2017年2月8日 7:47