65.9K
CodeProject 正在变化。 阅读更多。
Home

多线程备份实用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (67投票s)

2006年10月31日

CPOL

12分钟阅读

viewsIcon

255061

downloadIcon

14334

多线程是迟早我们要面对的事情。这个相对简单的应用程序向您展示了如何使用两个线程来复制文件。它也是一个非常方便的 Windows 备份工具,具有镜像功能、批量模式功能、日志文件功能和帮助按钮!

Sample Image

引言

Backup 是一个实用程序,它使用一个线程显示正在复制的文件名,另一个线程同时计算文件和文件夹的数量来将文件从一个路径复制到另一个路径。这意味着不会浪费时间等待获取文件计数后再开始复制;它使用两个线程同时进行复制和计数。

Backup 应用程序也是一个方便的工具,可以快速有效地将大量文件从一个路径复制到另一个路径。它之所以快速,仅仅是因为它不会复制一个已存在于目标路径且未更改的文件(为什么还要复制一个已经存在且未更改的文件?)。因此,它非常适合重复备份,因为它只复制新文件或更新的文件。

另一个附加功能是 Backup 不会因为某个文件出现安全错误或其他任何类型的错误(当然,硬件故障除外)而停止复制。它只会记录错误并继续进行,直到完成。之后,您可以查看日志文件中的错误,其中会显示错误以及未能复制的文件。大多数故障是由于安全配置问题引起的。

背景

如果您只需要 Backup 工具,那么无需继续阅读,因为本文的其余部分是关于多线程或线程的技术讨论。下载链接位于此页面左上角。

那么,为什么要使用多线程呢?一个原因是,您可能想在窗体忙碌时按下窗体上的按钮,但由于窗体忙碌而无法操作。旧的 VB6 方法是使用 Timer,或者在代码处理过程中定期使用 Do Events。我怀疑 Do Events 在 VB.NET 中对此目的无效,尽管 Timer 可以解决问题,但还有其他原因需要熟悉多线程。

另一个原因是,随着现在双核/多核处理和硬件及操作系统级别的真正多任务处理的可用,多线程将很难避免,尤其是在性能至关重要且应用程序是处理器密集型的情况下。

好的,您已经决定要在 .NET 中进行多线程。您可以使用 BackgroundWorker(Windows 窗体控件),但我认为您应该从 System.Threading 命名空间开始,并直接使用 Thread 类。我个人认为 Thread 类更容易使用,并为您提供更多灵活性,尽管 BackgroundWorker 会减少一些繁琐的编程工作。

那么,什么是线程呢?简单来说,它相当于从您的主程序启动的另一个完全独立的程序。一旦启动了线程,主程序就不知道关于该线程(或线程们)的任何信息;它们实际上是各自独立的程序。因此,您可能想知道的第一件事是如何创建它们,如何在它们之间传递数据,如何从线程调用主应用程序,最后,主应用程序如何知道哪个线程调用了它?

查看下面的代码片段应该有助于回答这些问题。

在开始之前,最后一点:使用线程复制文件不一定是多线程的最佳应用,因为复制文件是 I/O 密集型而不是处理器密集型。但仍然是合理的用途,因为文件计数和文件复制可以异步而非同步进行,何必等待?而且,我认为 Backup 同时是一个简单而方便的工具。

Using the Code

这段代码的描述将不讨论复制文件的细节,而仅展示用于创建线程以及如何从主窗体应用程序中使用它们的相关源代码;展示线程的创建方式以及如何将参数从线程传递到主 Windows 窗体应用程序。

首先,快速看一下 Backup 窗体中导入的命名空间

Imports System.Threading
Imports System.IO
Imports System.Diagnostics.Process

导入了 System.Threading 命名空间,以便您为每个线程创建 Thread 类。System.IO 命名空间用于文件操作,而 System.Diagnostics.Process 用于通过记事本查看日志文件。

Backup 实用程序的下一个代码片段显示了在主 Windows 窗体中声明了一个名为 CopyFilesFileCopy

Imports System.Windows

