Как использовать CancellationTokenSource для закрытия диалога в другом потоке?

Это связано с моим другим вопросом Как отменить фоновую печать.

Я пытаюсь лучше понять модель CancellationTokenSource и то, как ее использовать в разных потоках.

У меня есть главное окно (в потоке пользовательского интерфейса), где код делает:

 public MainWindow()
        {
            InitializeComponent();

            Loaded += (s, e) => {
                DataContext = new MainWindowViewModel();
                Closing += ((MainWindowViewModel)DataContext).MainWindow_Closing;

            };
        }

который правильно вызывает код CloseWindow при его закрытии:

 private void CloseWindow(IClosable window)
        {
            if (window != null)
            {
                windowClosingCTS.Cancel();
                window.Close();
            }
        }

При выборе пункта меню создается второе окно на фоновом потоке:

    // Print Preview
    public static void PrintPreview(FixedDocument fixeddocument, CancellationToken ct)
    {
        // Was cancellation already requested? 
        if (ct.IsCancellationRequested)
              ct.ThrowIfCancellationRequested();

               ............................... 

            // Use my custom document viewer (the print button is removed).
            var previewWindow = new PrintPreview(fixedDocumentSequence);

            //Register the cancellation procedure with the cancellation token
            ct.Register(() => 
                   previewWindow.Close() 
            );

            previewWindow.ShowDialog();

        }
    }

В MainWindowViewModel (в потоке пользовательского интерфейса) я помещаю:

public CancellationTokenSource windowClosingCTS { get; set; }

С его конструктором:

    // Constructor
    public MainMenu()
    {
        readers = new List<Reader>();
        CloseWindowCommand = new RelayCommand<IClosable>(this.CloseWindow);
        windowClosingCTS = new CancellationTokenSource();
    }

Теперь моя проблема. При закрытии MainWindow в потоке пользовательского интерфейса windowClosingCTS.Cancel() вызывает немедленный вызов делегата, зарегистрированного с помощью ct, т. е. вызывается previewWindow.Close(). Теперь это немедленно возвращает к " Если (Windows != null) с:

«Вызывающий поток не может получить доступ к этому объекту, потому что им владеет другой поток».

Так что я делаю неправильно?


person Alan Wayne    schedule 07.12.2016    source источник
comment
Я предполагаю, что вы используете WPF?   -  person d.moncada    schedule 07.12.2016
comment
Вы вообще не должны использовать многопоточность в этом процессе. Вы не привязаны к процессору. Придерживайтесь использования асинхронного однопоточного.   -  person Aron    schedule 07.12.2016
comment
@d.moncada да MVVM.   -  person Alan Wayne    schedule 07.12.2016
comment
@ Арон, а... ты потерял меня...   -  person Alan Wayne    schedule 07.12.2016
comment
При выборе пункта меню создается второе окно на фоновом потоке: это то, где вы ошибаетесь. Хотя я не вижу, где вы создаете фоновый поток, так как вы не опубликовали весь свой код. Выполняйте всю свою работу в одном потоке пользовательского интерфейса.   -  person Aron    schedule 07.12.2016
comment
второе окно создается в фоновом потоке: почему вы используете фоновый поток?   -  person d.moncada    schedule 07.12.2016
comment
@d.moncada Я подозреваю, что OP использует ShowDialog, чтобы ограничить время жизни окна задачей печати, однако это не MVVM. Решение MVPVM этой проблемы заключается в создании задачи печати, которая является ожидаемым вызовом метода (который может отображать окно).   -  person Aron    schedule 07.12.2016
comment
@Aron Полный код находится в моем другом вопросе, указанном вверху. Поскольку печать и предварительный просмотр могут занять несколько минут, я пытаюсь поместить их в отдельный поток и вообще не блокировать поток пользовательского интерфейса. Все работает, пока я не закрою приложение, не закрыв предварительно окно в фоновом потоке. Попытка использовать CancellationTokenSource для закрытия любого фонового потока до завершения работы основного приложения. Я пытаюсь освоить новые трюки...   -  person Alan Wayne    schedule 07.12.2016
comment
@ d.moncada Я использую отдельный поток, чтобы не блокировать поток пользовательского интерфейса, поскольку ввод данных пользователем не требуется, а печать / предварительный просмотр печати может занять несколько минут.   -  person Alan Wayne    schedule 07.12.2016
comment
@AlanWayne Вы не должны использовать фоновый поток, чтобы избежать блокировки потока пользовательского интерфейса. Сделать это правильно крайне сложно. Вы должны использовать асинхронный код обратного вызова (async/await).   -  person Aron    schedule 07.12.2016


Ответы (2)


Ваша проблема в том, что ваше окно предварительного просмотра работает в другом потоке. Когда вы инициируете отмену, вы выполняете зарегистрированное действие маркера отмены в этом потоке, а не в потоке, в котором выполняется предварительный просмотр.

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

Если вы хотите остаться со своим решением или если вы хотите инициировать отмену из фонового потока, вы должны маршалировать операцию закрытия в поток, в котором открыто ваше окно:

