This is a Ax-DLL (VBMThread11.DLL) for generic multithreading in VB6.
Current version: 1.1.2
There is only one public creatable component, the Thread class.
The Thread class is interface driven and once created the new thread's main function is fired in the BackgroundProcedure event.
In this event the background work will be done, as it will not lock or freeze the App.
But attention is required as showing Forms will crash VB.
Therefore in the BackgroundProcedure event is a param StatusCallback. (IThreadStatusCallback class)
By this you can fire an synchronous callback. The background thread will be suspended, the method request is marshaled to the main thread.
On this StatusCallback event you can safely show forms or report progress.
It is recommended to access project objects (classes, controls) only in the StatusCallback event.
Do not debug or use 'App.Path' in the BackgroundProcedure event, doing so will cause a crash. (Unless the DebugMode property was set to True)
The source code of the project can also be viewed on GitHub.
Registration-Free (Side-by-side) solution for VBMThread11.DLL can be found here.
Notes:
- P-Code will fail. Native compilation is necessary.
List of revisions:
Code:
29-Apr-2017
- The Thread class is not event driven anymore. All "events" are fired now trough an IThread interface.
- Included mandatory Owner parameter in the Create method. That owner will receive the IThread events.
Also included an optional Key parameter that helps to identify multiple threads on the same owner.
- Renamed event 'AsyncProcedure' to 'BackgroundProcedure' and 'SyncCallback' to 'StatusCallback'.
- Included event 'Complete' that fires after the thread ran to completion or was canceled.
- Optional Wait parameter included in the Terminate method.
- Included the ThreadData class that will be passed along the BackgroundProcedure and Complete event.
- Included Cancel method. This method sets the CancellationPending property to True in the ThreadData.
How fast the cancellation is done depends on the code in the background procedure and if the developer respects the cancellation request.
- Included DebugMode property. If set to True the background thread runs synchronous to the main thread.
This does allow to safely set breakpoints and perform debugging in the IDE.
17-Jun-2016
- Fixed a bug in the Suspended property.
15-Jun-2016
- First release.
Attachment: VBMThread11 Ax-DLL project and a demo project.
This is a Ax-DLL (VBMThread10.DLL) for generic multithreading in VB6.
I know that there are plenty of libraries out there providing exactly this.
However, I want to share this approach and want to demonstrate how to use it.
Registration-Free (Side-by-side) is not working on this Ax-DLL. It must be registered normally in order to function.
No libraries are out there providing a barebones minimal framework that is open source...
you beat me to it! Very nice. You've done a great many services to the community.
Last edited by DEXWERX; Jun 16th, 2016 at 10:18 AM.
Great, but it is the very restricted multithreading, because you can't work with the project specified objects classes/forms/controls/etc. I described the similar approach here.
Great, but it is the very restricted multithreading, because you can't work with the project specified objects classes/forms/controls/etc.
When you want to work with project specified objects (classes/forms/controls/etc.) you need to make a SyncCallback.Raise in the AsyncProcedure event.
In this SyncCallback event you can do everything safely as normal. (it is running on the original main thread)
In the AsyncProcedure event itself you can work with objects which you do create within the procedure, e.g. CreateObject() or simple project classes.
Example:
Code:
Private Sub BackWorker_AsyncProcedure(ByVal SyncCallback As VBMThread10.IThreadSyncCallback)
' ...
SyncCallback.Raise
End Sub
Private Sub BackWorker_SyncCallback(Argument As Variant)
CommonDialogPageSetup.ShowPageSetup ' Project object variable access
End Sub
So I think this approach makes it quite flexible.
And it was my goal to make a generic solution which is also simple.
Great, but it is the very restricted multithreading, because you can't work with the project specified objects classes/forms/controls/etc. I described the similar approach here.
Originally Posted by Krool
So I think this approach makes it quite flexible.
And it was my goal to make a generic solution which is also simple.
It's a very simple asyncronous framework.
It's simple enough for someone to incorporate Trick's methods, as well as add more flexibility or complexity.
It would be nice for it to be single threaded for testing / IDE debugging, although again that can be added outside of the framework.
Last edited by DEXWERX; Jun 16th, 2016 at 03:10 PM.
Yes. Also, it's very useful for the multithreading newbies and professionals too because it doesn't require the big efforts for understanding. Everything is very simple - Create/Asynch/Synch. Of course there is some issues with parameters but at first time it doesn't matter.
Originally Posted by DEXWERX
It's simple enough for someone to incorporate Trick's methods, as well as add more flexibility or complexity.
I would like to note the few details. The code should have the message loop in order to support the marshaling. For example if a project doesn't contain forms you have to loop message cycle (DoEvents, for example, but it loads a thread it's better use GetMessage/TranslateMessage/DispatchMessage functions). You should wait for the thread termination as well. Optionally, as an user of your library i would want to have the ability of passing a parameter to a thread.
Originally Posted by DEXWERX
It would be nice for it to be single threaded for testing / IDE debugging, although again that can be added outside of the framework.
The Krool approach is quite stable in your example, but it'll crash anyway in IDE. All my methods doesn't work in IDE (except EXE multithreading that works in the main thread). I absolutely have no time to translate my examples to English and public the new projects. In short, that example uses the precompiled DLL and creates the object in the new thread. You have the ability to call a method synchronously and asynchronously and receive the event from this object.
Update released.
Minor bugfix in the Suspended property.
The DLL in /Bin have been recompiled with binary compatibility.
Originally Posted by The trick
Optionally, as an user of your library i would want to have the ability of passing a parameter to a thread.
You could access the projects variables within the AsyncProcedure event and use this as a passing parameter. But, of course a Param could be included out of the event itself.
Originally Posted by ChenLin
Can this be used in the sql server database query?
If you create the objects within the AsyncProcedure event it should be possible.
Here is a solution to use the VBMThread11.DLL Registration-Free. (Side-by-side)
Keep in mind that this technology needs at minimum Windows XP SP2 or Windows Server 2003.
Big thanks goes to 'the trick' for the solution with "comInterfaceExternalProxyStub" in the manifest.
Tutorial:
The "Development" machine needs to register VBMThread11.DLL as usual and use the components for e.g. in a Std-EXE project.
The source project needs to include the Side-by-side resources. (see below)
Then on the "End user" machine you only need the VBMThread11.DLL and the .exe (Std-EXE project) on the same folder.
It will work then without any registration.
The source code of "VBMThread11SideBySide.res" is:
Dear Krool, how can i use the res file?
i have try to find the original post about comInterfaceExternalProxyStub, but found nothing.
can you indicate it
It is also possible to add more complexity in the interaction between the AsyncProcedure and SyncCallback events.
So one would create a project class called cParams, which could just contain two properties named 'Command' and 'Value'.
Code:
Private Sub BackWorker_AsyncProcedure(ByVal SyncCallback As VBMThread10.IThreadSyncCallback)
Dim Params As cParams
Set Params = New cParams
Params.Command = "Do that and this"
Params.Value = 150
SyncCallback.Raise Params
End Sub
Private Sub BackWorker_SyncCallback(Argument As Variant)
If Argument.Command = "Do that and this" Then MsgBox Argument.Value
End Sub
Conclusion:
In general there seems to be no issue to create or even access classes from the project, at least for simple classes, within the AsyncProcedure event.
It seems very useful.
Thanks for the sample project.
I would be even better if the objects of the main thread could be used, something like SyncCallback.Marshall(SomeRecordset).MoveFirst...
But perhaps it's too difficult to do (or am I saying nonsense?).
It seems very useful.
Thanks for the sample project.
I would be even better if the objects of the main thread could be used, something like SyncCallback.Marshall(SomeRecordset).MoveFirst...
But perhaps it's too difficult to do (or am I saying nonsense?).
You can marshall interfaces to the async procedure yourself if you really need to, but using the SyncCallback is way easier.
Also hopefully the example you posted wasnt a real world usage...
using a Recordset on the main thread from another thread - completely defeats the purpose of using another thread.
Last edited by DEXWERX; Apr 20th, 2017 at 01:53 PM.
You can marshall interfaces to the async procedure yourself if you really need to, but using the SyncCallback is way easier.
Also hopefully the example you posted wasnt a real world usage...
How do you do that?
How do you send an object from the SyncCallback to be used in the async procedure (the one that executes in the new thread)?
Originally Posted by DEXWERX
using a Recordset on the main thread from another thread - completely defeats the purpose of using another thread.
As I understand it, may be or may be not.
If I would be doing intensive use of the recorset you are right, but if I only needed to make some consult (and the main task is another one) not, I think.
I guess I can try and put together a demo, but the Threading DLL source shows how to manually martial a "Thread" interface from the main thread onto the new thread.
Originally Posted by Eduardo-
As I understand it, may be or may be not.
If I would be doing intensive use of the recorset you are right, but if I only needed to make some consult (and the main task is another one) not, I think.
That's correct, just trying to be clear. MoveFirst implied you're going to use a recordset in a loop. *shrugs*
Funny story... You can use COM's automatic martialling.
Code:
Option Explicit
Private WithEvents t As Thread
Private m_Rec As ADODB.Recordset
Private Sub Form_Click()
t.Create
End Sub
Private Sub Form_Load()
'Create In Memory Recordset
Set m_Rec = New ADODB.Recordset
With m_Rec
.Fields.Append "Name", adVarChar, 10, adFldMayBeNull
.CursorType = adOpenKeyset
.CursorLocation = adUseClient
.LockType = adLockPessimistic
.Open
Dim Ind As Long
For Ind = 1 To 5
.AddNew
.Fields(0) = "Name " & Ind
.Update
.MoveNext
Next
End With
Set t = New Thread
End Sub
Private Sub Form_Unload(Cancel As Integer)
If t.hThread <> 0 Then
Cancel = True
MsgBox "Thread is not complete."
Exit Sub
End If
End Sub
Private Sub t_AsyncProcedure(ByVal SyncCallback As VBMThread10.IThreadSyncCallback)
Dim a, MyRec As ADODB.Recordset
SyncCallback.Raise a
Set MyRec = a
MyRec.MoveFirst
Do Until MyRec.EOF
'OutputDebugString MyRec.Fields(0)
SyncCallback.Raise MyRec.Fields(0).Value
MyRec.MoveNext
Loop
End Sub
Private Sub t_SyncCallback(Argument As Variant)
If VarType(Argument) = vbString Then
MsgBox Argument
Else
Set Argument = m_Rec
End If
End Sub
Ah, that's great, then... does the Argument parameter marshall any COM object passed between the procedures?
(I still didn't study the DLL source code)
Another question: only one object at a time can be marshalled?
Ah, that's great, then... does the Argument parameter marshall any COM object passed between the procedures?
(I still didn't study the DLL source code)
Another question: only one object at a time can be marshalled?
It doesn't seem like private objects. But you can automatically Marshall private objects as IDispatch (Object).
And then latebound calls work.
Code:
Private Sub t_AsyncProcedure(ByVal SyncCallback As VBMThread10.IThreadSyncCallback)
Dim a, f, MyRec As ADODB.Recordset, MyForm As Object 'Form1 is private to main thread...
SyncCallback.Raise a
Set MyRec = a
f = "Form"
SyncCallback.Raise f
Set MyForm = f
MyForm.Caption = "Caption Set From the other thread!"
MyRec.MoveFirst
Do Until MyRec.EOF
SyncCallback.Raise MyRec.Fields(0).Value & " - " '& WINAPI.GetCurrentThreadId ' API's called on other thread using a typelib.
MyRec.MoveNext
Loop
End Sub
Private Sub t_SyncCallback(Argument As Variant)
If VarType(Argument) = vbString Then
Select Case Argument
Case "Form"
Dim Obj As Object
Set Obj = Me
Set Argument = Obj
Case Else
MsgBox "Main Thread: " & App.ThreadID() & " " & Argument
End Select
Else
Set Argument = m_Rec
End If
End Sub
or an Array of Variant or Object. Seems to work.
Code:
Private Sub t_AsyncProcedure(ByVal SyncCallback As VBMThread10.IThreadSyncCallback)
Dim a, MyRec As ADODB.Recordset, MyForm As Object 'Form1
SyncCallback.Raise a
Set MyForm = a(0)
Set MyRec = a(1)
MyForm.Caption = "Caption Set From the other thread!"
MyRec.MoveFirst
Do Until MyRec.EOF
SyncCallback.Raise MyRec.Fields(0).Value & " - " & WINAPI.GetCurrentThreadId
MyRec.MoveNext
Loop
End Sub
Private Sub t_SyncCallback(Argument As Variant)
Dim Args(1) ' As Object ' Or As Variant
Dim Obj As Object: Set Obj = Me
If VarType(Argument) = vbString Then
MsgBox "Main Thread: " & App.ThreadID() & " " & Argument
Else
Set Args(0) = Obj
Set Args(1) = m_Rec
Argument = Args
End If
End Sub
Last edited by DEXWERX; Apr 20th, 2017 at 04:04 PM.
Thanx for the project. Unfortunately, and maybe nothing you can do about it, it isn't very safe in the IDE. For example, I tried to terminate any active threads in form unload while the progress bar window was running, but it crashed. When compiled, no crash. The simple example looked like this:
Code:
Me.Hide
If BackgroundWorker.hThread Then
Do Until BackgroundWorker.Terminate = True
Sleep 100
Loop
End If
If FormWorker.hThread Then
Do Until FormWorker.Terminate = True
Sleep 100
Loop
End If
Maybe your library can provide an optional synchronous Terminate event? Returning only when thread successfully terminated? Kinda like:
Code:
Public Function Terminate(Optional AsSynchronous As Boolean = False) As Boolean
Edited: If you consider the above request, maybe consider adding a Synch option to other functions, like Suspend, etc. Doing so should negate the main thread from having to loop, if necessary, while waiting for the action to take effect
Last edited by LaVolpe; Apr 22nd, 2017 at 04:06 PM.
Insomnia is just a byproduct of, "It can't be done"
Hoping you will consider the synchronous functions described previously. In any case, I'd like to say this project feels pretty good and would like to provide some positive feedback after a few preliminary tests I've done:
Overview the way I understand it: The thread is purely a background thread. After the AsyncProcedure callback returns to the Thread class, the thread is automatically terminated. In other words, when the main thread's final SyncCallback routine exits, consider the thread dead. Final? Yep, as shown in posts above, you may have designed the AsyncProcedure to have multiple SyncCallback calls.
Task 1: I was thinking of using multi-threading for GDI+ loaded GIFs and other animation. The idea was to pass the thread a GDI+ image handle and frame number. The thread would prepare the frame, render to a DC with any special rendering options. The main thread would draw the frame when either the thread finished or the previous frame's duration occurred, whichever occurs latest. Afterwards, another thread created and next frame is processed. This appears to work pretty well. Main thread is not tied up during the processing of the frame. If multiple images are being animated; this is noticeable. Very good.
Task 2: Have GDI+ load an image within the background thread and return the image handle to the main thread when done. For very large images, the main thread is not tied up. Again, very good.
The key point here, for those that mess with GDI+, is that it appears the image handle can be passed between threads without crashing. I wasn't positive about that. I've tested creating the GDI+ image in the background thread and processing it in the main thread and vice versa. In either scenario the image handle was created and processed in different threads. Of course, some care needs to be taken. Accessing the image handle simultaneously in both threads would likely end in unexpected results, at best, crashes at worst; especially with multi-image formats like GIF/TIFF. More testing needed.
Last edited by LaVolpe; Apr 22nd, 2017 at 07:05 PM.
Insomnia is just a byproduct of, "It can't be done"
Hoping you will consider the synchronous functions described previously. In any case, I'd like to say this project feels pretty good and would like to provide some positive feedback after a few preliminary tests I've done:
Overview the way I understand it: The thread is purely a background thread. After the AsyncProcedure callback returns to the Thread class, the thread is automatically terminated. In other words, when the main thread's final SyncCallback routine exits, consider the thread dead. Final? Yep, as shown in posts above, you may have designed the AsyncProcedure to have multiple SyncCallback calls.
Thanks for testing. It seems like a 1.1 version is overdue.
I would also like to add a 'Argument2' in the SyncCallback to enable more flexibility out of the box without extra tricks, like array etc.
For the sync Terminate function. So like a "Public Function Terminate(Optional ByVal Wait As Boolean)"
When passing True at call the function would return only when the thread is finally terminated?
About your understanding overview. Yes, correct. If the AsyncProcedure returns the task is finished and thread is terminated automatically.
The SyncCallback can be called multiple times or none at all. That's not a condition to have it called at least once.
I would also like to add a 'Argument2' in the SyncCallback to enable more flexibility out of the box without extra tricks, like array etc.
Still may not be enough for many calls. In my GDI+ example, I may want to pass an image handle, frame number, color attributes handle and more for processing within the background thread. Dex showed how an array can be assigned to the variant argument for passing multiple values & I feel it's a good workaround. But at least two parameters would be better, something as you described in your post #13. This way SyncCallback can be better written by the user by testing the "command" parameter passed from AsyncProcedure.
Regarding raised events. Within VB, these are blocked when a modal form/window (i.e., MsgBox) is displayed within the IDE. An implementation vs withevents would solve that issue. Is this a real concern? Yes, possibly. In my GDI+ example, if the thread creates the image handle then calls the SyncCallback to pass the handle and the event is blocked, the main thread doesn't know about the handle and cannot free/release it. Of course, a workaround would be to require the SyncCallback to pass a flag within the arguments to determine if the call was received from AsyncProcedure. However, instead of testing every call, an implementation would be a better solution. Example of event blocking, the debug statement is not executed...
Code:
Private Sub Command1_Click()
BackgroundWorker.Create
MsgBox "Block Events"
End Sub
Private Sub BackgroundWorker_AsyncProcedure(ByVal SyncCallback As VBMThread10.IThreadSyncCallback)
SyncCallback.Raise 0
End Sub
Private Sub BackgroundWorker_SyncCallback(Argument As Variant)
' ^^^ This is blocked when modal window exists
Debug.Print "syncCallback received'
End Sub
When passing True at call the function would return only when the thread is finally terminated?
Yes, that's the idea & similar for Suspend
Last edited by LaVolpe; Apr 23rd, 2017 at 10:29 AM.
Insomnia is just a byproduct of, "It can't be done"
Regarding raised events. Within VB, these are blocked when a modal form/window (i.e., MsgBox) is displayed within the IDE. An implementation vs withevents would solve that issue. Is this a real concern? Yes, possibly. In my GDI+ example, if the thread creates the image handle then calls the SyncCallback to pass the handle and the event is blocked, the main thread doesn't know about the handle and cannot free/release it. Of course, a workaround would be to require the SyncCallback to pass a flag within the arguments to determine if the call was received from AsyncProcedure. However, instead of testing every call, an implementation would be a better solution. Example of event blocking, the debug statement is not executed...
Code:
Private Sub Command1_Click()
BackgroundWorker.Create
MsgBox "Block Events"
End Sub
Private Sub BackgroundWorker_AsyncProcedure(ByVal SyncCallback As VBMThread10.IThreadSyncCallback)
SyncCallback.Raise 0
End Sub
Private Sub BackgroundWorker_SyncCallback(Argument As Variant)
' ^^^ This is blocked when modal window exists
Debug.Print "syncCallback received'
End Sub
I could imagine the following solution. To have either way.
Include in the Thread class a 'CustomIThreadSyncCallback' property. When this is Nothing fire normal event, else use the interface.
So in Form you include IThreadSyncCallback and you then would do a "Set Thread.CustomIThreadSyncCallback = Me", where 'Me' is the Form.
That may be a workaround. However, if you are going to update this library, suggest using an Interface instead of raising events. I believe that is a better and cleaner overall solution. The form/uc/class that is creating the thread can pass itself to the thread's Create method and the Terminate method can release the reference. The raiseevent calls would be replaced by calling the implementation instead.
Insomnia is just a byproduct of, "It can't be done"
But the AsyncProcedure can stay as an event?
So in the Create method there would be an additional non-optional IThreadSyncCallback parameter by which 'Me' will be normally passed.
Or else a second interface is needed for the Async part.
Krool, I don't have the deep understanding of multi-threading that you do. Is there reason why you need to RaiseEvent vs. an Implementation? The idea I was suggesting is something like this. It is rough, not polished:
Code:
' new Interface to be used by customers. Let's say it's named: IThread
Public Sub AsyncProcedure(ByVal SyncCallback As IThreadSyncCallback): End Sub
Public Sub SyncCallback(ByRef Argument As Variant): End Sub
' the Thread.Cls
add this: Private m_Consumer As IThread
change Create to: Public Function Create(Owner As IThread) As Boolean
Set m_Consumer = Owner ' validate that Nothing was not passed
' then instead of raising the two events, this:
m_Consumer.SyncCallback Argument
and m_Consumer.AsyncProcedure SyncCallback
add this in the Terminate event:
Set m_Consumer = Nothing
The caller would use: Implements IThread
and then initiate with: BackgroundWorker.Create Me
Edited: Many of the changes you make may break binary compatibility, unless you create new function names, i.e., CreateEx instead of modifying the Create function signature.
Last edited by LaVolpe; Apr 23rd, 2017 at 11:01 AM.
Insomnia is just a byproduct of, "It can't be done"
Also I might include an optional 'ByVal CustData As Long' in the Create method which will be passed along AsyncProcedure.
Good idea. This way if the AsyncProcedure has no other reason to call SyncCallback, it doesn't need to and the task can be completely handled in the background thread as long as the user has a way to confirm that the task succeeded (exit code or some other method). Or Maybe the AsyncProcedure calls SyncCallback to pass success or failure?
Last edited by LaVolpe; Apr 23rd, 2017 at 11:12 AM.
Insomnia is just a byproduct of, "It can't be done"
Edited: Many of the changes you make may break binary compatibility, unless you create new function names, i.e., CreateEx instead of modifying the Create function signature.
I will need to break binary compatibility anyhow as I will remove some events, thus 1.1 version.
Ofcourse then I need to create a new manifest resource. But it's cleaner that way instead of bloating everything up.
One more, minor suggestion:
Change AsyncProcedure name to BkgThreadProcedure
Change SyncCallback name to MainThreadProcedure
Or some other names that may be more intuitive/descriptive of what is occurring in the event. It may be more helpful to users to better understand which event belongs to the multi-thread and which belongs to the main thread. Just a thought.
Insomnia is just a byproduct of, "It can't be done"
With the events interface it is possible to add several new threads code to the same form (or same instance of a class module), but with the version using implements, for also enabling that possibility, it would have to have an additional parameter to identify the thread, I think.
Edit: it can be the thread's name, so the code to create the thread, intead of:
Code:
BackgroundWorker.Create
it could be:
Code:
CreateThread "BackgroundWorker"
Last edited by Eduardo-; Apr 23rd, 2017 at 12:00 PM.
With the events interface it is possible to add several new threads code to the same form (or same instance of a class module), but with the version using implements, for also enabling that possibility, it would have to have an additional parameter to identify the thread, I think.
Good point. There are at least 2 ways of handling that
1. As you suggest, Krool could add a Key parameter to the Create event and that value passed with each implemented method. The user can Select Case / If:Else on that key. Many solutions use a key or user param to identify which instance is being used.
2. Create a class for each type of background thread and have the class Implement the interface, then pass the class instance instead of the form instance.
Insomnia is just a byproduct of, "It can't be done"
I also have the idea of bringing up a 'ThreadCancelBoolean' class which has a Value Boolean property (default property) that is passed along 'AsyncProcedure'.
Kind of "AsyncProcedure(SyncCallback As IThreadSyncCallback, Cancel As ThreadCancelBoolean)
In the Thread class there would be a Cancel method to make a 'soft terminate'.
So in the AsyncProcedure there could be an "If Cancel = True" be included by the coder to bring the AsyncProcedure faster to it's end.
Concerning the description. I do find Async.. and Sync.. already as self-explained? Or?
If passing a string is not much overhead, I suggest to identify each threads with a name.
Code:
Implements IThreads
Private Sub Form_Load()
CreateThread "BackgroundWorker1"
CreateThread "BackgroundWorker2"
End Sub
Private Sub IThread_AsyncProcedure(TheadName As String, asSyncCallback As IThreadSyncCallback, Cancel As ThreadCancelBoolean)
End Sub
Private Sub IThread_SyncCallback(TheadName As String, Argument1 As Variant, Argument2 As Variant)
End Sub
And about the names, I vote for Lavolpe's suggestion of BkgThreadProcedure and MainThreadProcedure. I think they are more intuitive.
Last edited by Eduardo-; Apr 23rd, 2017 at 12:17 PM.