Public Class Backup

    ' Declare the FileCopy class.
    ' This class will create 3 threads to copy, count and mirror files
    ' and raise events for each, so the events must be handled
    ' to update the form with status data.
    Dim WithEvents CopyFiles As FileCopy

    Private Sub StartCopy_Click(ByVal sender As System.Object, e As System.EventArgs) _
        Handles StartCopy.Click

        ' Create the FileCopy class which will initiate the threads
        CopyFiles = New FileCopy

        ' Initiate the copy, count and mirror threads from the FileCopy class
        CopyFiles.StartCopy()

    End Sub
    .
    . remaining forms code
    .
End Class

按下 Backup 窗体上的 Go 按钮将启动 StartCopy_Click 子程序。该子程序将 FileCopy 类实例化为 CopyFiles ,然后使用 CopyFiles.StartCopy() 方法开始复制。

到目前为止,我们在主 Windows 窗体中所做的只是声明一个类,实例化它,然后执行 StartCopy() 方法。那么,为什么我们要创建一个类呢?这仅仅是好的 OOD 实践吗?

那么,声明线程并启动它们的代码在哪里呢?是的,我在这里试图强调的一个非常重要的观点是,如果您想使用线程,最好创建一个类,并使用该类来启动线程。这是因为当类启动的线程需要与父线程通信时,该类可以引发事件。

好了,现在我们来看看 FileCopy 类,看看它是如何启动线程的

Imports System.IO

Public Class FileCopy

    ' Declares the variables you will use to hold your thread objects.
    Public CopyThread As System.Threading.Thread
    Public CountThread As System.Threading.Thread
    Public MirrorThread As System.Threading.Thread

    Public Sub StartCopy()

        ' Sets the copy and count threads using the AddressOf the subroutine where
        ' the thread will start.
        CopyThread = New System.Threading.Thread(AddressOf Copy)
        CopyThread.IsBackground = True
        CopyThread.Name = "Copy"
        CopyThread.Start()

        CountThread = New System.Threading.Thread(AddressOf Count)
        CountThread.IsBackground = True
        CountThread.Name = "Count"
        CountThread.Start()

    End Sub
    .
    . code for the rest of the class
    .
End Class

很简单吧?声明线程,然后有一个方法来启动它们。到目前为止,一切顺利。但现在我们已经启动了 CopyThread CountThread,我们如何让它们通过传递参数与 Backup 窗体“交谈”呢? Backup 窗体将如何知道哪个线程在做什么?考虑到线程在自己的地址空间中运行,并且彼此之间不知道,这些都是合理的问题。

如果您注意到在实例化线程时使用了 AddressOf 运算符,您会看到每个线程被分配给了 FileCopy 类中一个名为 Copy Count private 子程序。您真正需要做的就是让您的代码在这些子程序中引发事件,每当它们想要与主父线程(父线程是 Backup Windows 窗体控件/类)通信时。

让我们快速看一下展开的 FileCopy 类,看看它是如何完成的

Imports System.IO

Public Class FileCopy

    ' Declare the events that will be raised by each thread
    Public Event CopyStatus(ByVal sender As Object, ByVal e As BackupEventArgs)
    Public Event CountStatus(ByVal sender As Object, ByVal e As BackupEventArgs)
    Public Event MirrorStatus(ByVal sender As Object, ByVal e As BackupEventArgs)

    ' Declares the variables you will use to hold your thread objects.
    Public CopyThread As System.Threading.Thread
    Public CountThread As System.Threading.Thread
    Public MirrorThread As System.Threading.Thread

    ' Class variables' Class variables
    Private _filePath As String
    Private _fileSize As String
    Private _copiedFolders As Long
    Private _copiedFiles As Long
    Private _countedFolders As Long
    Private _countedFiles As Long
    Private _mirroredFolders As Long
    Private _mirroredFiles As Long
    .
    . even more class variables but we will just show the relevant ones.
    .

    Public Sub StartCopy()

        ' Sets the copy and count threads using the AddressOf the subroutine where
        ' the thread will start.
        CopyThread = New System.Threading.Thread(AddressOf Copy)
        CopyThread.IsBackground = True
        CopyThread.Name = "Copy"
        CopyThread.Start()

        CountThread = New System.Threading.Thread(AddressOf Count)
        CountThread.IsBackground = True
        CountThread.Name = "Count"
        CountThread.Start()

    End Sub

     Private Sub Copy()
        .
        . this is a program loop with logic to copy files
        .
        Loop to Copy Files

            Copy a file here

            ' Raise the copy status event at the end of the program loop
            RaiseEvent CopyStatus(Me, New BackupEventArgs_
                    ("", 0, _copiedFiles, _copiedFolders))
            Threading.Thread.Sleep(1)
        End Loop

      End Sub

      Private Sub Count()
        .
        . this is a program loop with logic to count files
        .
        Loop to Count Files

            Count a file here

            ' Raise the count status event at the end of the program loop
            RaiseEvent CountStatus(Me, New BackupEventArgs_
                    ("", 0, _countedFiles, _countedFolders))
            Threading.Thread.Sleep(1)
        End Loop

    End Sub
    .
    . code for the rest of the class
    .
