none
スレッドの実装方法がよく分かりません RRS feed

  • 質問

  • VB.NET 2012
    .NET Framework 4.5

    ファイルの大量コピーを行うにあたり、ファイルのコピーとDBの登録を必要としています。
    そこで、ファイルのコピーは順次行い、フォルダ単位でファイルコピーが完了した段階でDBの登録を行おうと思っています。
    しかし、DBの登録などを行っている最中、ファイルのコピーが止まると時間がロスしてしまうため、DBの登録とは切り離して、ファイルコピーは順次継続したいです。

    スレッドなどを利用するとできると思うのですが、Task、Async、Awaitの使い方がいまいち分からないのと、エラーが発生時にはログ出力を行いたいです。
    それで『' **********』の中の部分をスレッド化?したいんですが、どのようにすると適切に処理できるでしょうか?(特に呼び出し方、メソッドの作り方)

            ' "\\old-nas\" & nameフォルダごとに処理する
            For Each csvRow As DataRow In csvRows
                Dim name As String = csvRow("name").ToString()
    
                Dim sourceDirectory As String = "\\old-nas\" & name
    
                ' 実在しないディレクトリだった場合は処理しない
                If Not IO.Directory.Exists(sourceDirectory) Then
                    rowNo += 1
                    Continue For
                End If
                
                ' 旧NASから新NASのTempディレクトリへファイルをコピー
                Dim sourceFilePathList() As String = IO.Directory.GetFiles(sourceDirectory, "*.*", IO.SearchOption.AllDirectories)
                If sourceFilePathList.Length = 0 Then
                    Continue For
                End If
                Dim nasTempDirectory As String = "\\new-nas\Temp\"
                Dim nasTargetDirectory As String = nasTempDirectory & name
                Dim nasTempFilePath As String = ""
                For Each sourceFilePath As String In sourceFilePathList
                    ' サムネイルファイルは移行対象外
                    If "thumbs.db".Equals(IO.Path.GetFileName(sourceFilePath).ToLower()) Then
                        Continue For
                    End If
    
                    nasTempFilePath = sourceFilePath.Replace("\\old-nas\", "\\new-nas\Temp\")
                    Dim copyDirectory As String = IO.Path.GetDirectoryName(nasTempFilePath)
                    If Not IO.Directory.Exists(copyDirectory) Then
                        IO.Directory.CreateDirectory(copyDirectory)
                    End If
                    IO.File.Copy(sourceFilePath, nasTempFilePath, True)
                Next
    
                ' **********
                ' NASのファイルから本来のパスを求めてファイルを移動する。DBの登録を行う。
                Dim trans As SqlClient.SqlTransaction = Nothing
                Try
                	' ごにょごにょして、Tempディレクトリのファイルを、本来設置したいディレクトリへ移動する
                	
                	trans = Db.BeginTrans()
                	' DBの登録
    
                    trans.Commit()
                    
                Catch ex As Exception
                    If Not trans Is Nothing Then
                        trans.Rollback()
                    End If
                    ' ログファイルにログを出力する
    
                End Try
                ' **********
            Next

    2015年8月13日 7:43

回答

  • コピー処理を止めたくないなら、データベース処理とコピー処理とを別スレッドにして、ディレクトリコピーが済んだらデータベーススレッドに完了通知し、通知を受けたらDBの登録処理するのが良いと思う
    ついでUIスレッドからも分離すれば、UIスレッドがフリーズしなくなります。その場合、タスク開始がUIスレッドからで、完了後もUIスレッドで後処理する必要があるなら、Async,Awaitを使えばUIスレッドと同期してくれます。同期する必要が無いならTaskを起動して投げっぱなしでもいいです。

    Public Class Form1
    
        Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Dim btn As Button = CType(sender, Button)
            btn.Enabled = False
            Dim f As Func(Of Boolean) = Function()
                                            '適当にコピーするディレクトリを用意
                                            Dim destDir As String = "E:\TS\20150814"
                                            Dim sourceDir As String = "E:\TS\20150813"
                                            Dim dt As New System.Data.DataTable
                                            dt.Columns.Add("name", GetType(String))
                                            For Each d As String In System.IO.Directory.GetDirectories(sourceDir)
                                                Dim r As DataRow = dt.NewRow()
                                                r(0) = System.IO.Path.GetFileName(d)
                                                dt.Rows.Add(r)
                                            Next
                                            Dim csvRows = dt.Rows
    
                                            Dim subDirectories As IEnumerable(Of String) = csvRows.Cast(Of DataRow).Select(Function(x) x("name").ToString())
                                            Dim copyDirectories As IEnumerable(Of PathPair) = GetMovePaths(destDir, sourceDir, subDirectories)
                                            Return FileMove(copyDirectories, TimeSpan.FromSeconds(1))
                                        End Function
            'UIがフリーズしないようにAsync,Awaitで処理
            Dim result As Boolean = Await Task.Run(f)
            btn.Enabled = True
        End Sub
    
        Private Function FileMove(ByVal copyPaths As IEnumerable(Of PathPair), ByVal timeout As TimeSpan) As Boolean
            Try
                'Using con As System.Data.SqlClient.SqlConnection = New SqlClient.SqlConnection("")
                '    Dim trans As SqlClient.SqlTransaction = con.BeginTransaction
                '    Dim cmd As New System.Data.SqlClient.SqlCommand("", con)
                '    Try
                '        con.Open()
    
                For Each pp As PathPair In GetMoved(copyPaths, timeout)
                    'コピーが済んだディレクトリが順番に列挙されてくる
                    System.Diagnostics.Debug.WriteLine(pp)
                Next
    
                '        trans.Commit()
                '    Catch ex As Exception
                '        trans.Rollback()
                '        Throw
                '    End Try
                'End Using
    
                MessageBox.Show("完了")
                Return True
            Catch ex As Exception
                MessageBox.Show(ex.Message)'エラーログ取りたければ
                Return False
            End Try
    
        End Function
    
        ''' <summary>ディレクトリコピーが完了したら順に列挙される</summary>
        ''' <param name="copyPaths">コピーパス一覧</param>
        ''' <param name="timeout">トータル時間のタイムアウト</param>
        ''' <returns>コピー完了した結果一覧</returns>
        Private Iterator Function GetMoved(ByVal copyPaths As IEnumerable(Of PathPair), ByVal timeout As TimeSpan) As IEnumerable(Of PathPair)
            Dim finish As New System.Threading.ManualResetEvent(False) 'ディレクトリコピーがすべて完了したことを通知するフラグ
            Dim moved As New System.Threading.ManualResetEvent(False) 'ディレクトリ移動が完了したことを通知するフラグ
            Dim waithandles As System.Threading.WaitHandle() = {moved, finish}
            Dim canceller As New System.Threading.CancellationTokenSource(timeout) 'キャンセル通知用フラグ
            Dim queue As New Queue(Of PathPair) '完了済みをやり取りするためのキュー
            Dim exMove As Exception = Nothing '途中で発生したエラー
            Dim moves As Action = Sub()
                                      Try
                                          'コピー処理するパスを順に取出し
                                          For Each pp As PathPair In copyPaths
                                              If (canceller.IsCancellationRequested) Then
                                                  Exit For
                                              End If
                                              pp.CopyToTempFromSource() 'ディレクトリコピー
                                              If (canceller.IsCancellationRequested) Then
                                                  Exit For
                                              End If
                                              pp.MoveToDestFromTemp() 'ディレクトリ移動
                                              If (canceller.IsCancellationRequested) Then
                                                  Exit For
                                              End If
    
                                              SyncLock (queue)
                                                  queue.Enqueue(pp) '移動済み一覧に登録
                                              End SyncLock
                                              moved.Set() 'ディレクトリ移動できた事を通知
                                          Next
                                      Catch ex As Exception
                                          exMove = ex
                                      Finally
                                          finish.Set()
                                      End Try
                                  End Sub
    
            'DB処理とは別タスクでコピー処理を開始
            Dim copyTask = Task.Run(moves)
    
            Try
                Dim waitResult As Integer = 0
                Do While (waitResult = 0) '全完了するまでループ
    
                    'ディレクトリコピーか全完了のどちらかになるまで待機
                    waitResult = System.Threading.WaitHandle.WaitAny(waithandles, 60000)
                    If (waitResult = System.Threading.WaitHandle.WaitTimeout) Then
                        'タイムアウトしたらエラー
                        Throw New TimeoutException()
                    End If
    
                    moved.Reset() '次の通知を待てるように
    
                    'コピー済みの結果を取り出し
                    Dim copyed As PathPair()
                    SyncLock (queue)
                        copyed = queue.ToArray()
                        queue.Clear()
                    End SyncLock
    
    
                    For Each pp As PathPair In copyed
                        Yield pp 'コピー済みの結果を列挙として返す
                    Next
    
                Loop
            Catch
                canceller.Cancel() '何かエラーになったらキャンセル
                Throw
            Finally
                copyTask.Wait() 'タスクが停止するのを待つ
            End Try
            If (exMove IsNot Nothing) Then
                Throw exMove
            End If
        End Function
    
        ''' <summary>コピー元とコピー先のパスを順番に列挙</summary>
        ''' <param name="destDirectory">コピー先の基準ディレクトリのパス</param>
        ''' <param name="sourceDirectory">コピー元の基準ディレクトリのパス</param>
        ''' <param name="subDirectoryNames">コピーしたいサブディレクトリの一覧</param>
        Private Shared Iterator Function GetMovePaths(ByVal destDirectory As String, ByVal sourceDirectory As String, ByVal subDirectoryNames As IEnumerable(Of String)) As IEnumerable(Of PathPair)
            For Each subDirectoryName As String In subDirectoryNames
                Dim pp As New PathPair
                pp.Source = System.IO.Path.Combine(sourceDirectory, subDirectoryName)
                pp.Destination = System.IO.Path.Combine(destDirectory, subDirectoryName)
                pp.Temp = System.IO.Path.Combine(destDirectory, "Temp")
                Yield pp
            Next
        End Function
    
        ''' <summary>コピー元とコピー先の情報</summary>
        Public Class PathPair
            Public Source As String 'コピー元
            Public Destination As String 'コピー先
            Public Temp As String '一時保存場所
    
            ''' <summary>一時保存場所にコピー</summary>
            Public Sub CopyToTempFromSource()
                If (System.IO.Directory.Exists(Source)) Then
                    My.Computer.FileSystem.CopyDirectory(Source, Temp)
                ElseIf (System.IO.File.Exists(Source)) Then
                    My.Computer.FileSystem.CopyFile(Source, Temp)
                End If
            End Sub
    
            ''' <summary>一時保存場所からコピー先に移動</summary>
            Public Sub MoveToDestFromTemp()
                If (System.IO.Directory.Exists(Source)) Then
                    My.Computer.FileSystem.MoveDirectory(Temp, Destination)
                ElseIf (System.IO.File.Exists(Source)) Then
                    My.Computer.FileSystem.MoveFile(Temp, Destination)
                End If
            End Sub
    
            Public Overrides Function ToString() As String
                Return Source + " => " + Temp + "=>" + Destination
            End Function
        End Class
    
    End Class
    


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 Unripe01 2015年8月14日 4:35
    • 回答としてマーク takiru 2015年8月15日 11:29
    2015年8月13日 15:22

すべての返信

  • コピー処理を止めたくないなら、データベース処理とコピー処理とを別スレッドにして、ディレクトリコピーが済んだらデータベーススレッドに完了通知し、通知を受けたらDBの登録処理するのが良いと思う
    ついでUIスレッドからも分離すれば、UIスレッドがフリーズしなくなります。その場合、タスク開始がUIスレッドからで、完了後もUIスレッドで後処理する必要があるなら、Async,Awaitを使えばUIスレッドと同期してくれます。同期する必要が無いならTaskを起動して投げっぱなしでもいいです。

    Public Class Form1
    
        Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Dim btn As Button = CType(sender, Button)
            btn.Enabled = False
            Dim f As Func(Of Boolean) = Function()
                                            '適当にコピーするディレクトリを用意
                                            Dim destDir As String = "E:\TS\20150814"
                                            Dim sourceDir As String = "E:\TS\20150813"
                                            Dim dt As New System.Data.DataTable
                                            dt.Columns.Add("name", GetType(String))
                                            For Each d As String In System.IO.Directory.GetDirectories(sourceDir)
                                                Dim r As DataRow = dt.NewRow()
                                                r(0) = System.IO.Path.GetFileName(d)
                                                dt.Rows.Add(r)
                                            Next
                                            Dim csvRows = dt.Rows
    
                                            Dim subDirectories As IEnumerable(Of String) = csvRows.Cast(Of DataRow).Select(Function(x) x("name").ToString())
                                            Dim copyDirectories As IEnumerable(Of PathPair) = GetMovePaths(destDir, sourceDir, subDirectories)
                                            Return FileMove(copyDirectories, TimeSpan.FromSeconds(1))
                                        End Function
            'UIがフリーズしないようにAsync,Awaitで処理
            Dim result As Boolean = Await Task.Run(f)
            btn.Enabled = True
        End Sub
    
        Private Function FileMove(ByVal copyPaths As IEnumerable(Of PathPair), ByVal timeout As TimeSpan) As Boolean
            Try
                'Using con As System.Data.SqlClient.SqlConnection = New SqlClient.SqlConnection("")
                '    Dim trans As SqlClient.SqlTransaction = con.BeginTransaction
                '    Dim cmd As New System.Data.SqlClient.SqlCommand("", con)
                '    Try
                '        con.Open()
    
                For Each pp As PathPair In GetMoved(copyPaths, timeout)
                    'コピーが済んだディレクトリが順番に列挙されてくる
                    System.Diagnostics.Debug.WriteLine(pp)
                Next
    
                '        trans.Commit()
                '    Catch ex As Exception
                '        trans.Rollback()
                '        Throw
                '    End Try
                'End Using
    
                MessageBox.Show("完了")
                Return True
            Catch ex As Exception
                MessageBox.Show(ex.Message)'エラーログ取りたければ
                Return False
            End Try
    
        End Function
    
        ''' <summary>ディレクトリコピーが完了したら順に列挙される</summary>
        ''' <param name="copyPaths">コピーパス一覧</param>
        ''' <param name="timeout">トータル時間のタイムアウト</param>
        ''' <returns>コピー完了した結果一覧</returns>
        Private Iterator Function GetMoved(ByVal copyPaths As IEnumerable(Of PathPair), ByVal timeout As TimeSpan) As IEnumerable(Of PathPair)
            Dim finish As New System.Threading.ManualResetEvent(False) 'ディレクトリコピーがすべて完了したことを通知するフラグ
            Dim moved As New System.Threading.ManualResetEvent(False) 'ディレクトリ移動が完了したことを通知するフラグ
            Dim waithandles As System.Threading.WaitHandle() = {moved, finish}
            Dim canceller As New System.Threading.CancellationTokenSource(timeout) 'キャンセル通知用フラグ
            Dim queue As New Queue(Of PathPair) '完了済みをやり取りするためのキュー
            Dim exMove As Exception = Nothing '途中で発生したエラー
            Dim moves As Action = Sub()
                                      Try
                                          'コピー処理するパスを順に取出し
                                          For Each pp As PathPair In copyPaths
                                              If (canceller.IsCancellationRequested) Then
                                                  Exit For
                                              End If
                                              pp.CopyToTempFromSource() 'ディレクトリコピー
                                              If (canceller.IsCancellationRequested) Then
                                                  Exit For
                                              End If
                                              pp.MoveToDestFromTemp() 'ディレクトリ移動
                                              If (canceller.IsCancellationRequested) Then
                                                  Exit For
                                              End If
    
                                              SyncLock (queue)
                                                  queue.Enqueue(pp) '移動済み一覧に登録
                                              End SyncLock
                                              moved.Set() 'ディレクトリ移動できた事を通知
                                          Next
                                      Catch ex As Exception
                                          exMove = ex
                                      Finally
                                          finish.Set()
                                      End Try
                                  End Sub
    
            'DB処理とは別タスクでコピー処理を開始
            Dim copyTask = Task.Run(moves)
    
            Try
                Dim waitResult As Integer = 0
                Do While (waitResult = 0) '全完了するまでループ
    
                    'ディレクトリコピーか全完了のどちらかになるまで待機
                    waitResult = System.Threading.WaitHandle.WaitAny(waithandles, 60000)
                    If (waitResult = System.Threading.WaitHandle.WaitTimeout) Then
                        'タイムアウトしたらエラー
                        Throw New TimeoutException()
                    End If
    
                    moved.Reset() '次の通知を待てるように
    
                    'コピー済みの結果を取り出し
                    Dim copyed As PathPair()
                    SyncLock (queue)
                        copyed = queue.ToArray()
                        queue.Clear()
                    End SyncLock
    
    
                    For Each pp As PathPair In copyed
                        Yield pp 'コピー済みの結果を列挙として返す
                    Next
    
                Loop
            Catch
                canceller.Cancel() '何かエラーになったらキャンセル
                Throw
            Finally
                copyTask.Wait() 'タスクが停止するのを待つ
            End Try
            If (exMove IsNot Nothing) Then
                Throw exMove
            End If
        End Function
    
        ''' <summary>コピー元とコピー先のパスを順番に列挙</summary>
        ''' <param name="destDirectory">コピー先の基準ディレクトリのパス</param>
        ''' <param name="sourceDirectory">コピー元の基準ディレクトリのパス</param>
        ''' <param name="subDirectoryNames">コピーしたいサブディレクトリの一覧</param>
        Private Shared Iterator Function GetMovePaths(ByVal destDirectory As String, ByVal sourceDirectory As String, ByVal subDirectoryNames As IEnumerable(Of String)) As IEnumerable(Of PathPair)
            For Each subDirectoryName As String In subDirectoryNames
                Dim pp As New PathPair
                pp.Source = System.IO.Path.Combine(sourceDirectory, subDirectoryName)
                pp.Destination = System.IO.Path.Combine(destDirectory, subDirectoryName)
                pp.Temp = System.IO.Path.Combine(destDirectory, "Temp")
                Yield pp
            Next
        End Function
    
        ''' <summary>コピー元とコピー先の情報</summary>
        Public Class PathPair
            Public Source As String 'コピー元
            Public Destination As String 'コピー先
            Public Temp As String '一時保存場所
    
            ''' <summary>一時保存場所にコピー</summary>
            Public Sub CopyToTempFromSource()
                If (System.IO.Directory.Exists(Source)) Then
                    My.Computer.FileSystem.CopyDirectory(Source, Temp)
                ElseIf (System.IO.File.Exists(Source)) Then
                    My.Computer.FileSystem.CopyFile(Source, Temp)
                End If
            End Sub
    
            ''' <summary>一時保存場所からコピー先に移動</summary>
            Public Sub MoveToDestFromTemp()
                If (System.IO.Directory.Exists(Source)) Then
                    My.Computer.FileSystem.MoveDirectory(Temp, Destination)
                ElseIf (System.IO.File.Exists(Source)) Then
                    My.Computer.FileSystem.MoveFile(Temp, Destination)
                End If
            End Sub
    
            Public Overrides Function ToString() As String
                Return Source + " => " + Temp + "=>" + Destination
            End Function
        End Class
    
    End Class
    


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 Unripe01 2015年8月14日 4:35
    • 回答としてマーク takiru 2015年8月15日 11:29
    2015年8月13日 15:22
  • ありがとうございます。
    教えていただいたコードを読み解き、実現することができました!
    2015年8月15日 11:30