Action closeAction = () => previewWindow.Close();
previewWindow.Dispatcher.Invoke(closeAction);
person Sefe    schedule 07.12.2016
comment
Это сработало отлично. Итак, я думаю, что правильно использую токен отмены. Поскольку мне было указано (несколько человек) использовать только один поток, я не вижу, как это сделать без блокировки. При создании моего фиксированного документа используются меры и компоновка многих элементов структуры из нескольких тысяч записей на сетевом сервере. Для элементов фреймворка требуется поток STA. TASK использует поток MTA, поэтому они не смешиваются (и почему я использовал код в своем другом вопросе). Мне любопытно, как другие подошли к этой проблеме. Спасибо. Dispatcher.Invoke ответил на мои вопросы. - person Alan Wayne; 07.12.2016
comment
@AlanWayne Task не имеет ничего общего с потоками. Задача наверняка не использует поток MTA. Dispatcher.Invoke - правильный способ сделать это еще в .net 4.5. Но мы на .net 4.6.2, времена изменились. - person Aron; 07.12.2016
comment
@AlanWayne: обычно решение состоит не в том, чтобы запустить второй поток пользовательского интерфейса, а в рабочем потоке, отличном от пользовательского интерфейса. Обычно доступ к данным из элементов пользовательского интерфейса не является критичным для производительности, поэтому лучший способ сделать это — сначала синхронно собрать данные из элементов пользовательского интерфейса, запустить рабочую задачу, а затем маршалировать результат обратно в поток пользовательского интерфейса для отображения информации. . Что вы также можете сделать, так это использовать async/await в своих методах расчета. Если у вас есть методы, которые не поддерживают async/await (например, метод записи XPS в вашем другом вопросе), вы используете TaskCompletionSource (проверьте мой ответ там). - person Sefe; 07.12.2016
comment
@AlanWayne: Как ваше решение работает на практике? Потому что вы не отменяете создание документа, а просто не отображаете окно предварительного просмотра. Таким образом, создание документа будет продолжать выполняться в фоновом режиме и не будет отменено. Возможно, вы захотите проверить мой ответ на другой ваш вопрос, если вы хотите также отменить задачу печати. - person Sefe; 17.12.2016

Проблема с вашим кодом

При выборе пункта меню создается второе окно на фоновом потоке:

// Print Preview
public static void PrintPreview(FixedDocument fixeddocument, CancellationToken ct)
{
    // Was cancellation already requested? 
    if (ct.IsCancellationRequested)
          ct.ThrowIfCancellationRequested();

           ............................... 

        // Use my custom document viewer (the print button is removed).
        var previewWindow = new PrintPreview(fixedDocumentSequence);

        //Register the cancellation procedure with the cancellation token
        ct.Register(() => 
               previewWindow.Close() 
        );

        previewWindow.ShowDialog();

    }
}

И то, что я предполагаю

Task.Run(() => PrintPreview(foo, cancel));

Правильное решение — делать все в одном потоке.

public static Task<bool> PrintPreview(FixedDocument fixeddocument, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<bool>();
    // Was cancellation already requested? 
    if (ct.IsCancellationRequested)
          tcs.SetResult(false);
    else
    {
        // Use my custom document viewer (the print button is removed).
        var previewWindow = new PrintPreview(fixedDocumentSequence);

        //Register the cancellation procedure with the cancellation token
        ct.Register(() => previewWindow.Close());



        previewWindow.Closed += (o, e) =>
        {
             var result = previewWindow.DialogResult;
             if (result.HasValue)
                 tcs.SetResult(result.Value);
             else
                 tcs.SetResult(false);
         }
         previewWindow.Show();
    }

    return tcs.Task;
}

Тогда позвони

 var shouldPrint = await PrintPreview(foo, cancel);
 if (shouldPrint)
     await PrintAsync(foo);
person Aron    schedule 07.12.2016
comment
Я не понимаю, как это будет работать асинхронно. Кроме того, когда вы устанавливаете результат, если TCS в одном случае имеет значение DialogResult, а в другом — логическое значение, это вызовет проблемы. - person Sefe; 07.12.2016
comment
@Aron Изначально я делал все в одном потоке, но создание фиксированного документа требует обширного использования базы данных с созданием элементов структуры. Даже выполнение этого асинхронно само по себе занимало несколько минут, в течение которых поток пользовательского интерфейса был эффективно заблокирован. Помещение всего создания, печати и предварительного просмотра в отдельный поток на самом деле прекрасно работает без блокировки - пока я не попытаюсь отменить его. - person Alan Wayne; 07.12.2016
comment
@AlanWayne Доступ к базе данных не должен блокировать поток пользовательского интерфейса. Предполагая, что вы используете асинхронность полностью. Доступ к БД ограничен задержкой/пропускной способностью, а не ЦП. Вы должны иметь возможность использовать асинхронные вызовы базы данных. - person Aron; 07.12.2016
comment
@Sefe Дело в том, что OP, похоже, хочет использовать ShowDialog, который, похоже, хочет заблокировать некоторую обработку, пока пользователь взаимодействует с пользовательским интерфейсом. Кроме того, OP использует WPF, где Windows.DialogResult имеет тип bool?. - person Aron; 07.12.2016
comment
@Aron Проблема не в вызовах базы данных, а в том, что я использую множество различных элементов инфраструктуры xaml для создания фиксированной страницы из нескольких тысяч записей. Создание фиксированных страниц, используемых для отображения, может занять несколько минут. Создание элементов фреймворка и фиксированных страниц для длинных отчетов эффективно блокировало основной поток. - person Alan Wayne; 07.12.2016