End Class

好了,这就是了。一个基本的类声明,它启动线程,并在每次需要与主父线程(我们知道是 Backup Windows 窗体控件)通信时引发事件。

注意 Threading.Thread.Sleep 的使用。这是为了让父线程有时间完成它的事情,例如刷新窗体显示和响应按钮点击。到目前为止还不错,但我们还没有完全完成。

现在我们需要查看主父线程(Backup Windows 窗体控件),看看传递这些参数所需的代码。

首先,请注意上面代码片段中事件声明中的 e 参数。e 参数是一个自定义类 BackupEventArgs 的变量,该类有四个参数,分别称为 FilePathFileSizeFileCount FolderCount。这个自定义类继承自 EventArgs ,并且只是声明事件参数的标准方法。您也可以不创建包含参数的自定义类,而是为每个参数声明一个单独的变量。要了解 BackupEventArgs 类是什么,请参阅源代码,因为如何为事件声明参数超出了本文的范围。

现在,只需考虑 e 变量包含我们想要传递给主父线程并由主父线程处理的参数(FilePathFileSizeFileCount FolderCount)。

所以,让我们重新关注父线程,看看处理事件的代码

Imports System.Windows

Public Class Backup

    ' Declare the FileCopy class. This class will create 3 threads to copy, 
    ' count and mirror files
    ' and raise events for each, so the events must be handled to update
    ' the form with status data.
    Dim WithEvents CopyFiles As FileCopy

    ' Declare delegate handlers for the copy and count events
    Public Delegate Sub CopyHandler(ByVal FilePath As String, _
            ByVal FileSize As Long, ByVal FileCount As Long)
    Public Delegate Sub CountHandler(ByVal FileCount As Long, ByVal FolderCount As Long)
    .
    . more declarations
    .
    Private Sub StartCopy_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles StartCopy.Click

        ' Create the FileCopy class which will initiate the threads
        CopyFiles = New FileCopy

        ' Initiate the copy, count and mirror threads from the FileCopy class
        CopyFiles.StartCopy()

    End Sub

     Private Sub CopyStatus(ByVal FilePath As String, ByVal FileSize As Long, _
            ByVal FileCount As Long)
        .
        . some code to update the forms display
        .
    End Sub

    Private Sub CopyFiles_CopyStatus(ByVal sender As Object, _
            ByVal e As BackupEventArgs) Handles CopyFiles.CopyStatus
        ' BeginInvoke causes asynchronous execution to begin at the address
        ' specified by the delegate. Simply put, it transfers execution of
        ' this method back to the main thread. Any parameters required by
        ' the method contained at the delegate are wrapped in an object and
        ' passed.

        Me.BeginInvoke(New CopyHandler(AddressOf CopyStatus), _
                New Object() {e.FilePath, e.FileSize, e.FileCount})

    End Sub
    .
    . remaining forms code
    .
End Class

上面的代码片段有一个 CopyFiles_CopyStatus() 事件,该事件在使用 WithEvents 声明 CopyFiles 变量时被添加到主窗体(父线程)上。它使用窗体控件的 Me.BeginInvoke 方法将参数从 CopyThread 传递到父窗体线程。所有 Windows 控件,包括窗体控件,都有一个 BeginInvoke 方法(用于异步执行)和一个 Invoke 方法(用于同步执行),它们允许您执行指定的 delegate

