📜 ⬆️ ⬇️

OmniThreadLibrary library - simple multithreading in Delphi environment

It is very difficult to write an interesting article on a technical topic. We have to balance between not slipping into the technical jungle and not saying anything at all. Today I will try in general terms (without details) to talk about how things are going with the development of multi-threaded desktop applications in the not so popular for today, but probably familiar to many Russian developers Delphi environment. The article is focused on NOT newbies in programming, who are new to the development of multi-threaded applications.


The topic covered in the title is very extensive. Everything that will be written below is not even the tip of the iceberg, it is rather a flight at an altitude of 10,000 meters above the ocean in which these icebergs float. Why write such an article? Rather, in order to draw attention to the wide opportunities that have long been available, but which for some reason, many are afraid and shun.

Why Delphi?


I have been programming in Delphi for a very long time and I never stop enjoying it. It is in many ways a wonderful language. Its uniqueness is that it simultaneously allows you to create code of arbitrarily high level, while remaining “close to hardware”, since at the output, we get a native application, not code for a Java or .Net virtual machine. And at the same time, the Delphi language is very simple and concise, the code on it is pleasant to read and it is easy enough to figure out what I cannot say about the code in C or C ++ (with all my great respect for C developers, although someone will say that this is just a matter of habit).
Currently, Delphi has lost its former popularity. This was probably due to the fact that in the 2000s this product was practically abandoned by developers for several years, as a result of which it dropped out of the competitive race of development environments for a while. Indeed, after Delphi 7, released by Borland in 2002, a more or less stable product appeared only in 2007. It was CodeGear Delphi 2007, released by CodeGear, a subsidiary of Borland. All versions between Delphi 7 and Delphi 2007 were practically unusable. In 2008, Borland sold CodeGear to Embarcadero Technologies, which (for which she thanks a lot!) Immediately began to turn what it got into a modern, high-quality development environment. The current version of Delphi at the time of this writing is Embarcadero Delphi XE2, released in September 2011. Due to the relatively high quality of the latest versions of Delphi, this development environment gradually regains lost ground.
')

Why do we need multithreading?


People wanted to perform several tasks on a computer at the same time. This is called multitasking. Implements multitasking by means of the operating system. But if the OS can perform several applications at the same time, why should not one application within itself also perform several tasks at once. For example, when archiving a large list of files, the archiver can simultaneously read the next file, at this time archive the current read in memory and write the result to the output file on disk. Those. Instead of performing “read” -> “archive” -> “write result to disk” on each file in one stream, you can run 3 streams, one of which will read files into memory, the second stream - archive, and the third is to save to disk. Another example is the execution of a low-priority task in the background — for example, background backup of a file opened in a text editor.
If processors continued to increase their clock speed at the same pace as it did in the 90s and early 2000s, one would not bother with multithreading and continue to write the classic single-threaded code. However, in recent years, processors have ceased to actively increase the speed of a single core, but they have begun to increase the number of these cores themselves. To use the potential of modern processors for 100% without multithreading just can not do.

Why is it difficult to write multi-threaded code?


1) It is easy to make a mistake.
When several applications are running on a computer at the same time, the address space (memory) of each process is reliably isolated from other processes by the operating system and it is quite difficult to get into a foreign address space. With threads within a single process, the opposite is true - they all work with the common address space of the process and can change it arbitrarily. Therefore, in a multi-threaded application, you have to independently implement memory protection and thread synchronization, which leads to the need to write relatively complex, but not carrying code payload. This code is called the "boilerplate" (griddle), because the griddle must first be prepared before you start frying something on it. It is the need to write a “non-standard” boilerplate-code that hinders the development of multi-thread computing. There are many special mechanisms for thread synchronization: interlocked processor commands, operating system synchronization objects (critical sections, mutexes, semaphores, events, etc.), spin locks, etc.
2) The code of a multithreaded application is difficult to analyze.
One of the difficulties of a multi-threaded application is that visually not looking at the code of a multi-threaded application is it clear whether a particular method can be called (or is called) from different threads. Those. you have to keep in mind which methods can be called from different threads, and which not. Since making absolutely all methods thread-safe is not an option, there is always a chance to run into an error by calling a method that is not thread-safe from several threads.
3) A multi-threaded application is difficult to debug.
In a multithreaded application, many errors can occur when a certain state of concurrently executing threads (as a rule, with a sequence of commands executed in different threads). An interesting example is described here (http://www.thedelphigeek.com/2011/08/multithreading-is-hard.html). To recreate such a situation artificially is often very difficult, almost unreal. In addition, there are not very many tools for debugging multi-threaded applications in Delphi, Visual Studio is the clear leader in this regard.
4) In a multithreaded application, it is difficult to handle errors.
If an application has a graphical user interface, only one stream can interact with the user. Usually, when an error occurs in an application, we either process it inside the application or display a message to the user. If the error occurs in the additional stream, it cannot say anything to the user "immediately." Accordingly, it is necessary to save the error that occurred in the additional stream until it is synchronized with the main stream and only then issue it to the user. This can lead to a relatively complex and confusing code structure.

Is there any way to simplify your life a little?


I present to you OmniThreadLibrary (abbreviated OTL). OmniThreadLibrary is a library for creating multi-threaded applications in Delphi. Its author, Primoz Gabrijelcic from Slovenia, is an unsurpassed professional with many years of experience in developing applications in Delphi. OmniThreadLibrary is an absolutely free open source library. At the moment the library is already in a rather mature stage and is quite suitable for use in serious projects.

