Skip to main content
 首页 » 编程设计

c#之为什么使用两个 ManualResetEvents 会导致死锁

2024年02月06日34jpfss

我正在使用 Starksoft.Net.Ftp 执行异步上传操作。

看起来像这样:

    public void UploadFile(string filePath, string packageVersion) 
    { 
        _uploadFtpClient= new FtpClient(Host, Port, FtpSecurityProtocol.None) 
        { 
            DataTransferMode = UsePassiveMode ? TransferMode.Passive : TransferMode.Active, 
            FileTransferType = TransferType.Binary, 
        }; 
        _uploadFtpClient.TransferProgress += TransferProgressChangedEventHandler; 
        _uploadFtpClient.PutFileAsyncCompleted += UploadFinished; 
        _uploadFtpClient.Open(Username, Password); 
        _uploadFtpClient.ChangeDirectoryMultiPath(Directory); 
        _uploadFtpClient.MakeDirectory(newDirectory); 
        _uploadFtpClient.ChangeDirectory(newDirectory); 
        _uploadFtpClient.PutFileAsync(filePath, FileAction.Create); 
        _uploadResetEvent.WaitOne(); 
        _uploadFtpClient.Close(); 
    } 
 
    private void UploadFinished(object sender, PutFileAsyncCompletedEventArgs e) 
    { 
        if (e.Error != null) 
        { 
            if (e.Error.InnerException != null) 
                UploadException = e.Error.InnerException; 
        } 
        _uploadResetEvent.Set(); 
    } 

如您所见,其中有一个 ManualResetEvent,它在类的顶部被声明为私有(private)变量:

private ManualResetEvent _uploadResetEvent = new ManualResetEvent(false); 

嗯,感觉只是应该等待上传完成,但是它必须是异步的来报告进度,仅此而已。

现在,这一切正常。 如果需要,我有第二种方法可以取消上传。

public void Cancel() 
{ 
    _uploadFtpClient.CancelAsync(); 
} 

当上传被取消时,服务器上的一个目录也必须被删除。 我也有一个方法:

    public void DeleteDirectory(string directoryName) 
    { 
        _uploadResetEvent.Set(); // As the finished event of the upload is not called when cancelling, I need to set the ResetEvent manually here. 
 
        if (!_hasAlreadyFixedStrings) 
            FixProperties(); 
 
        var directoryEmptyingClient = new FtpClient(Host, Port, FtpSecurityProtocol.None) 
        { 
            DataTransferMode = UsePassiveMode ? TransferMode.Passive : TransferMode.Active, 
            FileTransferType = TransferType.Binary 
        }; 
        directoryEmptyingClient.Open(Username, Password); 
        directoryEmptyingClient.ChangeDirectoryMultiPath(String.Format("/{0}/{1}", Directory, directoryName)); 
        directoryEmptyingClient.GetDirListAsyncCompleted += DirectoryListingFinished; 
        directoryEmptyingClient.GetDirListAsync(); 
        _directoryFilesListingResetEvent.WaitOne(); // Deadlock appears here 
 
        if (_directoryCollection != null) 
        { 
            foreach (FtpItem directoryItem in _directoryCollection) 
            { 
                directoryEmptyingClient.DeleteFile(directoryItem.Name); 
            } 
        } 
        directoryEmptyingClient.Close(); 
 
        var directoryDeletingClient = new FtpClient(Host, Port, FtpSecurityProtocol.None) 
        { 
            DataTransferMode = UsePassiveMode ? TransferMode.Passive : TransferMode.Active, 
            FileTransferType = TransferType.Binary 
        }; 
        directoryDeletingClient.Open(Username, Password); 
        directoryDeletingClient.ChangeDirectoryMultiPath(Directory); 
        directoryDeletingClient.DeleteDirectory(directoryName); 
        directoryDeletingClient.Close(); 
    } 
 
    private void DirectoryListingFinished(object sender, GetDirListAsyncCompletedEventArgs e) 
    { 
        _directoryCollection = e.DirectoryListingResult; 
        _directoryFilesListingResetEvent.Set(); 
    } 

由于取消时没有调用上传完成事件,我需要在DeleteDirectory方法中手动设置ResetEvent。

现在,我在这里做什么:我首先列出目录中的所有文件以便删除它们,因为无法删除已填充的文件夹。

此方法 GetDirListAsync 也是异步的,这意味着我需要另一个 ManualResetEvent,因为我不希望表单卡住。

此 ResetEvent 是 _directoryFilesListingResetEvent。它的声明类似于上面的 _uploadResetEvent。

现在,问题是,它转到 _directoryFilesListingResetEvent 的 WaitOne 调用,然后卡住了。出现死锁,表单卡住。 (我在代码中也标出了)

这是为什么呢? 我试图移动 _uploadResetEvent.Set() 的调用,但它没有改变。 有人看到问题了吗?

当我尝试在没有任何上传的情况下单独调用 DeleteDirectory 方法时,它也能正常工作。 我认为问题在于两个 ResetEvents 使用相同的资源或其他东西并且它们自身重叠,我不知道。

感谢您的帮助。

请您参考如下方法:

你没有正确使用这个库。您添加的 MRE 会导致死锁。从 _uploadResetEvent.WaitOne() 开始,阻塞了 UI 线程。这通常是非法的,CLR 确保您的 UI 不会通过自己发送消息循环而完全死机。这使它看起来 就像它还活着一样,例如,它仍在重绘。大致等同于 DoEvents(),尽管没有那么危险。

但它最大的问题是它不允许您的 PutFileAsyncCompleted 事件处理程序运行,底层异步工作程序是一个普通的 BackgroundWorker。它在启动它的同一个线程上触发它的事件,这非常好。但在 UI 线程空闲之前,它无法调用其 RunWorkerCompleted 事件处理程序。这不太好,线程卡在 WaitOne() 调用中。对于您现在正在调试的内容完全相同,您的 GetDirListAsyncCompleted 事件处理程序由于相同的原因无法运行。所以它只是停在那里而无法取得进展。

因此完全消除 _uploadResetEvent,转而依赖您的 UploadFinished() 方法。您可以从 e.Cancelled 属性中查明它是否被取消。只有然后您才启动删除目录的代码。遵循相同的模式,使用相应的 XxxAsyncCompleted 事件来决定下一步做什么。根本不需要 MRE。