非同期DB更新方法について への返答

投稿で使用できる特殊コードの説明。(別タブで開きます。)
本名は入力しないようにしましょう。
投稿した後で削除するときに使うパスワードです。返答があった後は削除できません。
返答する人が目安にします。相手が小学生か社会人かで返答の仕方も変わります。
最初の投稿が質問の場合、質問者が解決時にチェックしてください。(以降も追加書き込み・返信は可能です。)
※「過去ログ」について書くときはその過去ログのURLも書いてください。

以下の返答は逆順(新しい順)に並んでいます。

投稿者 YUU  (社会人) 投稿日時 2016/9/29 15:44:57
返信ありがとうございます。
メソッドを分割する手法をとることにしました。

UIスレッドと非UIスレッドを正しく認識し利用する必要がありそうですね。

>ちなみに,これらをContinueWith使って書き直すのは結構骨が折れます。
>たぶん,こんな感じです (コンパイルすらしていません)。
コンパイルが出来ませんでした。task2~4で怒られます。

task2に関しては、下記で通りました。
        Dim task2 = task1.ContinueWith(Function(t)
                                           Return BulkCopyToTableAsync(tableName, dt)
                                       End Function, TaskContinuationOptions.OnlyOnRanToCompletion).Unwrap()


task3と4はこれらで通りませんでした。
'TaskContinuationOptions.OnlyOnOnlyOnFaultedでエラー?。 
        Dim task3 = task1.ContinueWith(Sub(t) MessageBox.Show(t.Exception.Message), CancellationToken.None, TaskContinuationOptions.OnlyOnOnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext())
        Dim task4 = task2.ContinueWith(Sub(t) MessageBox.Show(t.Exception.Message), CancellationToken.None, TaskContinuationOptions.OnlyOnOnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext())



また、これら更新メソッドを呼ぶ側にも気になる点がございます。
    Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        Button1.Enabled = False
        Loading.Visible = False 'gif表示 

        Dim dt As DataTable = '対象のファイルを読み込みDataTable型で渡す。 
        If dt Is Nothing Then
            Exit Sub
        End If

        Await UpdateTableAsync("TableName", dt)

        Button1.Enabled = True
        LoadingAnime.Visible = False

    End Sub


○buttonイベントで呼び出しているのですが、ボタンの状態offとgifの表示がワンクッション遅れてしまいます。Asyncの特性なのでしょうか?。それとも画面の更新が追いついていないだけでしょうか。
投稿者 YuO  (社会人) 投稿日時 2016/9/28 13:05:46
> ContinueWithを使用した実装処理はどのように実装すればよいのでしょうか。
まじめに書くとAsync/Awaitを使うのに比べて非常に面倒になります。
なので,Async/Awaitだけで書いた方がよいと思います。

まず,単純にメソッドを一度まとめてしまうと,
Private Async Function UpdateTableAsync(ByVal tableName As StringByVal dt As DataTable) As Task
    Try
        '対象のテーブルを一括削除。  
        Using con As New SqlConnection(strConnection)
            Using cmd As New SqlCommand()
                Await con.OpenAsync()
                cmd.Connection = con
                cmd.CommandText = "DELETE FROM " & tableName
                cmd.CommandTimeout = 3600
                Await cmd.ExecuteNonQueryAsync()
                con.Close()
            End Using
        End Using

        '対象のテーブルにBulkCopy。  
        Using con As New SqlConnection(strConnection)
            Using sqlBulkCopy As New SqlBulkCopy(con)
                sqlBulkCopy.DestinationTableName = tableName
                sqlBulkCopy.BulkCopyTimeout = 3600
                Await con.OpenAsync()
                Await sqlBulkCopy.WriteToServerAsync(dt)
                con.Close()
            End Using
        End Using
    Catch ex As Exception
        MessageBox.Show(ex.Message)
    End Try
End Function
のようになります。
DELETEに失敗した時点でBulk Copyする意味が無いため,Try-Catchの範囲を変更していますが。

なお,Async/Awaitを使っている場合,UIスレッドから呼び出されたとすると,
・Awaitしているメソッドが内部でTaskを作っているような場合は非UIスレッド
・それ以外の部分はUIスレッドで動作します。
# ConfigureAwait(False)をしている場合を除く。この場合はメソッド終了まで非UIスレッドで動きます。

