トップ回答者
スレッドの実装方法がよく分かりません

質問
-
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
回答
-
コピー処理を止めたくないなら、データベース処理とコピー処理とを別スレッドにして、ディレクトリコピーが済んだらデータベーススレッドに完了通知し、通知を受けたら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!)
すべての返信
-
コピー処理を止めたくないなら、データベース処理とコピー処理とを別スレッドにして、ディレクトリコピーが済んだらデータベーススレッドに完了通知し、通知を受けたら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!)