VB.NET异步大文件复制改造:实现暂停、停止与进度统计
改造VB.NET批量文件复制为异步模式(支持暂停/停止与进度追踪)
我来帮你把这段同步的批量文件复制代码改造成支持异步执行、暂停/停止功能和实时进度追踪的版本。核心思路是利用VB.NET的Async/Await异步模式、取消令牌机制和安全的UI进度更新方案,彻底解决原代码阻塞UI、无法中断的问题。
完整修改后的代码
首先我们需要添加类级别的控制字段,然后重构核心逻辑为异步模式:
' 异步操作控制字段 Private _cts As CancellationTokenSource Private _pauseEvent As ManualResetEventSlim Private _progress As IProgress(Of ProgressUpdate) ' 自定义进度更新实体,用于传递进度数据 Private Class ProgressUpdate Public Property ProcessedRecords As Integer Public Property CopiedImages As Integer Public Property TotalRecords As Integer End Class ' 异步版本的主操作按钮事件 Private Async Sub btnMoverImagenes_Click(sender As Object, e As EventArgs) Handles btnMoverImagenes.Click ' 初始化异步控制对象 _cts = New CancellationTokenSource() _pauseEvent = New ManualResetEventSlim(True) ' 初始状态为运行中 _progress = New Progress(Of ProgressUpdate)(AddressOf UpdateProgressUI) Dim contadorIMGCopiadas As Integer = 0 Dim contadorRegistros As Integer = 0 Dim contadorArchivosSinImagen As Integer = 0 Dim h1 As Date = System.DateTime.Now Dim fechaDesde As String Dim fechaHasta As String ' 日期格式化逻辑保持不变 dtpDesde.Format = DateTimePickerFormat.Custom dtpDesde.CustomFormat = "yyyyMMdd" fechaDesde = dtpDesde.Text dtpHasta.Format = DateTimePickerFormat.Custom dtpHasta.CustomFormat = "yyyyMMdd" fechaHasta = dtpHasta.Text Try ' 获取数据(如果EjecutarXML支持异步,建议改为Async版本) Dim dsImagenes = EjecutarXML(conexionDat, fechaDesde, fechaHasta) If dsImagenes.Tables(dsImagenes.Tables.Count - 1).Rows(0).Item(3) <= 0 Then MsgBox("sin datos para la fecha seleccionada") Return End If Dim dsregistros = dsImagenes.Tables(0).Rows.Count ' 初始化进度条 ProgressBar1.Minimum = 0 ProgressBar1.Maximum = dsregistros ProgressBar1.Value = 0 ProgressBar2.Minimum = 0 ProgressBar2.Maximum = dsregistros ProgressBar2.Value = 0 Label5.Text = $"0 de {dsregistros}" Label6.Text = $"0 de {dsregistros}" Log("******* SE INICIA CARGA *******") If dsregistros > 0 Then For Each row As DataRow In dsImagenes.Tables(0).Rows ' 检查是否被用户取消 If _cts.Token.IsCancellationRequested Then Log("******* PROCESO CANCELADO POR EL USUARIO *******") Exit For End If ' 检查是否处于暂停状态,暂停则等待直到继续或取消 _pauseEvent.Wait(_cts.Token) ' 提取行数据(和原逻辑一致) Dim archivo = row.Item(0).ToString() Dim carpetaProducto = row.Item(3).ToString() Dim rutaOrigen = row.Item(1).ToString() Dim idregistro = Convert.ToInt32(row.Item(2)) Dim agno = row.Item(4).ToString() Dim mes = row.Item(5).ToString() Dim dia = row.Item(6).ToString() Dim secuencia = row.Item(7).ToString() Dim fechaCarga = row.Item(9).ToString() Dim imagenMulti = row.Item(8).ToString() Dim nombreImagenOrigen = row.Item(10).ToString() Dim nuevaRutaImagenMulti = $"{carpetaProducto}\{agno}\{mes}\{dia}\{secuencia}\{nombreImagenOrigen}" Dim RutaDestinoOp = $"{RutaDestinoNasOp}\{carpetaProducto}\{agno}\{mes}\{dia}\{secuencia}" Dim RutaDestinoHis = $"{RutaDestinoNasHis}\{carpetaProducto}\{agno}\{mes}\{dia}\{secuencia}" Dim DestinoArchivoOrigen = $"{RutaDestinoNasOp}\{carpetaProducto}\{agno}\{mes}\{dia}\{secuencia}\{nombreImagenOrigen}" Dim DestinoArchivoHistorico = $"{RutaDestinoNasHis}\{carpetaProducto}\{agno}\{mes}\{dia}\{secuencia}\{archivo}" Log("------------------------------------------------------------------------------") Log($"******* Inicio copia archivo IDREGISTRO: {idregistro} *******") Try ' 核心业务逻辑,替换为异步方法调用 If ValidaExisteImagenOrigen(rutaOrigen) Then If ValidarRutaOrigen(rutaOrigen) Then If Await CargaNasHistoricoAsync(rutaOrigen, RutaDestinoHis, DestinoArchivoHistorico, conexionDat, idregistro, _cts.Token) Then If Await CargaNasOperativoAsync(rutaOrigen, RutaDestinoOp, DestinoArchivoOrigen, conexionDat, idregistro, nuevaRutaImagenMulti, _cts.Token) Then contadorIMGCopiadas += 1 End If End If Else If ValidaImagenHistorico(DestinoArchivoHistorico) Then If Await CargaNasHistoricoAsync(rutaOrigen, RutaDestinoHis, DestinoArchivoHistorico, conexionDat, idregistro, _cts.Token) Then contadorIMGCopiadas += 1 Else Log($"******* TODO OK, Registro id: {idregistro}. no se realiza copia de imagen *******") contadorIMGCopiadas += 1 End If End If End If Else Log($"******* ALERTA: Registro id: {idregistro} NO EXISTE *******") contadorArchivosSinImagen += 1 End If Catch ex As OperationCanceledException Log($"******* Copia de archivo {idregistro} cancelada *******") Exit For Catch ex As Exception Log($"******* Error al procesar archivo {idregistro}: {ex.Message} *******") End Try contadorRegistros += 1 Log($"******* Finaliza copia archivo IDREGISTRO: {idregistro} *******") ' 报告最新进度 _progress.Report(New ProgressUpdate With { .ProcessedRecords = contadorRegistros, .CopiedImages = contadorIMGCopiadas, .TotalRecords = dsregistros }) Next ' 完成统计 Dim h2 As Date = System.DateTime.Now Dim tiempoCarga = (h2 - h1).ToString() MsgBox("Proceso Terminado, revisar log para ver el detalle del proceso completo") Log("------------------------------------------------------------------------------") Log($"******* Copia Finalizada, IMAGENES COPIADAS: {contadorIMGCopiadas} DE {dsregistros} *******") Log($"******* REGISTROS SIN IMAGENES: {contadorArchivosSinImagen} *******") Log($"******* TIEMPO DE EJECUCION: {tiempoCarga} *******") End If Catch ex As Exception MsgBox($"ocurrio un error al buscar los datos: {ex.Message}") Log($"******* ERROR GENERAL: {ex.Message} *******") Finally ' 清理资源,避免内存泄漏 _cts?.Dispose() _pauseEvent?.Dispose() End Try End Sub ' 安全更新UI进度的方法(自动在UI线程执行) Private Sub UpdateProgressUI(update As ProgressUpdate) ProgressBar1.Value = update.ProcessedRecords ProgressBar2.Value = update.CopiedImages Label5.Text = $"{update.ProcessedRecords} de {update.TotalRecords}" Label6.Text = $"{update.CopiedImages} de {update.TotalRecords}" End Sub ' 异步版本的历史文件复制方法 Private Async Function CargaNasHistoricoAsync(rutaOrigen As String, rutaDestino As String, destinoArchivo As String, conexionDat As String, idregistro As Integer, token As CancellationToken) As Task(Of Boolean) Try ' 异步创建目录 Await Task.Run(Sub() If Not Directory.Exists(rutaDestino) Then Directory.CreateDirectory(rutaDestino) End If End Sub, token) ' 异步复制文件 Await File.CopyAsync(rutaOrigen, destinoArchivo, True, token) ' 替换为你的异步数据库操作(示例) ' Using conn As New SqlConnection(conexionDat) ' Await conn.OpenAsync(token) ' Using cmd As New SqlCommand("UPDATE ...", conn) ' cmd.Parameters.AddWithValue("@Id", idregistro) ' Await cmd.ExecuteNonQueryAsync(token) ' End Using ' End Using Return True Catch ex As OperationCanceledException Throw ' 向上抛出取消异常,让上层处理 Catch ex As Exception Log($"******* Error en CargaNasHistoricoAsync para {idregistro}: {ex.Message} *******") Return False End Try End Function ' 异步版本的操作文件复制方法 Private Async Function CargaNasOperativoAsync(rutaOrigen As String, rutaDestino As String, destinoArchivo As String, conexionDat As String, idregistro As Integer, nuevaRutaImagenMulti As String, token As CancellationToken) As Task(Of Boolean) Try Await Task.Run(Sub() If Not Directory.Exists(rutaDestino) Then Directory.CreateDirectory(rutaDestino) End If End Sub, token) Await File.CopyAsync(rutaOrigen, destinoArchivo, True, token) ' 这里添加原方法中的其他业务逻辑,全部改为异步实现 Return True Catch ex As OperationCanceledException Throw Catch ex As Exception Log($"******* Error en CargaNasOperativoAsync para {idregistro}: {ex.Message} *******") Return False End Try End Function ' 停止按钮事件 Private Sub btnStop_Click(sender As Object, e As EventArgs) Handles btnStop.Click _cts?.Cancel() _pauseEvent?.Set() ' 唤醒暂停的线程以便取消 MsgBox("Proceso cancelado") End Sub ' 暂停/继续按钮事件 Private Sub btnPause_Click(sender As Object, e As EventArgs) Handles btnPause.Click If _pauseEvent Is Nothing Then Return If _pauseEvent.IsSet Then _pauseEvent.Reset() ' 进入暂停状态 btnPause.Text = "继续" Else _pauseEvent.Set() ' 恢复运行 btnPause.Text = "暂停" End If End Sub
关键改造要点解释
Async/Await异步模式
- 将按钮事件改为
Async Sub,确保UI线程不会被阻塞,用户可以随时点击暂停/停止按钮 - 所有耗时操作(文件复制、数据库操作)都封装为异步方法,用
Await等待执行完成
- 将按钮事件改为
取消令牌(CancellationTokenSource)
- 用于触发停止操作,所有异步方法都接受
CancellationToken参数,实时检查是否被取消 - 取消时会抛出
OperationCanceledException,我们可以捕获并终止流程
- 用于触发停止操作,所有异步方法都接受
暂停控制(ManualResetEventSlim)
- 比普通布尔标志+Sleep更高效的暂停实现,暂停时调用
Reset()让线程等待,继续时调用Set()唤醒线程 - 等待过程中仍会响应取消操作,避免暂停后无法停止的问题
- 比普通布尔标志+Sleep更高效的暂停实现,暂停时调用
安全的UI进度更新
- 使用
IProgress<T>接口配合Progress<T>类,自动在UI线程执行进度更新回调,避免跨线程操作控件的异常 - 自定义
ProgressUpdate类传递多维度进度数据(已处理记录数、已复制文件数等)
- 使用
注意事项
- 如果
EjecutarXML方法是耗时操作,建议也改为异步版本(EjecutarXMLAsync),避免初始化阶段阻塞UI - 数据库操作必须使用异步API(如
OpenAsync、ExecuteNonQueryAsync),否则仍会阻塞线程 - 日志写入操作如果是文件IO,建议改为
File.AppendAllTextAsync异步方法,提升整体性能
内容的提问来源于stack exchange,提问作者Throkar
相关产品推荐
相关产品推荐

