【问题标题】:How to use CancellationTokenSource to close a dialog on another thread?如何使用 CancellationTokenSource 关闭另一个线程上的对话框?
【发布时间】:2016-12-07 05:58:41
【问题描述】:

这与我的另一个问题How to cancel background printing 有关。

我试图更好地理解 CancellationTokenSource 模型以及如何跨线程边界使用它。

我有一个主窗口(在 UI 线程上),后面的代码在其中:

 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(在 UI 线程上),我放了:

public CancellationTokenSource windowClosingCTS { get; set; }

其构造函数为:

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

现在我的问题。在 UI 线程上关闭 MainWindow 时,windowClosingCTS.Cancel() 会立即调用 ct 注册的委托,即调用 previewWindow.Close()。 现在立即返回" If (Windows != null) with:

"调用线程无法访问该对象,因为不同的 线程拥有它。”

那我做错了什么?

【问题讨论】:

  • 我假设您正在使用 WPF?
  • 你不应该在这个过程中使用多线程。您不受 CPU 限制。坚持使用异步单线程。
  • @d.moncada 是 MVVM。
  • @Aron 啊...你失去了我...
  • "在后台线程上创建了第二个窗口:"你为什么使用后台线程?

标签: c# wpf multithreading cancellationtokensource


【解决方案1】:

您的问题是您的预览窗口在另一个线程上运行。当您触发取消时,您在该线程上执行取消令牌的注册操作,而不是在运行预览的线程上。

这些情况下的黄金标准是不使用两个 UI 线程。这通常会引起麻烦,而您需要处理它们的工作通常不值得。

如果您想继续使用您的解决方案,或者如果您想从后台线程触发取消,您必须将关闭操作编组到打开窗口的线程:

Action closeAction = () => previewWindow.Close();
previewWindow.Dispatcher.Invoke(closeAction);

【讨论】:

  • 效果很好。所以我想我正确使用了取消令牌。由于(多人)向我指出只使用一个线程,我无法看到如何在不阻塞的情况下执行此操作。我的固定文档的创建使用来自网络服务器上数千条记录的许多框架元素的测量和布局。框架元素需要一个 STA 线程。 TASK 使用 MTA 线程,因此两者不会混合(以及为什么我在另一个问题中使用代码)。我很好奇其他人是如何解决这个问题的。谢谢。 Dispatcher.Invoke 回答了我的问题。
  • @AlanWayne 任务与线程没有任何关系。任务肯定不使用 MTA 线程。 Dispatcher.Invoke 是在 .net 4.5 中执行此操作的“正确”方式。但是我们正在使用 .net 4.6.2,时代变了。
  • @AlanWayne:解决方案通常不是启动第二个 UI 线程,而是启动一个非 UI 工作线程。通常,从 UI 元素访问数据不是性能关键,因此最好的方法是首先同步从 UI 元素收集数据,启动工作任务,然后将结果编组回 UI 线程以显示信息.您还可以在计算方法中使用 async/await。当您有不支持 async/await 的方法时(例如您的另一个问题中的 XPS 写入方法),您可以使用 TaskCompletionSource(在此处查看我的答案)。
  • @AlanWayne:您的解决方案在实践中效果如何?因为您所做的不是取消文档创建,而是您只是不显示预览窗口。所以文档创建会继续在后台运行,不会被取消。如果您还想取消打印任务,您可能想查看我对您其他问题的回复。
【解决方案2】:

你的代码有问题

选择一个菜单项后,第二个窗口就会在 后台线程:

// 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);

【讨论】:

  • 我不知道这将如何异步运行。此外,当您将 TCS 一次设置为 DialogResult 并在另一次设置为 bool 时,这将导致麻烦。
  • @Aron 我最初在同一个线程上做了所有事情,但是创建固定文档需要大量使用数据库并创建框架元素。即使以异步方式执行此操作本身也需要几分钟,在此期间 UI 线程被有效阻塞。将所有创建、打印和打印预览放在一个单独的线程上实际上可以很好地工作而不会阻塞——直到我尝试取消它。
  • @AlanWayne 数据库访问不应阻塞 UI 线程。假设您一直使用异步。数据库访问受延迟/带宽限制,不受 CPU 限制。您应该能够使用异步数据库调用。
  • @Sefe 关键是 OP 似乎想要使用 ShowDialog 似乎想要阻止一些处理,而用户与 UI 交互。此外,OP 正在使用 WPF,其中 Windows.DialogResult 的类型为 bool?。
  • @Aron 问题不在于数据库调用,而是我使用许多不同的 xaml 框架元素从数千条记录构建固定页面。构建用于显示的固定页面可能需要几分钟时间。为长报告构建框架元素和固定页面实际上阻塞了主线程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-10
  • 2011-11-05
相关资源
最近更新 更多