Where can I find information on OTL?
Also, the author of the library is currently engaged in filling the wiki-book about OmniThreadLibrary and multithreading, articles about most of the high-level OTL primitives are already ready.

What features does OTL provide?


This library contains low-level and high-level classes that allow you to easily manage multithreading, without going into details of the processes of creating / releasing / synchronizing threads at the WinAPI level.
Of particular interest are high-level primitives for simplified multi-threaded control. They are remarkable in that they are relatively easy to integrate into a single-threaded ready-made application, almost without changing the structure of the source code. These primitives allow you to create multi-threaded applications, concentrating on the useful application code, and not on the auxiliary code for multithreading control.
The main high-level primitives include Future (asynchronous function), Pipeline (pipeline), Join (parallel call of several methods), ForkJoin (recursion with parallelism), Async (asynchronous method), ForEach (parallel loop).
In my opinion, the most interesting and useful primitives are Future and Pipeline, because for their use, the existing code almost does not need to be rewritten.

Future


This primitive allows you to make an asynchronous function call and at the right time wait for the completion of the calculation and get the result of the execution. With the help of this primitive, the call of any procedure or function can be turned into asynchronous painlessly.
It looks like this:

uses OtlParallel; ... procedure TestFuture; var vFuture: IOmniFuture<integer>; begin //      vFuture := Parallel.Future<integer>( function: integer var i: integer; begin Result := 0; for i := 1 to 100000 do Result := Result + i; end ); //   -     (      (    ,    ,        ) //     ,     ShowMessage(IntToStr(vFuture.Value)); end; 


Pay attention that the reference to vFuture.Value is the moment of synchronization of the main thread with the additional, i.e. until we turn to Value, we don’t know anything at all about the state of the other thread. As soon as we call Value, the main thread is suspended until the completion of the calculation in the additional thread.

If required, you can implement nonblocking wait for the result in the main thread:
 while not vFuture.IsDone do Application.ProcessMessages; 

Thus, the Future primitive allows you to perform some task asynchronously and return the result to the main thread exactly at the moment it is needed there.

Pipeline


Pipeline is a much more powerful primitive compared to Future.
Imagine that a certain algorithm is executed in a loop for a set of elements. For example, some processing of files in the directory is performed. A single-threaded program will take another file, read it, perform some actions and save the modified file to disk. Having a pipeline, the original algorithm can be divided into stages (reading, processing, saving) and run these stages in parallel threads. At the very beginning, only the very first stage will start and read the first file. As soon as the reading is completed, the second stage will start and will start processing the read file or its portions (if the first stage reads the files not entirely but in portions). At this time, the first stage will begin to read the second file. As soon as the second stage processes the first file, the third stage will connect and start saving. At this moment we will get a state in which all three stages work in parallel.
An example for Pipeline, close to real life, would be too loaded an article, therefore, to illustrate the use of Pipeline, I limit myself to a copy of an absolutely synthetic example from OtlBook (don't mind it!):

 uses OtlCommon, OtlCollections, OtlParallel; var sum: integer; begin sum := Parallel.Pipeline .Stage( procedure (const input, output: IOmniBlockingCollection) var i: integer; begin for i := 1 to 1000000 do output.Add(i); end) .Stage( procedure (const input: TOmniValue; var output: TOmniValue) begin output := input.AsInteger * 3; end) .Stage( procedure (const input, output: IOmniBlockingCollection) var sum: integer; value: TOmniValue; begin sum := 0; for value in input do Inc(sum, value); output.Add(sum); end) .Run.Output.Next; end; 

In this example, the first stage generates a million numbers, passing them one by one to the next stage. The second stage multiplies each number by 3 and transfers to the third stage. The third stage summarizes the results and returns a single number. Each Stage is executed in its thread. Moreover, Otl allows you to specify how many threads for each Stage to use (if one is small) due to the simple modifier .NumTasks (N). OTL's capabilities are really very wide.

The base class for supporting data exchange between pipeline stages is the thread-protected queue class - TOmniBlockingCollection. This class allows multiple threads to simultaneously add and read items. The high speed of the collection is achieved by tricky memory management and the use of locks based on thread-safe processor instructions instead of locks based on OS synchronization objects. You can read about the implementation details of the TOmniBlockingCollection class here , here and here .

Conclusion


Someone looking at the above examples will say "yes, I've already seen it all." Indeed, the Task Parallel Library for .Net Framework 4 contains approximately the same classes. At the same time, there are a number of differences between the way the threads inside the .Net machine are executed and how the threads are executed on the real processor. Consideration of these differences is beyond the scope of this article. I just wanted to focus on a great library, and the wide possibilities it provides to Delphi developers. I want to note that the library is equipped with a large number of examples illustrating the use of both low-level and high-level classes.

In order to dispel concerns about the maturity and reliability of this library, I will only say that by using Pipeline in a complex commercial multi-user application (not the web), it was possible to reduce the time to perform operations on a group of files on a client by almost half due to exploding file processing on separate threads on the client and their transfer to the server. Whether to use a bunch of Delphi + OmniThreadLibrary in your projects is up to you;)

Source: https://habr.com/ru/post/144681/


All Articles