One may ask with very good reason why even bother, since we already have easy-to-use multi-threading libraries available for more sophisticated languages ? Sometimes you may not have any other choice. A few years ago I started to work with one "state-of-the-art" analytics library for processing some extremely time-consuming calculations. By that time, all calculations were supposed to be processed in Excel and we did not have any access to real development API. Very fortunately, I discovered a collection of relevant VBA interface functions available for that library. Despite of this amazing discovery, processing calculations in VBA was still annoyingly slow. Finally I decided to test the scheme described above. I created multiple copies of one master workbook (which was having a relevant program for processing required calculations), opened multiple Excel workbooks and finally started a program within each workbook (almost) simultaneously. This was truly a "poor man's multi-threading", but despite of that it really did the job well. Example program in this post is taking this described scheme a bit further, as it completely removes the burden of administrating required Excel workbooks.
The main idea for this program is to create desired amount of active workbook copies (which has a relevant program for processing required calculations) into a folder. Moreover, for each of the workbook copy, corresponding VB script file will be created for starting required VBA program within Excel workbook copy. VB script is also taking care of all relevant administrative responsibilities (cleaning all Excel workbooks and VB script files from folder after program execution). Calculation results from different Excel threads will be printed into a shared text file. It should be noted, that SomeComplexAlgorithm procedure is an entry point for Excel thread (started by VB script). For brevity reasons, the content of this example program has been left to be trivial (simulate random number between one and ten for delay time execution and finally store that number into a collection).
Insert a new VBA module and copy-paste the following program.
Option Explicit ' ' common text file for results from all Excel threads Private Const resultsFilePathName As String = "C:\Users\Administrator\Desktop\ExcelThreading\shared.txt" ' Public Sub CreateExcelThreads() ' ' clean results text file Dim fileSystem As New Scripting.FileSystemObject fileSystem.CreateTextFile resultsFilePathName, True Set fileSystem = Nothing ' ' create (and execute) Excel workbook threads Dim ExcelThreadsFolderPathName As String: ExcelThreadsFolderPathName = "C:\Users\Administrator\Desktop\ExcelThreading\" Dim numberOfExcelThreads As Integer: numberOfExcelThreads = 4 Dim ExcelThreadName As String Dim i As Integer ' For i = 1 To numberOfExcelThreads ExcelThreadName = "ExcelThread_" + VBA.CStr(i) ExecuteExcelThread "SomeComplexAlgorithm", ExcelThreadName, ExcelThreadsFolderPathName Next i End Sub ' Public Function ExecuteExcelThread(ByVal TargetProgram As String, ByVal ExcelThreadName As String, ByVal ExcelThreadsPathName As String) ' ' save a copy of current active workbook Dim ExcelThreadFilePathName As String: ExcelThreadFilePathName = ExcelThreadsPathName + ExcelThreadName + ".xlsm" TargetProgram = ExcelThreadName + ".xlsm!" + TargetProgram Dim VBScriptFilePathName As String: VBScriptFilePathName = ExcelThreadsPathName + ExcelThreadName + ".vbs" ActiveWorkbook.SaveCopyAs ExcelThreadFilePathName ' ' create commands for VB script file Dim fileSystem As New Scripting.FileSystemObject Dim writer As Scripting.TextStream Set writer = fileSystem.OpenTextFile(VBScriptFilePathName, ForWriting, True) ' re-open previously saved Excel workbook writer.WriteLine "Set ExcelApplication = CreateObject(""Excel.Application"")" writer.WriteLine "Set ExcelWorkbook = ExcelApplication.Workbooks.Open(""" + ExcelThreadFilePathName + """)" writer.WriteLine "ExcelApplication.Visible = False" ' run target VBA program and close Excel workbook writer.WriteLine "ExcelWorkbook.Application.Run """ + TargetProgram + """" writer.WriteLine "ExcelApplication.ActiveWorkbook.Close True" writer.WriteLine "ExcelApplication.Application.Quit" ' delete copies of Excel workbook and VB script writer.WriteLine "Set fileSystem = CreateObject(""Scripting.FileSystemObject"")" writer.WriteLine "fileSystem.DeleteFile """ + ExcelThreadFilePathName + """" writer.WriteLine "fileSystem.DeleteFile """ + VBScriptFilePathName + """" writer.Close Set writer = Nothing Set fileSystem = Nothing ' ' execute VB script Dim scriptingShell As Object: Set scriptingShell = VBA.CreateObject("WScript.Shell") scriptingShell.Run VBScriptFilePathName Set scriptingShell = Nothing End Function ' Public Sub SomeComplexAlgorithm() ' ' this is target program to be executed by Excel thread ' program creates N random numbers between 1 and 10, stores these into ' collection and finally prints the content into a specific text file Dim simulationResult As New Collection Dim i As Integer For i = 1 To 25 ' due to brevity reasons, we just simulate some time-consuming algorithm Dim delayTime As Long: delayTime = WorksheetFunction.RandBetween(1, 10) Sleep delayTime ' store one simulated result (random delay time) into collection simulationResult.Add delayTime Next i ' ' print result collection into a specific text file ' we have to be prepared for the case in which multiple users (Excel threads) ' are accessing the same specific text file at the same time recoveryPoint: On Error GoTo errorHandler Dim fileSystem As New Scripting.FileSystemObject Dim writer As Scripting.TextStream ' ' if the file is in use, error will be thrown below here Set writer = fileSystem.OpenTextFile(resultsFilePathName, ForAppending, False) For i = 1 To simulationResult.Count writer.WriteLine ActiveWorkbook.Name + "=" + VBA.CStr(simulationResult(i)) Next i writer.Close Set simulationResult = Nothing Set writer = Nothing Set fileSystem = Nothing Exit Sub ' errorHandler: ' get one second delay and re-access text file Sleep 1 Resume recoveryPoint End Sub ' Public Function Sleep(ByVal seconds As Long) ' Dim startTime As Long: startTime = VBA.Timer Do If (VBA.Timer >= (startTime + seconds)) Then Exit Do Loop End Function '
Simulating 100 random numbers (setting delay time to 1) using just one Excel thread : processing time 0:01:50.
Simulating 100 random numbers (setting delay time to 1) using four Excel threads (25 numbers for each thread) : processing time 0:00:30, which turns out to be almost quadruple time improvement in comparison with single-threaded processing.
-Mike
Correct to say there is no multithreading in VBA however, you can very easily create your multithreading functionality in a c# dll and expose the interface via Interops for VBA to consume. Then all you need to do is to pass in the data into the function and walla, you have multithreading. Interops is a very powerful feature and VBA fully supports it
ReplyDeleteLooking back this post now: we should talk about "Multi-processing" instead of "Multi-threading".
ReplyDelete-Mike
If you are interested about getting this stuff done in a bit more sensible way, check out my following post:
ReplyDelete"Excel: implementing multithreading using C# and Excel-DNA"
https://mikejuniperhill.blogspot.com/2018/02/excel-implementing-multithreading-using.html
Going to check out your updated post.
DeleteHow would you write the script line [ writer.WriteLine "ExcelWorkbook.Application.Run """ + TargetProgram + """" ] to pass parameters to the subroutine SomeComplexAlgorithm(xVal as integer, xTxt as String) ?
ReplyDeleteHi,
ReplyDeleteI am working on multithreading or parallel processing solution containing ideas based on what you 'MJH' and few others have proposed and shown, but I am not copying the Excel into new workbooks storing the software, but generating new thread specific workbooks (.xslm) by building code from scratch (or code modules). Everything looks to be working nicely until I run the VBScript.
The result is however ugly 800A03EC error with saying "The macro may not be available in this workbook or all macros may be disabled".
The macro is there, but I have few doubts
1. As the error message says and as your code is probably put via Admin account could that be reason why VBScript does not allow running it? At least some blogs tell about same issue implying to Microsoft access things.
2. My macro is put in Module1 into new file, but your code did not care the module name. Is it actually needed? I did try both ways "file.xlsm!Module1.macroname" or without the "Module1.", but the error is still same.
3. What I wonder, but probably does not matter at the point of initiating is that the new workbook is missing few reference libraries which the master code uses, so I need to fix that anyway, but that is only thing I can think of now.
I believe case 1 is the most probable and real reason, but as novice ad-hoc coder in VBA I am empty of ideas how to fix the access to run macros could be done. Any ideas?
Just figured out what was wrong! Works now
Delete