Hello everyone! Many people wonder multithreaded programs written in VB6. Write multithreaded programs in VB6 quite real, I have many examples that I also published in my blog, but there are restrictions, one way or another can be circumvented. I consider this question in this post will not, and will consider more correct (in terms of programming in VB6) method of of multithreading - using objects. In this method, there are no restrictions, unlike threading Standart EXE, and has all the advantages of OOP. Also, I hasten to note that the IDE is not intended for debugging multithreaded programs, so to debug such programs in the IDE will not work. For debugging I use another debugger. You can also debug streams separately, and then collect the EXE.
Using multiple threads, we have the ability to call methods asynchronously while maintaining synchronicity; ie we can call methods as well as in a separate thread, and in his. For example methods require large computational load should cause asynchronously and receive, at the end of the notice in the form of events. Such methods (properties) that work fast, you can call synchronously.
One of the problems create a thread on VB6 in Standart EXE, is the inability to use WinAPI calls functions through Declare. Unlike the functions declared in a type library and entering the import, Declared-function after each call to set the properties of the object variable Err.LastDllError. This is done by calling the function __vbaSetSystemError of MSVBVM. Object Err, is thread-dependent, and the reference to it is in the thread local storage (TLS). For each thread must create its own object Err, otherwise the function call __vbaSetSystemError, runtime inquiry link from the storage, and we have it is not there (or rather there is 0) and will read the wrong address, as a consequence of crash.
To prevent this behavior, you can declare a function in tlb, then the function will not be called __vbaSetSystemError. You can also initialize the Err object, create an object instance of the DLL in the new thread, then the runtime initializes the object itself. But to create a new object, you must first initialize the thread to work with COM, it needs to call CoInitialize (Ex), but we can not call functions. It is possible to declare a tlb (it only one), then all is fair; it can also be called from assembler code or in any other way. I always go to another. Why do I LastDllError? I can just simply call GetLastError himself when I need to. So I just find the address of the function __vbaSetSystemError and write the first instruction output from the procedure (ret). This is certainly not so nice, but reliably and quickly. You can have only one function CoInitialize, and then restore __vbaSetSystemError.
Now we can call Declared-function in a new thread, which gives us endless possibilities. After creating the object (CreateObject), we can call its methods, properties, events receive from him, etc., but just a link between streams can not be passed because errors can occur because of concurrent access to data, etc. To send a link exists between threads marshaling. We will use the universal marshaller, because we ActiveX DLL has a type library. The principle of work, I will not describe in detail, it has a lot of articles online. The general sense is that instead of a direct call to the object, the RPC request to another computer / process / thread. For processing queries need to use the message loop, and once it happened, then the communication between threads is done through the posts.
To test, I wrote a simple ActiveX DLL that lets you download a file from a network that has several methods and generates events.
The code basically simple, if you read the description of the API functions. When calling the method "Download", starts will download from time to time (depending on the size of the buffer) event is generated Progress. If an error occurs, an event "Error", and at the end of the "Complete". "BufferSize" - sets the size of the buffer, which is generated when filling event. Demo code and contains bugs.*
Class I named "MultithreadDownloader", and the library "MTDownloader", respectively ProgID of the object - "MTDownloader.MultithreadDownloader". After compiling obtain a description of the interfaces through OleView, PEExplorer etc. In my example, CLSID = {20FAEF52-0D1D-444B-BBAE-21240219905B}, IID = {DF3BDB52-3380-4B78-B691-4138300DD304}. I also put a check "RemoteServerFiles" to get the output type library for our DLL, and will connect it instead of DLL for guaranteed start of the application.
Last edited by The trick; May 4th, 2019 at 05:18 AM.
We examine in detail the code. When loading forms (Form_Load), we patch the runtime error to exclude the use Declared functions in an uninitialized flow (RemoveLastDllError). The principle I described above. If we create an object in another thread, we need to somehow check in the main thread, whether to create the object. For this, I use a simple synchronization objects - an event with manual reset. Initialize it in a reset state. Then create a stream function ThreadProc, as a parameter to pass the structure of sync events and links to the stream object (Stream), which need to be marshaled. This object returns a reference to marshal a pointer to the object. If successful, forward trigger events (WaitForSingleObject). On this main thread suspends execution until we establish event hEvent. In a new thread first initialize COM (CoInitialize), translate the CLSID and IID in binary form, create an object (CoCreateInstance). Here, if you do not need error handling, you can use:
In this code to create an object I used CoCreateInstance, as before the creation of the first object we can not include error handling (the reason described above), after the creation of the first object can be further create objects through "CreateObject". If the error handling is not necessary, you can immediately use CreateObject. If successful, marshals for this call the "CoMarshalInterThreadInterfaceInStream", which writes the Stream (stream) information to create a proxy object in another thread. Set the event, thereby indicate the main flow of the initialization was successful. On failure, and set the event and perform deinitialization "COM" in the stream output (flow completed). A sign of a successful initialization becomes a pointer to the IStream. Further, in this stream to enter the standard loop. Because we have established event, the main flow and wakes up, we check whether successfully passed the initialization. If "IStream" contains a pointer, then all is well, otherwise an error. Then get a pointer to a proxy object from the stream by calling "CoGetInterfaceAndReleaseStream", thus also release the object "Stream". Assign our object variable, subscribe to the event, a pointer to a proxy object. All these manipulations we can now access the object in another thread and receive events from it. Check whether correctly initialized object itself (hInternet <> 0), and set the buffer size to 64 KB, the information will be updated when the next batch of uploaded data in 64 KB. The initialization is completed. To it was impossible to perform several queries on the download, we will synchronize requests to create an event. Otherwise, if just a few clicks on the button Download, the data will be downloaded sequentially, if mistakenly press 2 times, then the file will be downloaded 2 times and overwritten, the error will not. When pressed we check the status of the event, if it is established that download in the moment. To transfer data to another stream, perform transportation (marshalling) parameters in the other thread (MT_DOWNLOAD_packParam). To do this, allocate memory on the heap and copy the data (in this case the URL and FileName) into it, and give a link to the newly created thread. I decided to save the easiest way - 2 unicode-strings series with a trailing null terminals. A reference to the parameters in turn flow through PostThreadMessage, as the number of the message using the first free identifier WM_APP, which I called WM_MT_DOWNLOAD. In another thread in the cycle, when receiving a message WM_MT_DOWNLOAD, take out the parameters of the heap and invokes the Download, pre-reset event hEvent. All. While a method we can not call him again, and thanks marshaled we receive notification from the object as the events in the main stream. Event handlers elementary and self-explanatory. The only thing that I want to add that the file size I selected Currency, as 64-bit integers no, but it's almost Currency same, only divided by 1000010.
In addition to the asynchronous calls have also remains the possibility of a synchronous call, that is, in the form code can legitimately write "Downloader.Download URL, FileName". You can compare the advantages and disadvantages of synchronous and asynchronous calls.* Example does not require registration of ActiveX DLL, enough to put it in the same folder thanks to the manifesto. As a result, we have a multi-threaded application that runs on any machine without prompting the admin rights.
_____________________________________________
Multithreading on VB6 real and feasibility. In this article, I described a method of creating an object in a separate thread and subscribe to its events. If we do not need to have a connection with different object streams, the code is repeatedly shortened (retracted cycle, marshaling, etc.); You can even create an object in inline assembly - that will limit the debug this code in the IDE. But all these methods I describe it some other time. Good luck to everyone!
I had previously asked the question of how to tell if threads use different CPU cores when available. To answer that question, I modified TrickMTDownloader to service 2 separate threads. I then started a download of a 180 MB jpeg, and quickly started a simple page file, with the following results.
Start 1 31833.39
Start 2 31833.82
End 2 31833.85
End 1 31833.9
Download 1 is a 180 KB file - elapsed time 510 ms
Download 2 is a 1 KB file - elapsed time 30 ms
I am assuming that this means the second download runs on a different CPU core.
I then compiled the program and ran the test again. The results were pretty much the same, indicating that network speed was the limiting factor. I have only included the test program in the attached download. To run this program you will need the registered ActiveX DLL program from the trick download.
I used arrays for most of the download variables, but I had difficulty with the Downloader object. I left most of the translated comments in place.