在上面的代码中,指定的委托是 CopyHandler ,它使用 AddressOf 运算符分配给了 CopyStatus 子程序。当 CopyFiles_CopyStatus() 事件发生时,它通过委托异步执行 CopyStatus() 子程序,将参数传递回主父线程(Backup Windows 窗体控件)。

如果我让您感到困惑,请考虑一下,尽管看起来 CopyFiles_CopyStatus() 事件发生在 Backup Windows 窗体控件/类中,但实际上并非如此。它实际上发生在 CopyThread 线程中,这是一个独立的程序代码,它不知道父线程。这是一个很难理解的概念,因为事件是在 Windows 窗体控件类中声明的,但它就是这样工作的。

继续前进(对于仍然跟着我的人来说),有几个要点可以帮助那些刚接触多线程的新手。如果我们修改下面的 FileCopy 类代码片段以包含线程上可用的 Join 方法,我们可以让一个线程在代码的某个点等待另一个线程。

我遇到的一个问题(实际上是一个挑战)是,我需要让复制线程在它(复制线程)自然终止(即完成文件复制)之前等待计数线程完成。您可能期望计数线程首先完成,但当没有文件可复制时,有时复制线程会先完成!

使用 join 方法可以帮助我处理这种情况。所以,如果一个线程依赖于另一个线程的完成并且必须等待它,请记住 Join 方法。

Imports System.IO

Public Class FileCopy

    ' Declare the events that will be raised by each thread
    Public Event CopyStatus(ByVal sender As Object, ByVal e As BackupEventArgs)
    Public Event CountStatus(ByVal sender As Object, ByVal e As BackupEventArgs)
    Public Event MirrorStatus(ByVal sender As Object, ByVal e As BackupEventArgs)

    ' Declares the variables you will use to hold your thread objects.
    Public CopyThread As System.Threading.Thread
    Public CountThread As System.Threading.Thread
    Public MirrorThread As System.Threading.Thread

    ' Class variables' Class variables
    Private _filePath As String
    Private _fileSize As String
    Private _copiedFolders As Long
    Private _copiedFiles As Long
    Private _countedFolders As Long
    Private _countedFiles As Long
    Private _mirroredFolders As Long
    Private _mirroredFiles As Long
    .
    . even more class variables but we will just show the relevant ones.
    .

    Public Sub StartCopy()

        ' Sets the copy and count threads using the AddressOf the subroutine where
        ' the thread will start.
        CopyThread = New System.Threading.Thread(AddressOf Copy)
        CopyThread.IsBackground = True
        CopyThread.Name = "Copy"
        CopyThread.Start()

        CountThread = New System.Threading.Thread(AddressOf Count)
        CountThread.IsBackground = True
        CountThread.Name = "Count"

        CountThread.Start()

    End Sub

    Private Sub Copy()
       .
       . this is a program loop with logic to copy files
       .
       Loop to Copy Files

           Copy a file here

           ' Raise the copy status event at the end of the program loop
           RaiseEvent CopyStatus(Me, New BackupEventArgs_
               ("", 0, _copiedFiles, _copiedFolders))
           Threading.Thread.Sleep(1)
       End Loop

       ' After all files have been copied, cause this CopyThread to wait 
       ' until CountThread has finished
       If CountThread.IsAlive Then CountThread.Join()

     End Sub

     Private Sub Count()
       .
       . this is a program loop with logic to count files
       .
       Loop to Count Files

           Count a file here

           ' Raise the count status event at the end of the program loop
           RaiseEvent CountStatus(Me, New BackupEventArgs_
                   ("", 0, _countedFiles, _countedFolders))
           Threading.Thread.Sleep(1)
       End Loop

   End Sub
   .
   . code for the rest of the class
   .
End Class

最后一点。如何终止线程?使用 Abort 方法被一些人诟病,尽管有一些共识认为在中止整个应用程序时可以使用它,一些纯粹主义者认为出于技术原因(关于在执行过程中如何分配和取消分配资源),它应该被弃用。换句话说,当线程正在处理某事时,您应该信号线程自行终止,而不是中止它。

