Getting started with the parallel world

I mentioned multithreaded programming a lot in the previous chapter, so it is not hard to guess where that path is taking me next. Multithreaded programming, or multithreading, is the art of running multiple parts of your program at the same time.

Multithreading is definitely not my first choice for improving existing code. It is hard to write multithreaded programs, and very simple to introduce problems that are then hard to find. Multithreaded programs are also very hard to debug.

To understand multithreading we should first know the processes and threads. In general terms, a process equates to a running program. A process encompasses application code, loaded in memory, and all the resources (memory, files, windows, and so on) used by the program.

A thread, on the other hand, represents a state of the program's execution. A thread is nothing more than the current state of the CPU registers, variables local to the thread (threadvar statement introduces them), and the stack belonging to this thread. (Actually, the stack is part of the process' memory, and the thread only owns the stack pointer register.)

Every process starts with one thread that makes the process execute. We call it a main thread. Additional threads that are created from the code are called background threads.

I said before and I'll repeat it here—adding multithreading to your code can create more problems than it is worth. That's why you should know about the problems before you learn how to do multithreading.

The first rule in Delphi is always: Never access the UI from a background thread. This is so important that I'll repeat it again.

Never access the user interface from a background thread!

If the code is running in a background thread, and definitely needs to access the user interface, you have to use mechanisms that take a part of the code (an anonymous method) and execute it in the main thread. I'm talking about TThread.Synchronize and TThread.Queue.

The next problem appears when two threads are accessing the same data structure at the same time. It is not really surprising that you should not read from a TList when another thread is deleting elements from the same list, but it may shock you to know that problems can also appear when one thread is reading from a simple int64 variable while another thread is writing to it.

Don't think that the first example (simultaneous access to a list) is purely theoretical. I recently fixed a similar example in my code (yes, it happens to the best of us). One thread was reading from a TFileStream and sending data to a web server. Another thread calculated the progress, and in that calculation called the TFileStream.Size function (they were different threads because I was using the asynchronous mode of the WinHTTP API). In my defense, I know very well that it is not a good idea, but, well, mistakes happen.

What's the problem with calling Size? Its implementation. When you read from Size, the GetSize method (shown following) calls Seek three times. Imagine what happened when one thread read from a stream and another changed the current position at the same time:

function TStream.GetSize: Int64;
var
Pos: Int64;
begin
Pos := Seek(0, soCurrent);
Result := Seek(0, soEnd);
Seek(Pos, soBeginning);
end;

To solve such issues, we typically introduce locking. This technique creates a sort of barrier that allows only one thread at a time to enter a critical path through the code. Critical sections are the standard locking mechanism, but we can also use mutexes and semaphores, although both are slow and more useful when synchronizing multiple processes.

Delphi also offers two mechanisms that are slightly faster than critical sections—TMonitor and TSpinLock. In situations when threads are mostly reading from shared data and rarely writing to it, the badly-named TMultiReadExclusiveWriteSynchronizer can be used. You can also refer to it with an alias, which is much simpler to type—TMREWSync. It's just too bad that the implementation of this mechanism is terribly slow and that pure critical sections typically operate faster.

When you need to use locking, you should use TMonitor or TSpinlock.

Locking techniques introduce at least two problems to the program. They lead to slower operations, and they can result in deadlocks when improperly used. In a deadlocked program, one thread is waiting on another thread, which is waiting on the first one, and all operation stops.

An alternative to locking is interlocked (atomic) operations. They are faster than locking but only support a small range of operations—incrementing and decrementing a memory location, modifying a memory location, and conditionally modifying a memory location.

The latest variation, TInterlocked.CompareExchange, can be used to implement optimistic initialization. This mechanism can be used to safely create a shared object or interface. The latter is preferred as it will be safely destroyed at the right time.

Instead of using one of the synchronization techniques described previously, you can introduce communication mechanisms to the program. Instead of accessing a shared value, you can use data duplication (sending a copy of full or partial input data to each worker thread) and aggregation (putting partial results calculated in threads together). Each thread can then work on its own copy of data and doesn't have to use any locking to access it.

Communication is better than synchronization!

Standard techniques to implement messaging in a Delphi program are Windows messages, TThread methods Synchronize and Queue, and polling in connection with a thread-safe message queue, such as TThreadedQueue<T>.

The best way to find problems in a multithreaded application is to do stress-testing, repeating multithreaded operations multiple times, and testing your program on only one CPU core (Windows API SetProcessAffinityMask will help you with that). You'll be surprised how many problems can be found this way. If at all possible (the implementation may prevent you from doing this), you should also test the code by creating more worker threads/tasks than there are CPU cores in the computer.

Use automated tests as much as possible and you'll be able to trust your code more. Never trust the multithreaded code fully. It actually never fully works; it's just that the bugs are so deeply hidden that they almost never appear.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.125.116