【问题标题】:Task blocks UI thread with .dll method任务用 .dll 方法阻塞 UI 线程
【发布时间】:2019-11-27 09:52:37
【问题描述】:

我创建了用于将数据导出到 Excel 的应用程序。创建 Excel 文件的时间大约需要 10 分钟,所以我决定在另一个线程中执行此操作,而不阻塞 UI 线程,以便用户可以继续使用 app.我有一个用于导出数据的 .dll,而且我在 Winforms 中多次做过同样的事情,完全没有问题。我的 .dll 文件将取消标记作为参数,因此用户可以随时取消导出,这就是我更喜欢使用此 .dll 的原因。

不幸的是,我对 WPF 很陌生,所以我不知道如何创建与 Winforms 中相同的东西。这是我的代码 sn-p:

public CancellationTokenSource cancel_task = new CancellationTokenSource();  

private async void Button1_Click(object sender, RoutedEventArgs e)
{
            //Doing some work in UI thread before calling Task.Run …

            await Task.Run(() =>
            {
               Export_to_Excel(dict_queries, dtp.Value);
            }, cancel_task.Token);
}

void Export_to_Excel(Dictionary<string, int> queries, DateTime? date)
{
    try
    {
       using (var con = new OracleConnection(conn_string))
       {
         con.Open();

         //Fetching DB Command to start with exporting data ...

         //Inicializing my .dll method, added as reference to project   
         var export_xlsx = new Excel_export();

         //Here is where It starts to block UI thread
         Application.Current.Dispatcher.Invoke(() => { export_xlsx.Export_command(cmd_export,cancel_task.Token);});
       } 
     }
     catch (Exception ex)
     {
          MessageBox.Show (ex.Message);
     }
 }

如果我只使用这一行export_xlsx.Export_command(cmd_export,cancel_task.Token); 而不是Application.Current.Dispatcher.Invoke...,然后我得到“System.Threading.ThreadStateException:当前线程必须设置为单线程单元(STA)模式,然后才能进行OLE调用......”错误。这在我看来就像一个 .dll 回到了 UI 线程上,即使它是从非 UI 线程调用的......但是这条线在 Winforms 中非常适合我,所以我不知道这里有什么问题。

我尝试了 Task.RunTask.Factory.StartNew 的许多不同变体(就像我通常在 Winforms 中所做的那样),但一切都导致我阻塞 UI 线程或不同类型的错误。

我的设计是带框架的主窗口,在该框架内我打开一个页面,单击按钮导出数据。如前所述,我是 WPF 的新手,所以也许这导致了我的问题。任何建议都非常感谢!

编辑:

我想我知道出了什么问题。在我的 .dll 代码中,我调用了 SaveFileDialog.ShowDialog(),据我所知,它是一个 UI 线程操作。不幸的是,我在这个应用程序中需要它,在这种情况下我能做些什么吗?

我猜这就是为什么在 Winforms 中一切正常的原因,因为我不在那里使用 SaveFileDialog。

【问题讨论】:

  • 您是否尝试过启动线程(不是任务)并使用方法Thread.SetApartmentStateThread.ApartmentState 属性设置为值ApartmentState.STA
  • @TheodorZoulias,我试过了,但我不知道在哪里把它放在代码中。可能有点指导?
  • Button1_Click 处理程序中,不要使用await Task.Run 启动任务,而是创建并启动一个运行Export_to_Excel 方法的新线程。并在启动前设置线程的ApartmentState
  • @TheodorZoulias,我收到与 Erwin 发布的解决方案相同的错误,只是这次它在我调用方法 Export_to_Excel 时显示错误。
  • 你能在Task.Run()之后贴上ConfigureAwait(false),看看有什么变化吗?

标签: c# wpf multithreading task multitasking


【解决方案1】:

根据您的代码,您同时陷入两个错误:

  1. 您正在尝试在非 STA 线程中使用 OLE
  2. 您正试图从另一个线程访问 DispatcherObject (SaveFileDialog)

为什么您的原始代码有效(但会阻止 UI)?因为您正在将 OLE 工作分派给 STA 的 UI 线程,并且您可以从 UI 线程毫无问题地访问 SaveFileDialog。

关于“调用线程无法访问此对象...”错误:

