Почему работа с двумя ManualResetEvents вызывает здесь взаимоблокировку?

Я выполняю асинхронную операцию загрузки с помощью 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();
    }

Поскольку событие завершения загрузки не вызывается при отмене, мне нужно установить ResetEvent вручную в методе DeleteDirectory.

Теперь, что я здесь делаю: я сначала перечисляю все файлы в каталоге, чтобы удалить их, так как заполненная папка не может быть удалена.

Этот метод GetDirListAsync также является асинхронным, что означает, что мне нужно еще одно ManualResetEvent, так как я не хочу, чтобы форма зависала.

Это ResetEvent — _directoryFilesListingResetEvent. Оно объявляется аналогично _uploadResetEvent выше.

Теперь проблема в том, что он переходит к вызову WaitOne события _directoryFilesListingResetEvent, а затем зависает. Появляется взаимная блокировка, и форма зависает. (я также отметил это в коде)

Почему это? Я попытался переместить вызов _uploadResetEvent.Set(), но ничего не изменилось. Кто-нибудь видит проблему?

Когда я пытаюсь вызвать метод DeleteDirectory без какой-либо загрузки, он также работает. Я думаю, проблема в том, что оба ResetEvents используют один и тот же ресурс или что-то в этом роде и перекрывают друг друга, я не знаю.

Спасибо за вашу помощь.


person Dominic B.    schedule 11.10.2014    source источник
comment
Может ли загрузка заблокировать каталог, который вы пытаетесь удалить?   -  person jaywayco    schedule 11.10.2014
comment
Хороший вопрос, это может быть, поскольку я создаю его там, но не должен ли CancelAsync управлять этим?   -  person Dominic B.    schedule 11.10.2014
comment
Запускает ли cancelasync событие uploadfinished? Если нет, то клиент загрузки не будет закрыт и может зависнуть на ресурсах.   -  person jaywayco    schedule 11.10.2014
comment
Нет, он не срабатывает, но проблема сохраняется, так как взаимоблокировка все еще появляется, даже при принудительном закрытии соединения.   -  person Dominic B.    schedule 11.10.2014
comment
Как я понял из обсуждения, следующий код в private void DirectoryListingFinished _uploadResetEvent.Set(); выполнит эту работу, так как здесь проблема, кажется, @ _uploadResetEvent.WaitOne();, а не в другом WaitOne   -  person Mrinal Kamboj    schedule 11.10.2014
comment
Вставил _uploadResetEvent.Set(); есть, но тупик все еще там.   -  person Dominic B.    schedule 11.10.2014
comment
@Trade Проблема решена?   -  person Alireza    schedule 15.10.2014
comment
@Alireza Спасибо за ваш отзыв. Нет, проблема осталась, и тупик просто не хочет сдаваться...   -  person Dominic B.    schedule 16.10.2014
comment
Просто хотел проверить. Вечером попробую :)   -  person Alireza    schedule 16.10.2014


Ответы (2)


Вы неправильно используете эту библиотеку. Добавленные вами MRE вызывают взаимоблокировку. Это началось с _uploadResetEvent.WaitOne(), блокирующего поток пользовательского интерфейса. Обычно это незаконно, CLR гарантирует, что ваш пользовательский интерфейс не станет полностью мертвым, запустив сам цикл сообщений. Это делает его выглядящим живым, например, он все еще перекрашивается. Грубый эквивалент DoEvents(), хотя и не такой опасный.

Но самая большая проблема с ним заключается в том, что он не позволит запустить обработчик событий PutFileAsyncCompleted, базовый асинхронный рабочий процесс — это простой BackgroundWorker. Он запускает свои события в том же потоке, который его запустил, что очень приятно. Но он не может вызвать свой обработчик событий RunWorkerCompleted до тех пор, пока поток пользовательского интерфейса не станет бездействующим. Что нехорошо, поток застрял в вызове WaitOne(). Точно такая же история с тем, что вы сейчас отлаживаете, ваш обработчик событий GetDirListAsyncCompleted не может работать по той же причине. Так что он просто зависает там, не имея возможности добиться прогресса.

Поэтому полностью исключите _uploadResetEvent, вместо этого полагайтесь на свой метод UploadFinished(). Узнать, было ли оно отменено, можно из свойства e.Cancelled. Только затем вы запускаете код для удаления каталога. Следуйте тому же шаблону, используя соответствующее событие XxxAsyncCompleted, чтобы решить, что делать дальше. МРЭ вообще не нужны.

person Hans Passant    schedule 17.10.2014
comment
Ах, хорошо, значит, мне нужно уделить внимание AsyncCompletedEventArgs. Я попробую, спасибо. - person Dominic B.; 18.10.2014
comment
Итак: я сохранил MRE для загрузки, потому что мне нужно дождаться завершения метода. Но теперь я перенес удаление в обработчик UploadFinished в запросе, где проверяется свойство Cancelled. Спасибо, теперь все работает! - person Dominic B.; 19.10.2014

Глядя на исходник, кажется, FtpClient использует BackgroundWorker для выполнения асинхронных операций. Это означает, что его событие завершения будет отправлено на любой SynchronizationContext, который был установлен во время создания воркера. Бьюсь об заклад, завершение CancelAsync подтолкнет вас обратно к потоку пользовательского интерфейса, который блокируется, когда вы вызываете WaitOne в событии сброса списка каталогов. Событие GetDirListAsyncCompleted отправляется в цикл сообщений пользовательского интерфейса, но, поскольку поток пользовательского интерфейса заблокирован, он никогда не будет запущен, и событие сброса никогда не будет установлено.

БУМ! Тупик.

person Mike Strobel    schedule 17.10.2014
comment
Спасибо за объяснение. Есть ли что-нибудь, что я мог бы попытаться исправить? - person Dominic B.; 18.10.2014
comment
Вы можете попробовать избавиться от события сброса для операции со списком каталогов и запустить код удаления каталога непосредственно из обработчика GetDirListAsyncCompleted. - person Mike Strobel; 18.10.2014
comment
Звучит хорошо, я попробую и скажу вам, если это сработало. Спасибо! - person Dominic B.; 18.10.2014
comment
Кроме того, как правило, всякий раз, когда у вас есть поток, бесконечно ожидающий сигнала, вы должны сделать все возможное, чтобы гарантировать отправку сигнала (даже если операция, которую вы ожидаете, не удалась). Я бы порекомендовал вызывать Set() в блоке finally на случай, если что-то приведет к выбросу сигнала. О, а также никогда не блокируйте поток пользовательского интерфейса: D. - person Mike Strobel; 18.10.2014