为什么使用两个ManualResetEvent导致死锁?

时间:2014-10-11 18:25:58

标签: c# .net deadlock manualresetevent

我正在使用 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 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 -method而不进行任何上传时,它也可以正常工作。 我认为问题是两个ResetEvents都使用相同的资源或其他东西并重叠自己,我不知道。

感谢您的帮助。

2 个答案:

答案 0 :(得分:3)

您没有正确使用此库。您添加的MRE会导致死锁。这开始于_uploadResetEvent.WaitOne(),阻止了UI线程。这通常是非法的,CLR通过泵送消息循环来确保您的UI不会完全死亡。这使它看起来就像它还活着一样,它仍然重新粉刷。粗略相当于DoEvents(),尽管不是那么危险。

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

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

答案 1 :(得分:1)

查看the sourceFtpClient使用BackgroundWorker执行异步操作。这意味着它的完成事件将被发布到创建工作人员时设置的SynchronizationContext。我打赌完成CancelAsync会将您推回到UI线程,当您在目录列表重置事件上调用WaitOne时,该线程会阻塞。 GetDirListAsyncCompleted事件被发布到UI消息循环,但由于UI线程被阻止,它将永远不会运行,并且永远不会设置重置事件。

BOOM!死锁。