Application.Current.Dispatcher.Invoke 会将工作委托给 UI 踏板,因此在您的代码中,您正在创建一个线程以使 UI 再次工作;这就是您阻止 UI 的原因。使用 Invoke 仅从另一个线程更新 UI 元素(或任何 DispatcherObject),而不是完成所有工作。如果您尝试在没有 Invoke 的情况下从另一个线程访问 DispatcherObject(如 UI 控件),则会收到“调用线程无法访问此对象,因为另一个线程拥有它”错误。

现在关于“当前线程必须设置为单线程单元 (STA)...”错误:

尝试设置您正在创建的新线程的单元状态并在 SaveFileDialog 上使用 Invoke:

    public static Task<bool> StartSTATask(Action func) {
      var tcs = new TaskCompletionSource<bool>();
      Thread thread = new Thread(() =>
      {
        try {
          func();
          tcs.SetResult(true);
        }
        catch (Exception e) {
          tcs.SetException(e);
        }
      });
      thread.SetApartmentState(ApartmentState.STA);
      thread.Start();
      return tcs.Task;
    }

public CancellationTokenSource cancel_task = new CancellationTokenSource();  

private async void Button1_Click(object sender, RoutedEventArgs e)
{
            //Doing some work in UI thread before calling Task.Run …
          try{
            await StartSTATask(() => { Export_to_Excel(dict_queries, dtp.Value);});
           }
          catch(Exception ex){
              MessageBox.Show (ex.Message); //lets keep UI things in UI thread
          }

}

void Export_to_Excel(Dictionary<string, int> queries, DateTime? date)
{

       using (var con = new OracleConnection(conn_string))
       {
         con.Open();

         //Fetching DB Command to start with exporting data ...

         //Inicializing my .dll method, added as reference to project   
         var export_xlsx = new Excel_export();

         export_xlsx.Export_command(cmd_export,cancel_task.Token);
       } 
     }  
 }

//into dll

public void Export_command(cmd_export,cancel_task.Token){
  //do work
  Application.Current.Dispatcher.Invoke( () => SaveFileDialog.ShowDialog());
}

【讨论】:

  • @jvaquero,您提供的链接对我不起作用,一旦我更改应用程序的构建操作,我就会收到错误消息。关于您发布的代码 - 我如何使用它以及在哪里使用,对我来说有点太复杂了?
  • 差不多这样
  • 请看我的编辑。
  • @Lucy82 我应该假设您不能更改 .dll 吗?因为如果您可以更改它,您可以应用我答案的第一部分;调用 Invoke( ()=> SaveFileDialog.ShowDialog()) 应该可以工作....
  • Invoke( ()=> SaveFileDialog.ShowDialog()) 在我的 .dll 中不起作用。我想出了一种在 UI 中显示对话框的方法,但代码变得非常混乱。虽然还没有尝试过你的完整示例,但会让你知道。
【解决方案2】:

这可以解决您的问题:

   Excel_export export_xlsx;
private async void Button1_Click(object sender, RoutedEventArgs e)
{
        //Doing some work in UI thread before calling Task.Run …

        await Task.Run(() =>
        {
           export_xlsx = new Excel_export();
           Export_to_Excel(dict_queries, dtp.Value);
        }, cancel_task.Token);
}

void Export_to_Excel(Dictionary<string, int> queries, DateTime? date)
{
try
{
   using (var con = new OracleConnection(conn_string))
   {
     con.Open();

     //Fetching DB Command to start with exporting data ...

     //Here is where It starts to block UI 
      export_xlsx.Export_command(cmd_export,cancel_task.Token);
   } 
 }
 catch (Exception ex)
 {
      MessageBox.Show (ex.Message);
 }

}

【讨论】:

  • 这导致我在 Task.Run 行中出现“调用线程无法访问此对象,因为另一个线程拥有它”错误。
  • 这很奇怪,试试 Application.Current.Dispatcher.Invoke(() => { export_xlsx.Export_command(cmd_export,cancel_task.Token);});查看 UI 是否仍然响应
  • 不,我仍然收到同样的错误。我的 App.xaml 可能有问题吗?我稍微改了一下,.cs 里面没有 Main() 方法。
  • 请看我的编辑。
猜你喜欢
  • 2017-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多