VBA中的多线程
Posted
技术标签:
【中文标题】VBA中的多线程【英文标题】:Multi-threading in VBA 【发布时间】:2011-08-08 22:59:44 【问题描述】:这里有人知道如何让 VBA 运行多个线程吗?我正在使用 Excel。
【问题讨论】:
这里列出的大多数方法在技术上都是多处理,而不是多线程。 【参考方案1】:无法使用 VBA 本机完成。 VBA 构建在单线程单元中。获得多个线程的唯一方法是在具有 COM 接口的 VBA 以外的其他东西中构建一个 DLL,然后从 VBA 调用它。
INFO: Descriptions and Workings of OLE Threading Models
【讨论】:
@blog.tkacprow.pl - 这些方法都不是本机 VBA。它们都依赖于 VBA 之外的东西。 我同意。它们不是“本机”VBA 方法。但是,还有两种其他方法不需要求助于 COM/DLL。两者都只涉及 VBscript 和 VBA - 两种语言几乎相同。 1几乎只涉及VBA(用VBscript只创建VBA线程)。 @blog.tkacprow.pl - 您可能不必求助于编写自己的 DLL,但您必须使用 COM,因为这是唯一的方法(嗯,COM和 Win32 API),VBScript 和 VBA 可以通过它与自身外部的对象进行通信。每当您看到 GetObject 或 CreateObject 时,您就在使用 COM。您可以实现的最佳效果是使用有效的多进程解决方案,从而实现多线程。 是的,在这方面你是对的 :-)。感谢您的关注。无论哪种方式,仍然有 dll 的替代品——这是我唯一想强调的事情 正如答案所暗示的,使用单线程单元并不意味着您只能拥有一个线程。请参阅答案中链接的文档。公寓模型影响线程如何使用内存。 VBA 不支持多线程这一事实与其单元模型无关。【参考方案2】:正如您可能了解到的那样,VBA 本身并不支持多线程,但是。实现多线程有3种方法:
-
COM/dlls - 例如C# 和 Parallel 类在不同的线程中运行
使用 VBscript 工作线程 - 在单独的 VBscript 线程中运行您的 VBA 代码
使用执行的 VBA 工作线程,例如通过 VBscript - 复制 Excel 工作簿并并行运行宏。
我在这里比较了所有线程方法:http://analystcave.com/excel-multithreading-vba-vs-vbscript-vs-c-net/
考虑到方法 #3,我还制作了一个 VBA 多线程工具,可让您轻松地将多线程添加到 VBA:http://analystcave.com/excel-vba-multithreading-tool/
请看下面的例子:
多线程 For 循环
Sub RunForVBA(workbookName As String, seqFrom As Long, seqTo As Long)
For i = seqFrom To seqTo
x = seqFrom / seqTo
Next i
End Sub
Sub RunForVBAMultiThread()
Dim parallelClass As Parallel
Set parallelClass = New Parallel
parallelClass.SetThreads 4
Call parallelClass.ParallelFor("RunForVBA", 1, 1000)
End Sub
异步运行 Excel 宏
Sub RunAsyncVBA(workbookName As String, seqFrom As Long, seqTo As Long)
For i = seqFrom To seqTo
x = seqFrom / seqTo
Next i
End Sub
Sub RunForVBAAndWait()
Dim parallelClass As Parallel
Set parallelClass = New Parallel
Call parallelClass.ParallelAsyncInvoke("RunAsyncVBA", ActiveWorkbook.Name, 1, 1000)
'Do other operations here
'....
parallelClass.AsyncThreadJoin
End Sub
【讨论】:
在我的情况下,1
和 1000
是强制性的吗?我有一系列大单元要处理它与这种多线程的匹配度如何?
1
和1000
只是示例RunForVBA
函数的参数。 ParallelAsync 线程不需要这些参数。您也可以只保留 workbookName 参数。下载工具看看里面的例子:analystcave.com/excel-vba-multithreading-tool
我仔细看了看,我认为我必须自定义ParallelFor
open sourced by you right
除了写在工作表上之外,有没有更快的方法将结果从“线程”传回主代码?我需要从多个 websocket 流中接收数据数组,并尽可能减少交易机器人的延迟【参考方案3】:
我一直在寻找类似的东西,官方的回答是否定的。不过,我在 ExcelHero.com 上找到了 Daniel 的一个有趣的概念。
基本上,您需要创建worker vbscripts 来执行您想要的各种事情并将其报告回excel。对于我正在做的事情,从各种网站检索 html 数据,效果很好!
看看:
http://www.excelhero.com/blog/2010/05/multi-threaded-vba.html
【讨论】:
你可以有一百个 VBScript 程序从网络上获取数据,如果 VBA 是单线程的,它一次只能处理一个结果。 @JimmyPena 但是如果脚本正在完成所有工作,并且返回结果需要最少的处理,那么这仍然可以有效减少处理时间(虽然是的,它不是真正的线程)。您正在排队许多非常小的操作,而不是非常大的操作。 @Gaffi 我同意有一些方法可以使某些活动更快。但是 VBA 不是多线程的。期间。 要在 VBA 中进行 XMLHttp 抓取,我建议阅读 Tushar Mehta 的优秀 article on the topic。多年来一直使用这种方法,我可以证明它既快速又可靠。如果您不知道它,您会认为它是多线程的,因为请求是异步处理的。示例:youtu.be/Z0n7Wd7WiIE【参考方案4】:我之所以添加这个答案,是因为从更现代的语言开始使用 VBA 并在 Stack Overflow 上搜索 VBA 中的多线程的程序员可能不知道有几种本机 VBA 方法有时有助于弥补 VBA 缺乏真正的多线程。
如果多线程的动机是在执行长时间运行的代码时拥有一个响应速度更快的 UI,那么 VBA 确实有几个在实践中通常有效的低技术解决方案:
1) 可以使用户表单无模式显示 - 这允许用户在表单打开时与 Excel 进行交互。这可以在运行时通过将 Userform 的 ShowModal 属性设置为 false 来指定,或者可以通过放置该行作为从加载动态完成
UserForm1.Show vbModeless
在用户表单的初始化事件中。
2) DoEvents 语句。这会导致 VBA 将控制权交给操作系统以执行事件队列中的任何事件 - 包括 Excel 生成的事件。一个典型的用例是在代码执行时更新图表。如果没有 DoEvents,在宏运行之前不会重新绘制图表,但使用 Doevents,您可以创建动画图表。这个想法的一个变体是创建进度表的常见技巧。在要执行 10,000,000 次(并由循环索引 i 控制)的循环中,您可以有一段代码,例如:
If i Mod 10000 = 0 Then
UpdateProgressBar(i) 'code to update progress bar display
DoEvents
End If
这些都不是多线程——但在某些情况下它可能是一个足够的组合。
【讨论】:
谢谢。不幸的是,这并没有真正解决最初的问题,这与来自 VBA 的多线程有关。使用这些方法可能有助于防止 GUI 锁定,但最终会增加运行时间,这就是为什么许多程序员更喜欢在技术上可行的情况下在多线程上运行某些迭代任务。 我同意——我只是认为指出一些有时会有所帮助的纯 VBA 组件不会有什么坏处 有效观察。 非常有帮助和附加的答案! ? 添加这些变通方法;异步编程可以使用 VBA + 一点 WinAPI 或某些支持它的库 - 可以使用这种方法代替DoEvents
来提高响应能力(没有任何 nasty side effects。请参阅 this 和 this 以调度异步 Web 请求, 和任意 VBA 函数(完全公开;我写了这两个)【参考方案5】:
如前所述,VBA 不支持多线程。
但您不需要使用 C# 或 vbScript 来启动其他 VBA 工作线程。
我使用 VBA 创建 VBA 工作线程。
首先为您要启动的每个线程复制 makro 工作簿。
然后您可以通过创建 Excel.Application 的实例来启动新的 Excel 实例(在另一个线程中运行)(为避免错误,我必须将新应用程序设置为可见)。
要在另一个线程中实际运行某些任务,我可以使用主工作簿中的参数在另一个应用程序中启动一个 makro。
要在不等待的情况下返回主工作簿线程,我只需在工作线程(我需要它的地方)中使用 Application.OnTime。
作为信号量,我只是使用一个与所有线程共享的集合。 对于回调,将主工作簿传递给工作线程。那里的 runMakroInOtherInstance 函数可以被重用来启动回调。
'Create new thread and return reference to workbook of worker thread
Public Function openNewInstance(ByVal fileName As String, Optional ByVal openVisible As Boolean = True) As Workbook
Dim newApp As New Excel.Application
ThisWorkbook.SaveCopyAs ThisWorkbook.Path & "\" & fileName
If openVisible Then newApp.Visible = True
Set openNewInstance = newApp.Workbooks.Open(ThisWorkbook.Path & "\" & fileName, False, False)
End Function
'Start macro in other instance and wait for return (OnTime used in target macro)
Public Sub runMakroInOtherInstance(ByRef otherWkb As Workbook, ByVal strMakro As String, ParamArray var() As Variant)
Dim makroName As String
makroName = "'" & otherWkb.Name & "'!" & strMakro
Select Case UBound(var)
Case -1:
otherWkb.Application.Run makroName
Case 0:
otherWkb.Application.Run makroName, var(0)
Case 1:
otherWkb.Application.Run makroName, var(0), var(1)
Case 2:
otherWkb.Application.Run makroName, var(0), var(1), var(2)
Case 3:
otherWkb.Application.Run makroName, var(0), var(1), var(2), var(3)
Case 4:
otherWkb.Application.Run makroName, var(0), var(1), var(2), var(3), var(4)
Case 5:
otherWkb.Application.Run makroName, var(0), var(1), var(2), var(3), var(4), var(5)
End Select
End Sub
Public Sub SYNCH_OR_WAIT()
On Error Resume Next
While masterBlocked.Count > 0
DoEvents
Wend
masterBlocked.Add "BLOCKED", ThisWorkbook.FullName
End Sub
Public Sub SYNCH_RELEASE()
On Error Resume Next
masterBlocked.Remove ThisWorkbook.FullName
End Sub
Sub runTaskParallel()
...
Dim controllerWkb As Workbook
Set controllerWkb = openNewInstance("controller.xlsm")
runMakroInOtherInstance controllerWkb, "CONTROLLER_LIST_FILES", ThisWorkbook, rootFold, masterBlocked
...
End Sub
【讨论】:
【参考方案6】:我知道这个问题指定了 Excel,但由于 Access 的同一个问题被标记为重复,所以我将在此处发布我的答案。 原理很简单:打开一个新的 Access 应用程序,然后在该应用程序中打开一个带有计时器的表单,将要执行的函数/子发送到该表单,如果计时器命中则执行任务,并在执行完成后退出应用程序完成的。这允许 VBA 处理来自数据库的表和查询。注意:如果您已独占锁定数据库,它将引发错误。
这都是 VBA(与其他答案相反)
异步运行子/函数的函数
Public Sub RunFunctionAsync(FunctionName As String)
Dim A As Access.Application
Set A = New Access.Application
A.OpenCurrentDatabase Application.CurrentProject.FullName
A.DoCmd.OpenForm "MultithreadingEngine"
With A.Forms("MultiThreadingEngine")
.TimerInterval = 10
.AddToTaskCollection (FunctionName)
End With
End Sub
实现这一点所需的表单模块
(表单名称 = MultiThreadingEngine,没有设置任何控件或属性)
Public TaskCollection As Collection
Public Sub AddToTaskCollection(str As String)
If TaskCollection Is Nothing Then
Set TaskCollection = New Collection
End If
TaskCollection.Add str
End Sub
Private Sub Form_Timer()
If Not TaskCollection Is Nothing Then
If TaskCollection.Count <> 0 Then
Dim CollectionItem As Variant
For Each CollectionItem In TaskCollection
Run CollectionItem
Next CollectionItem
End If
End If
Application.Quit
End Sub
实现对参数的支持应该很容易,但是返回值很困难。
【讨论】:
【参考方案7】:Sub MultiProcessing_Principle()
Dim k As Long, j As Long
k = Environ("NUMBER_OF_PROCESSORS")
For j = 1 To k
Shellm "msaccess", "C:\Autoexec.mdb"
Next
DoCmd.Quit
End Sub
Private Sub Shellm(a As String, b As String) ' Shell modificirani
Const sn As String = """"
Const r As String = """ """
Shell sn & a & r & b & sn, vbMinimizedNoFocus
End Sub
【讨论】:
【参考方案8】:'speed up thread
dim lpThreadId as long
dim test as long
dim ptrt as long
'initparams
ptrt=varptr(lpThreadId)
Add = CODEPTR(thread)
'opensocket(191.9.202.255) change depending on configuration
numSock = Sock.Connect("191.9.202.255", 1958)
'port recieving
numSock1=sock.open(5963)
'create thread
hThread= CreateThread (byval 0&,byval 16384, Add , byval 0&, ByVal 1958, ptrt )
edit3.text=str$(hThread)
' use
Declare Function CreateThread Lib "kernel32" Alias "CreateThread" (lpThreadAttributes As long, ByVal dwStackSize As Long, lpStartAddress As Long, lpParameter As long, ByVal dwCreationFlags As Long, lpThreadId As Long) As Long
【讨论】:
以上是关于VBA中的多线程的主要内容,如果未能解决你的问题,请参考以下文章