下面的代码片段显示了 FileCopy 类中的一个名为 StopThreads() 的方法,该方法只是将一个名为 _stopNow boolean 类变量设置为 True。当从父线程(Backup)执行 StopThreads() 方法时,所有线程只需要检查 _stopNow 并退出处理循环以优雅地终止线程,如上面(我们已经看到它被分配给复制线程)的 Copy 子程序代码片段所示。

Public Class FileCopy
    .
    . class code
    .
    Public Sub StopThreads()

        _stopNow = True

    End Sub
    .
    . more class code
    .
    .
    Private Sub Copy()

        While Loop to copy files

            If _stopNow Then
                Exit While
            End If

            copy a file
            raise an event to notify parent thread

        End While

    End Sub
    .
    .
    .
End Class

关注点

上面的大部分代码是通过复制和改编 MSDN 的代码开发的。所以,如果您真的想深入了解多线程的细节,可以在 msdn.microsoft.com 上找到一些非常好的、解释得很清楚的示例。

我必须说,MSDN 终于有所改进,令人印象深刻。那里的一些文章甚至详细讨论了最佳实践,并提供了非常好的演练。所以,非常感谢 MSDN 的编码专家。

最后,在告别并感谢那些能够读到这里的人时,请为我的文章投票,慷慨一点,您也将得到回报。

历史

2007 年 10 月

上传了版本 1.0.7,其中包含错误修复,并添加了批量模式,用于在没有窗体的情况下运行和处理命令行参数。

2008 年 6 月

上传了版本 2.0.0,这是源代码和本文的完整修订版,还添加了帮助文件和镜像复制功能。

2008 年 7 月

上传了版本 2.1.0,它有一个新的迷你状态栏,并修复了版本 2.0.0 中可能出现的一些故障。请升级。

另请注意,到目前为止 Backup 尚未在 Windows Vista 上进行测试。您可以尝试在 Vista 上运行它,如果出现问题,请定期回来查看,因为在接下来的几个月中应该会有另一个版本在 Vista 上进行测试。

升级到最新版本

如果从以前的版本升级,请先卸载以前的版本。从“控制面板”(“开始”/“控制面板”)中,双击“添加/删除程序”图标,然后从列表中选择 Backup 来执行此操作。

使用最新的源代码

Backup 2.0 版的源代码是使用 Visual Studio 2008 和 .NET Framework 3.5 创建的,因此旧版本的 Visual Studio 可能会出现兼容性问题。

如何安装

  1. 在此页面左上角查找“Download BackUp”链接。单击该链接进行下载,然后解压缩。
  2. 您将看到一个“setup.exe”文件和一个“setup.msi”文件。如果您要在 Windows 8 或更高版本上安装,请右键单击“setup.exe”文件,选择“属性”,然后按“兼容性”选项卡。选择“Windows Vista (Service Pack 2)”并按“确定”按钮。如果您使用的是 Windows 7 或更早版本,请跳过此步骤。
  3. 双击“setup.exe”进行安装,但请选择自己的安装文件夹。
  4. 最好选择自己的安装文件夹,例如“c:\backup”,因为如果您安装到 c:\program files (x86) 文件夹,您将收到错误“访问路径 ‘C\Program files (x86)\moneysoft\backup\191006-050101-Log.txt’ 被拒绝”。您可以“以管理员身份运行”来解决此错误,或者向此文件夹添加写入权限,但安装到新位置(例如“c:\backup”)会更容易。
  5. 您应该会在桌面上看到 Backup 图标。
  6. 如果您在 Windows 8 或更高版本上运行,请同时按住“Shift”键并右键单击 Backup 图标,然后选择“以管理员身份运行”。如果您不“以管理员身份运行”,您将收到权限错误,因为出于安全原因,Windows 8 或更高版本将拒绝访问应用程序目录。

故障排除

错误消息

访问路径 ‘C\Program files (x86)\moneysoft\backup\191006-050101-Log.txt’ 被拒绝

原因

默认情况下,当您安装备份实用程序时,它将安装到“C\Program files (x86)”,除非您指定了其他文件夹。

解决方案

安装时,请选择其他安装文件夹,例如 c:\backup

如果您已经安装,请先卸载,然后重新安装。

替代解决方案

以管理员身份运行。

替代解决方案

设置已安装文件夹的写入权限。

© . All rights reserved.