使用多线程进行慢速 Ms 互操作

Posted

技术标签:

【中文标题】使用多线程进行慢速 Ms 互操作【英文标题】:Using Multi-threading for Slow Ms Interop 【发布时间】:2020-09-17 01:08:56 【问题描述】:

我有一个程序可以在单击按钮时创建两个 pdf 文件。它在 WinForms 中使用 Microsoft Office 互操作,文件创建过程如下;

    用户在程序中处理某事 点击按钮 程序根据包含书签的模板创建 word 文件 写入书签及其表格 另存为 pdf 格式 关闭活动文档 关闭单词app 关闭子窗体并切换到另一个窗体

*单词 app 对用户不可见

它本身可以正常工作,但是完成这两个文件需要 8 秒,所以我尝试使用多线程,这样用户就不必等待,可以处理其他事情或继续执行步骤再 1 次。

但是,它会引发各种错误; COM、RPC 等,甚至在数据库连接中,我认为原因是因为有两个单独的线程在工作并且它使用相同的资源,所以某个时间点另一个可能关闭了另一个线程的资源正在使用/即将使用 所以我尝试使用join(),这样另一个线程可能会先完成它的工作,关闭其各自的资源,然后继续下一个。

这很好用,如果用户没有在另一个文件之后单击按钮来创建另一个文件(在用户完成第一步比预期更快的情况下)

join() 足以处理错误以及在后台处理文件创建,但是,我想处理文件创建紧随其后的情况,因为在这种情况下,然后看起来由于线程相互等待,进程是线性的,除了它会产生瓶颈,其中线程现在排队并阻塞主线程,这使用户等待 12 秒或更长时间。

我想知道/需要澄清的是;

    我可以创建这两个使用 interop 的文件使用不同的资源,这样就不会产生错误吗? 我可以使用join() 的替代方法还是有替代方法,它不会阻塞 UI 线程或主线程?这样文件创建线程就在后台排队(仍然等待对方完成),而不会让用户等待。 我是否错误地使用了 join() 或最终消耗更多时间的线程?

这是我的代码;

用于创建文档的模块中的过程和公共变量:

Public tsThread As Thread
Public psThread As Thread

Sub SaveDoc(docType,someArgs)
        Dim wordApp = New Word.Application
        Dim templateBookmarks As Word.Bookmarks
        Dim templateName As String
        Dim template As New Word.Document
        wordApp = CreateObject("Word.Application")

        Select Case docType
            Case "Type1"
                templateName = "SampleType.docx"
                template = wordApp.Documents.Add(templatePath & templateName)
                templateBookmarks = template.Bookmarks

                templateBookmarks.Item("bookmarkInWord").Range.Text = "Foo"
                template.Tables(1).Cell(msWordRow, 1).Range.Text = "Value in cell 1"   
            Case "Type2"
                ‘Same thing, just different values and template
            Case "Type3"
        End select
        template.SaveAs2(savePath & saveName, Word.WdSaveFormat.wdFormatPDF)
        wordApp.ActiveDocument.Close(Word.WdSaveOptions.wdDoNotSaveChanges)
        wordApp.Quit()
End Sub

单击按钮时子窗体中的过程:

        If Not IsNothing(tsThread) Then
            If tsThread.IsAlive Then
                tsThread.Join()
            End If
        End If
        If Not IsNothing(psThread) Then
            If psThread.IsAlive Then
                psThread.Join()
            End If
        End If
        tsThread = New Thread(Sub() SaveDoc(docType1,someArgs))
        tsThread.Start()

        If Not IsNothing(tsThread) Then
            If tsThread.IsAlive Then
                tsThread.Join()
            End If
        End If
        psThread = New Thread(Sub() SaveDoc(docType2,someArgs))
        psThread.Start()

        If records = maxRecords Then
            If psThread.IsAlive or tsThread.isAlive Then
                tsThread.Join()
                psThread.Join()
                Dim fooThread = New Thread(Sub() SaveDoc(docType3,someArgs))
                fooThread.Start()
            End If
        End If
   SwitchForm("Child Form for Step 1")
   End Sub

没有使用join()的错误通常出现在这些代码部分:

wordApp.ActiveDocument.Close(Word.WdSaveOptions.wdDoNotSaveChanges)

或在

wordApp.Quit()

或在书签中

templateBookmarks.Item("bookmarkInWord").Range.Text = "Foo"

或者有时在不同时间或数据库连接过程中的一个案例中的任何地方。

我对多线程以及 Interop 女士还是比较陌生,所以如果我有任何误解,我会寻求帮助。代码在 VB 中,但我也可以理解 C#。非常感谢任何帮助/指导。谢谢!

【问题讨论】:

你打电话给wordApp = New Word.ApplicationwordApp = CreateObject("Word.Application")有什么原因吗? 【参考方案1】:
    我不会尝试一次使用来自多个线程的任何 word/office 互操作。以我的经验,这些事情本身就足够不可靠,而不会引入可能的并发问题。但这可能取决于您到底在做什么。 线程和连接是处理多线程的相当老派的方法。较新的方法是使用Tasks and async/await。这可能有助于强制执行一致的排序,而不会阻塞主线程。 您通常应避免在从主线程执行时可能阻塞的任何方法,包括 .Join()。

我会考虑使用生产者/消费者模式。当用户单击按钮时,主线程将生成要处理的文档,而后台线程将消耗文档并进行处理。 blocking collection 通常可以用作两个线程之间的接口。

另一种选择是limited concurrency task scheduler。这使您可以安排任务进行处理,同时确保同时处理一个(或其他一些限制)。

【讨论】:

【参考方案2】:

我的建议是使用嵌入的 VBA 程序创建一个启用宏的模板,该程序执行所有书签和保存到 PDF。这将 Interop 调用减少为仅创建/处理单词 application 并使用传入的参数运行 VBA 过程。

【讨论】:

以上是关于使用多线程进行慢速 Ms 互操作的主要内容,如果未能解决你的问题,请参考以下文章

程序中的线程加互操作

如何在互操作期间阻止 MS Graph 组件弹出?

.NET 核心 3.0 和 MS Office 互操作

DotNet2.0不允许线程互操作各控件的解决方法。

C# 对本机互操作的限制

Kotlin 与 Objective-C 框架的多平台/本机互操作性