メソッドに分割し直すと,
'対象のテーブルを一括削除。  
Private Async Function DeleteTableAsync(ByVal tableName As StringAs Task
    Using con As New SqlConnection(strConnection)
        Using cmd As New SqlCommand()
            Await con.OpenAsync().ConfigureAwait(False)
            cmd.Connection = con
            cmd.CommandText = "DELETE FROM " & tableName
            cmd.CommandTimeout = 3600
            Await cmd.ExecuteNonQueryAsync().ConfigureAwait(False)
            con.Close()
        End Using
    End Using
End Function

'対象のテーブルにBulkCopy。  
Private Async Function BulkCopyToTableAsync(ByVal tableName As StringByVal dt As DataTable) As Task
    Using con As New SqlConnection(strConnection)
        Using sqlBulkCopy As New SqlBulkCopy(con)
            sqlBulkCopy.DestinationTableName = tableName
            sqlBulkCopy.BulkCopyTimeout = 3600
            Await con.OpenAsync().ConfigureAwait(False)
            Await sqlBulkCopy.WriteToServerAsync(dt).ConfigureAwait(False)
            con.Close()
        End Using
    End Using
End Function

Private Async Function UpdateTableAsync(ByVal tableName As StringByVal dt As DataTable) As Task
    Try
        Await DeleteTableAsync(tableName)
        Await BulkCopyToTableAsync(tableName, dt)
    Catch ex As Exception
        MessageBox.Show(ex.Message)
    End Try
End Function
でしょうか。
UIに関係ない部分をメソッドに切り出して,ConfigureAwait(False)によってUIスレッドに戻す処理を行わないようにしています。


ちなみに,これらをContinueWith使って書き直すのは結構骨が折れます。
Private Function UpdateTableAsync(ByVal tableName As StringByVal dt As DataTable) As Task
    Dim task1 = DeleteTableAsync(tableName)
    Dim task2 = task1.ContinueWith(Function (t) BulkCopyToTableAsync(tableName, dt) End Function, TaskContinuationOptions.OnlyOnRanToCompletion).UnWrap()
    Dim task3 = task1.ContinueWith(Sub (t) MessageBox.Show(t.Exception.Message) End Sub, CancellationToken.None,TaskContinuationOptions.OnlyOnOnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext())
    Dim task4 = task2.ContinueWith(Sub (t) MessageBox.Show(t.Exception.Message) End Sub, CancellationToken.None,TaskContinuationOptions.OnlyOnOnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext())

    Return Task.WhenAll(task3, task4)
End Function
たぶん,こんな感じです (コンパイルすらしていません)。
なお,Async/Awaitは状態機械を作るので,上記とは異なるコードを作成します。
投稿者 YUU  (社会人) 投稿日時 2016/9/28 11:22:28
YuO様、返信いただきありがとうございます。

>DELETE→BULK COPYでないといけない作業ですよね。
その認識で誤りございません。順序を適切に守る必要のあるメソッドではあるのですがどうしてもUIの硬直が気になり非同期デビューした次第です。

AsyncAwaitが導入された4.5以降書きやすくなっているとはいわれていますが、いまひとつサンプルが少なく四苦八苦しております。

現状のコードではTask1とTask2が平行処理されてしまうのですね。テーブルの整合性をチェックしても問題なく更新できているようなので気付きませんでした。(たまたま?。)

ContinueWithを使用した実装処理はどのように実装すればよいのでしょうか。

>ただ,MessageBox.ShowするならばUIスレッドで呼んだ方が良いでしょう。 
invokeするということでしょうか?メッセージを表示するのもUIスレッドに当たるのですね。

お手数おかけいたします。
投稿者 YuO  (社会人) 投稿日時 2016/9/27 12:59:15
同一テーブルへの操作で,本来順序のある作業ですよね。
つまり,DELETE→BULK COPYでもBULK COPY→DELETEでもよいわけではなく,
DELETE→BULK COPYでないといけない作業ですよね。

そうであるならば,この2つを並列なタスクとしてはいけません。
DELETE後にBULK COPYを行う一つのタスクにするか,ContinueWithによる継続タスクにするかです。


エラーの表示に関して,MessageBox.ShowがUIスレッド以外から呼ばれて良いかどうかはわかりません。
WinForms/WPFの常識で言えば,UI要素はUIスレッド以外から触れないのでダメですが,
MSDNにはUIスレッド以外から呼んだときに例外が発生するようなことが書かれていないので。

ただ,MessageBox.ShowするならばUIスレッドで呼んだ方が良いでしょう。
投稿者 YUU  (社会人) 投稿日時 2016/9/26 18:15:19
現在下記のコードにてテーブル更新処理を行っております。

    ''' <summary> 
    ''' テーブルデータの削除 
    ''' </summary> 
    ''' <param name="tableName"></param> 
    ''' <returns></returns> 
    Private Async Function DeleteTable(ByVal tableName As StringAs Task

        Try
            Using con As New SqlConnection(strConnection)
                Using cmd As New SqlCommand()
                    Await con.OpenAsync()
                    cmd.Connection = con
                    cmd.CommandText = "DELETE FROM " & tableName
                    cmd.CommandTimeout = 3600
                    Await cmd.ExecuteNonQueryAsync()
                    con.Close()
                End Using
            End Using
        Catch ex As Exception
            MessageBox.Show(ex.Message)
        End Try

    End Function

    ''' <summary> 
    ''' BulkCopyを利用したテーブル更新。 
    ''' </summary> 
    ''' <param name="tableName">テーブル名</param> 
    ''' <param name="dt">データテーブル</param> 
    ''' <returns></returns> 
    Private Async Function UpdateTableAsync(ByVal tableName As StringByVal dt As DataTable) As Task

        '対象のテーブルを一括削除。 
        Dim Task1 = DeleteTable(tableName)

        '対象のテーブルにBulkCopy。 
        Dim Task2 = Task.Run(Sub()
                                 Try
                                     Using con As New SqlConnection(strConnection)
                                         Using sqlBulkCopy As New SqlBulkCopy(con)
                                             sqlBulkCopy.DestinationTableName = tableName
                                             sqlBulkCopy.BulkCopyTimeout = 3600
                                             con.Open()
                                             sqlBulkCopy.WriteToServer(dt)
                                             con.Close()
                                         End Using
                                     End Using
                                 Catch ex As Exception
                                     MessageBox.Show(ex.Message)
                                 End Try
                             End Sub)

        Await Task.WhenAll(Task1, Task2)

    End Function



一応、上記のコードにて当方の目的どおり更新されているのですが疑問がございます。

○Task1とTask2の実装方法は適切でしょうか。いろいろ調べているとロック等が必要?。
Task1とTask2は同時に実行されてしまうというような記事を見つけました。削除されていない状態でBulkCopyは使用できないのですが現在は実行できているように見受けられます。WhenAllでの処理待ちはこの場合適切?。

○エラー処理の実装は適切なのでしょうか。デバック時にエラーが表示されず、スルーされているようなのですが。

お知恵お貸ししていただけますと幸いです。