For what purposes are multithreaded systems used. Eight Simple Rules for Developing Multithreaded Applications

What topic raises the most questions and difficulties for beginners? When I asked my teacher and Java programmer Alexander Pryakhin about this, he immediately replied: “Multithreading”. Thanks to him for the idea and help in preparing this article!

We will look into the inner world of the application and its processes, figure out what the essence of multithreading is, when it is useful, and how to implement it - using Java as an example. If you’re learning a different OOP language, don’t worry: the basic principles are the same.

About streams and their origins

To understand multithreading, let's first understand what a process is. A process is a piece of virtual memory and resources that the OS allocates to run a program. If you open several instances of the same application, the system will allocate a process for each. In modern browsers, a separate process can be responsible for each tab.

You have probably come across the Windows "Task Manager" (in Linux it is "System Monitor") and you know that unnecessary running processes load the system, and the most "heavy" of them often freeze, so they have to be terminated forcibly.

But users love multitasking: don't feed them bread - just open a dozen windows and jump back and forth. There is a dilemma: you need to ensure the simultaneous operation of applications and at the same time reduce the load on the system so that it does not slow down. Let's say the hardware can't keep up with the needs of the owners - you need to solve the issue at the software level.

We want the processor to execute more instructions and process more data per unit of time. That is, we need to fit more of the executed code in each time slice. Think of a unit of code execution as an object — that's a thread.

A complex case is easier to approach if you break it down into several simple ones. So when working with memory: a "heavy" process is divided into threads that take up fewer resources and are more likely to deliver the code to the calculator (how exactly - see below).

Each application has at least one process, and each process has at least one thread, which is called the main thread and from which, if necessary, new ones are launched.

Difference between threads and processes

    Threads use the memory allocated for the process, and the processes require their own memory space. Therefore, threads are created and completed faster: the system does not need to allocate them a new address space each time, and then release it.

    Processes each work with their own data - they can exchange something only through the mechanism of interprocess communication. Threads access each other's data and resources directly: what one changed is immediately available to everyone. The thread can control the "fellow" in the process, while the process controls exclusively its "daughters". Therefore, switching between streams is faster and communication between them is easier.

What is the conclusion from this? If you need to process a large amount of data as quickly as possible, break it up into chunks that can be processed by separate threads, and then piece the result together. It's better than spawning resource-hungry processes.

But why does a popular application like Firefox go the route of creating multiple processes? Because it is for the browser that isolated tabs work is reliable and flexible. If something is wrong with one process, it is not necessary to terminate the entire program - it is possible to save at least part of the data.

What is multithreading

So we come to the main thing. Multithreading is when the application process is split into threads that are processed in parallel - at one unit of time - by the processor.

The computational load is distributed between two or more cores, so that the interface and other program components do not slow down each other's work.

Multi-threaded applications can be run on single-core processors, but then the threads are executed in turn: the first one worked, its state was saved - the second was allowed to work, saved - returned to the first or launched the third, etc.

Busy people complain that they only have two hands. Processes and programs can have as many hands as needed to complete the task as quickly as possible.

Wait for a signal: synchronization in multi-threaded applications

Imagine that several threads are trying to change the same data area at the same time. Whose changes will be eventually accepted and whose changes will be canceled? To avoid confusion with shared resources, threads need to coordinate their actions. To do this, they exchange information using signals. Each thread tells the others what it is doing and what changes to expect. So the data of all threads about the current state of resources is synchronized.

Basic Synchronization Tools

Mutual exclusion (mutual exclusion, abbreviated as mutex) - a "flag" going to the thread that is currently allowed to work with shared resources. Eliminates access by other threads to the occupied memory area. There can be several mutexes in an application, and they can be shared between processes. There is a catch: mutex forces the application to access the operating system kernel every time, which is costly.

Semaphore - allows you to limit the number of threads that can access a resource at a given moment. This will reduce the load on the processor when executing code where there are bottlenecks. The problem is that the optimal number of threads depends on the user's machine.

Event - you define a condition upon the occurrence of which control is transferred to the desired thread. Streams exchange event data to develop and logically continue each other's actions. One received the data, the other checked its correctness, the third saved it to the hard disk. Events differ in the way they are canceled. If you need to notify several threads about an event, you will have to manually set the cancellation function to stop the signal. If there is only one target thread, you can create an auto-reset event. It will stop the signal itself after it reaches the stream. Events can be queued for flexible flow control.

Critical section - a more complex mechanism that combines a loop counter and a semaphore. The counter allows you to postpone the start of the semaphore for the desired time. The advantage is that the kernel is only activated if the section is busy and the semaphore needs to be turned on. The rest of the time the thread runs in user mode. Alas, a section can only be used within one process.

How to implement multithreading in Java

The Thread class is responsible for working with threads in Java. Creating a new thread to execute a task means creating an instance of the Thread class and associating it with the code you want. This can be done in two ways:

    subclass Thread;

    implement the Runnable interface in your class, and then pass the class instances to the Thread constructor.

While we will not touch on the topic of deadlocks, when threads block each other's work and freeze, we will leave that for the next article.

Java multithreading example: ping pong with mutexes

If you think something terrible is about to happen, breathe out. We will consider working with synchronization objects almost in a playful way: two threads will be thrown by a mutex. But in fact, you will see a real application where only one thread can process publicly available data at a time.

First, let's create a class that inherits the properties of the Thread we already know, and write a kickBall method:

Public class PingPongThread extends Thread (PingPongThread (String name) (this.setName (name); // override the thread name) @Override public void run () (Ball ball = Ball.getBall (); while (ball.isInGame ()) (kickBall (ball);)) private void kickBall (Ball ball) (if (! ball.getSide (). equals (getName ())) (ball.kick (getName ());)))

Now let's take care of the ball. He will not be simple with us, but memorable: so that he can tell who hit him, from which side and how many times. To do this, we use a mutex: it will collect information about the work of each of the threads - this will allow isolated threads to communicate with each other. After the 15th hit, we will take the ball out of the game, so as not to seriously injure it.

Public class Ball (private int kicks = 0; private static Ball instance = new Ball (); private String side = ""; private Ball () () static Ball getBall () (return instance;) synchronized void kick (String playername) (kicks ++; side = playername; System.out.println (kicks + "" + side);) String getSide () (return side;) boolean isInGame () (return (kicks< 15); } }

And now two player threads are entering the scene. Let's call them, without further ado, Ping and Pong:

Public class PingPongGame (PingPongThread player1 = new PingPongThread ("Ping"); PingPongThread player2 = new PingPongThread ("Pong"); Ball ball; PingPongGame () (ball = Ball.getBall ();) void startGame () throws InterruptedException (player1 .start (); player2.start ();))

"Full stadium of people - time to start the match." We will officially announce the opening of the meeting - in the main class of the application:

Public class PingPong (public static void main (String args) throws InterruptedException (PingPongGame game = new PingPongGame (); game.startGame ();))

As you can see, there is nothing furious here. This is just an introduction to multithreading for now, but you already know how it works, and you can experiment - limit the duration of the game not by the number of strokes, but by time, for example. We'll come back to the topic of multithreading later - we'll look at the java.util.concurrent package, the Akka library, and the volatile mechanism. Let's also talk about implementing multithreading in Python.

Multithreaded programming is fundamentally no different from writing event-driven graphical user interfaces, or even writing simple sequential applications. All the important rules governing encapsulation, separation of concerns, loose coupling, etc. apply here. But many developers find it difficult to write multithreaded programs precisely because they neglect these rules. Instead, they try to put into practice the much less important knowledge about threads and synchronization primitives, gleaned from the texts on multithreading programming for beginners.

So what are these rules

Another programmer, faced with a problem, thinks: "Oh, exactly, we need to apply regular expressions." And now he already has two problems - Jamie Zawinski.

Another programmer, faced with a problem, thinks: "Oh, right, I'll use streams here." And now he has ten problems - Bill Schindler.

Too many programmers who undertake to write multi-threaded code fall into the trap, like the hero of Goethe's ballad " The Sorcerer's Apprentice". The programmer will learn how to create a bunch of threads that, in principle, work, but sooner or later they get out of control, and the programmer does not know what to do.

But unlike a wizard-dropout, the unfortunate programmer cannot hope for the arrival of a powerful sorcerer who will wave his wand and restore order. Instead, the programmer goes to the most unsightly tricks, trying to cope with constantly emerging problems. The result is always the same: an overly complicated, limited, fragile and unreliable application is obtained. It has a persistent threat of deadlock and other dangers inherent in bad multithreaded code. I'm not even talking about unexplained crashes, poor performance, incomplete or incorrect work results.

You may have wondered: why is this happening? A common misconception is: "Multi-threaded programming is very difficult." But this is not the case. If a multi-threaded program is unreliable, then it usually flips for the same reasons as a low-quality single-threaded program. It's just that the programmer doesn't follow the foundational, well-known and proven development methods. Multithreaded programs only seem more complex, because the more parallel threads go wrong, the more mess they make - and much faster than a single thread would.

The misconception about "the complexity of multi-threaded programming" has become widespread due to those developers who have developed professionally in writing single-threaded code, first encountered multithreading and did not cope with it. But instead of rethinking their biases and habits of work, they stubbornly fix the fact that they do not want to work in any way. Making excuses for unreliable software and missed deadlines, these people repeat the same thing: "multithreaded programming is very difficult."

Please note that above I am talking about typical programs that use multithreading. Indeed, there are complex multi-threaded scenarios - as well as complex single-threaded ones. But they are rare. As a rule, in practice nothing supernatural is required from the programmer. We move data, transform it, from time to time perform some calculations and, finally, save information in a database or display it on the screen.

There is nothing difficult about improving the average single-threaded program and turning it into a multi-threaded one. At least it shouldn't be. Difficulties arise for two reasons:

  • programmers do not know how to apply simple, well-known proven development methods;
  • most of the information presented in books on multi-threaded programming is technically correct, but completely inapplicable for solving applied problems.

The most important programming concepts are universal. They apply equally to single-threaded and multi-threaded programs. Programmers drowning in a maelstrom of streams simply did not learn important lessons when they mastered single-threaded code. I can say this because such developers make the same fundamental mistakes in multi-threaded and single-threaded programs.

Perhaps the most important lesson to be learned in sixty years of programming history is: global mutable state- evil... Real evil. Programs that rely on globally mutable state are relatively difficult to reason about, and generally unreliable because there are too many ways to change state. There have been a lot of studies that confirm this general principle, there are countless design patterns, the main goal of which is to implement one or another method of data hiding. To make your programs more predictable, try to eliminate mutable state as much as possible.

In a single threaded sequential program, the likelihood of data corruption is directly proportional to the number of components that can alter the data.

As a rule, it is not possible to completely get rid of the global state, but the developer has very effective tools in his arsenal that allow you to strictly control which program components can change the state. In addition, we learned how to create restrictive API layers around primitive data structures. Therefore, we have good control over how these data structures change.

The problems of globally mutable state gradually became apparent in the late 80s and early 90s, with the proliferation of event-driven programming. Programs no longer started "from the beginning" or followed a single, predictable path of execution "to the end." Modern programs have an initial state, after exiting from which events occur in them - in an unpredictable order, with variable time intervals. The code remains single-threaded, but it becomes asynchronous. The likelihood of data corruption is increased precisely because the order of occurrence of events is very important. Situations of this kind are quite common: if event B occurs after event A, then everything works fine. But if event A occurs after event B, and event C has time to intervene between them, then the data may be distorted beyond recognition.

If parallel streams are involved, the problem is further aggravated, since several methods can simultaneously operate on the global state. It becomes impossible to judge how exactly the global state changes. We are already talking not only about the fact that events can occur in an unpredictable order, but also about the fact that the state of several threads of execution can be updated. simultaneously... With asynchronous programming, you can, at a minimum, ensure that a particular event cannot happen before another event has finished processing. That is, it is possible to say with certainty what the global state will be at the end of the processing of a particular event. In multithreaded code, as a rule, it is impossible to tell which events will occur in parallel, so it is impossible to describe with certainty the global state at any given time.

A multithreaded program with extensive globally mutable state is one of the most eloquent examples of the Heisenberg uncertainty principle I know of. It is impossible to check the state of a program without changing its behavior.

When I start another philippic about global mutable state (the essence is outlined in the previous few paragraphs), programmers roll their eyes and assure me that they have known all this for a long time. But if you know this, why can't you tell from your code? Programs are crammed with global mutable state, and programmers wonder why the code doesn't work.

Unsurprisingly, the most important work in multithreaded programming happens during the design phase. It is required to clearly define what the program should do, develop independent modules to perform all the functions, describe in detail what data is required for which module, and determine the ways of exchanging information between the modules ( Yes, don't forget to prepare nice T-shirts for everyone involved in the project. First thing.- approx. ed. in the original). This process is not fundamentally different from designing a single-threaded program. The key to success, as with single-threaded code, is to limit interactions between modules. If you can get rid of the shared mutable state, the data sharing problems simply won't arise.

Someone might argue that sometimes there is no time for such a delicate design of the program, which will make it possible to do without the global state. I believe that it is possible and necessary to spend time on this. Nothing affects multithreaded programs as destructively as trying to cope with global mutable state. The more detail you have to manage, the more likely your program will peak and crash.

In realistic applications, there must be some kind of shared state that can change. And this is where most programmers start having problems. The programmer sees that a shared state is required here, turns to the multithreaded arsenal and takes from there the simplest tool: a universal lock (critical section, mutex, or whatever they call it). They seem to believe that mutual exclusion will solve all data sharing problems.

The number of problems that can arise with such a single lock is staggering. Consideration must be given to race conditions, gating problems with overly extensive blocking, and allocation fairness issues are just a few examples. If you have multiple locks, especially if they are nested, you will also need to take action against deadlocks, dynamic deadlocks, lock queues, and other concurrency threats. In addition, there are inherent single blocking problems.
When I write or review code, I have a near-fail-safe rule of thumb: if you made a lock, then you apparently made a mistake somewhere.

This statement can be commented in two ways:

  1. If you need locking, then you probably have global mutable state that you want to protect against concurrent updates. The presence of global mutable state is a flaw in the application design phase. Review and redesign.
  2. Using locks correctly is not easy, and localizing bugs related to locking can be incredibly difficult. It is very likely that you will be using the lock incorrectly. If I see a lock, and the program behaves in an unusual way, then the first thing I do is check the code that depends on the lock. And I usually find problems in it.

Both of these interpretations are correct.

Writing multi-threaded code is easy. But it's very, very difficult to use synchronization primitives correctly. You may not be qualified to use even one lock correctly. After all, locks and other synchronization primitives are constructs erected at the level of the entire system. People who understand concurrent programming much better than you use these primitives to build concurrent data structures and high-level synchronization constructs. And you and I, ordinary programmers, just take such constructions and use them in our code. An application programmer should not use low-level synchronization primitives more often than he makes direct calls to device drivers. That is, almost never.

Trying to use locks to solve data sharing problems is like putting out a fire with liquid oxygen. Like a fire, such problems are easier to prevent than to fix. If you get rid of the shared state, you don't have to abuse the synchronization primitives either.

Most of what you know about multithreading is irrelevant

In the tutorials on multithreading for beginners, you will learn what threads are. Then the author will begin to consider various ways in which you can establish the parallel operation of these threads - for example, talk about controlling access to shared data using locks and semaphores, dwell on what things can happen when working with events. Will take a close look at condition variables, memory barriers, critical sections, mutexes, volatile fields, and atomic operations. We will look at examples of how to use these low-level constructs to perform all sorts of system operations. Having read this material to half, the programmer decides that he already knows enough about all these primitives and their use. After all, if I know how this thing works at the system level, I can apply it the same way at the application level. Yes?

Imagine telling a teenager how to assemble an internal combustion engine by himself. Then, without any training in driving, you put him behind the wheel of a car and say, "Go!" The teenager understands how a car works, but has no idea how to get from point A to point B on it.

Understanding how threads work at the system level usually doesn't help in any way at the application level. I am not suggesting that programmers do not need to learn all these low-level details. Just don't expect to be able to apply this knowledge right off the bat when designing or developing a business application.

The introductory threading literature (and related academic courses) should not explore such low-level constructs. You need to focus on solving the most common classes of problems and show developers how these problems are solved using high-level capabilities. In principle, most business applications are extremely simple programs. They read data from one or more input devices, perform some complex processing on this data (for example, in the process, they request some more data), and then output the results.

These programs often fit perfectly into the provider-consumer model, which requires only three threads:

  • the input stream reads the data and puts it on the input queue;
  • a worker thread reads records from the input queue, processes them, and puts the results into the output queue;
  • the output stream reads entries from the output queue and stores them.

These three threads work independently, communication between them occurs at the queue level.

While technically these queues can be thought of as zones of shared state, in practice they are just communication channels in which their own internal synchronization operates. Queues support working with many producers and consumers at once, and you can add and remove items to them in parallel.

Since the input, processing, and output stages are isolated from each other, their implementation can be easily changed without affecting the rest of the program. As long as the type of data in the queue does not change, you can refactor individual program components at your discretion. In addition, since an arbitrary number of suppliers and consumers participate in the queue, it is not difficult to add other producers / consumers. We can have dozens of input streams writing information to the same queue, or dozens of worker threads taking information from the input queue and digesting data. Within the framework of a single computer, such a model scales well.

Most importantly, modern programming languages ​​and libraries make it very easy to create producer-consumer applications. In .NET, you will find Parallel Collections and the TPL Dataflow Library. Java has the Executor service as well as BlockingQueue and other classes from the java.util.concurrent namespace. C ++ has a Boost threading library and Intel's Thread Building Blocks library. Microsoft's Visual Studio 2013 introduces asynchronous agents. There are similar libraries in Python, JavaScript, Ruby, PHP and, as far as I know, in many other languages. You can create a producer-consumer application using any of these packages, without ever having to resort to locks, semaphores, condition variables, or any other synchronization primitives.

A wide variety of synchronization primitives are freely used in these libraries. This is fine. All of these libraries are written by people who understand multithreading incomparably better than the average programmer. Working with such a library is practically the same as using a runtime language library. It can be compared to programming in a high-level language rather than assembly language.

The supplier-consumer model is just one of many examples. The above libraries contain classes that can be used to implement many of the common threading design patterns without going into low-level details. It is possible to create large-scale multithreaded applications without worrying about exactly how the threads are coordinated and synchronized.

Work with libraries

So, creating multi-threaded programs is not fundamentally different from writing single-threaded synchronous programs. The important principles of encapsulation and data hiding are universal and only grow in importance when multiple concurrent threads are involved. If you neglect these important aspects, then even the most comprehensive knowledge of low-level threading will not save you.

Modern developers have to solve a lot of problems at the level of application programming, it happens that there is simply no time to think about what is happening at the system level. The more intricate applications get, the more complex details have to be hidden between API levels. We have been doing this for more than a dozen years. It can be argued that the qualitative hiding of the complexity of the system from the programmer is the main reason why the programmer is able to write modern applications. For that matter, aren't we hiding the complexity of the system by implementing the UI message loop, building low-level communication protocols, etc.?

The situation is similar with multithreading. Most of the multi-threaded scenarios that the average business application programmer might encounter are already well known and well implemented in libraries. The library functions do a great job at hiding the mind-boggling complexity of parallelism. You need to learn how to use these libraries in the same way that you use libraries of user interface elements, communication protocols, and numerous other tools that just work. Leave the low-level multithreading to the specialists - the authors of the libraries used in the creation of applications.

E This article is not for seasoned Python tamers, for whom unraveling this ball of snakes is child's play, but rather a superficial overview of multithreading capabilities for newly addicted python.

Unfortunately, there is not so much material in Russian on the topic of multithreading in Python, and pythoners who had not heard anything, for example, about the GIL, began to come across to me with enviable regularity. In this article I will try to describe the most basic features of multithreaded python, I will tell you what the GIL is and how to live with it (or without it), and much more.


Python is a charming programming language. It perfectly combines many programming paradigms. Most of the tasks that a programmer may face are solved here easily, elegantly and concisely. But for all these problems, a single-threaded solution is often sufficient, and single-threaded programs are usually predictable and easy to debug. The same cannot be said about multithreaded and multiprocessing programs.

Multithreaded applications


Python has a module threading , and it has everything you need for multi-threaded programming: there are various types of locks, and a semaphore, and an event mechanism. In one word - everything that is needed for the vast majority of multithreaded programs. Moreover, using all these tools is quite simple. Let's consider an example of a program that starts 2 threads. One thread writes ten "0", the other - ten "1", and strictly in turn.

import threading

def writer

for i in xrange (10):

print x

Event_for_set.set ()

# init events

e1 = threading.Event ()

e2 = threading.Event ()

# init threads

0, e1, e2))

1, e2, e1))

# start threads

t1.start ()

t2.start ()

t1.join ()

t2.join ()


No magic or voodoo code. The code is clear and consistent. Moreover, as you can see, we have created a stream from a function. This is very convenient for small tasks. This code is also quite flexible. Suppose we have a third process that writes “2”, then the code will look like this:

import threading

def writer (x, event_for_wait, event_for_set):

for i in xrange (10):

Event_for_wait.wait () # wait for event

Event_for_wait.clear () # clean event for future

print x

Event_for_set.set () # set event for neighbor thread

# init events

e1 = threading.Event ()

e2 = threading.Event ()

e3 = threading.Event ()

# init threads

t1 = threading.Thread (target = writer, args = ( 0, e1, e2))

t2 = threading.Thread (target = writer, args = ( 1, e2, e3))

t3 = threading.Thread (target = writer, args = ( 2, e3, e1))

# start threads

t1.start ()

t2.start ()

t3.start ()

e1.set () # initiate the first event

# join threads to the main thread

t1.join ()

t2.join ()

t3.join ()


We added a new event, a new thread and slightly changed the parameters with which
streams start (of course, you can write a more general solution using, for example, MapReduce, but this is beyond the scope of this article).
As you can see, there is still no magic. Everything is simple and straightforward. Let's go further.

Global Interpreter Lock


There are two most common reasons to use threads: first, to increase the efficiency of using the multicore architecture of modern processors, and hence the performance of the program;
secondly, if we need to divide the logic of the program into parallel, fully or partially asynchronous sections (for example, to be able to ping several servers at the same time).

In the first case, we are faced with such a limitation of Python (or rather its main CPython implementation) as the Global Interpreter Lock (or GIL for short). The concept of the GIL is that only one thread can be executed by a processor at a time. This is done so that there is no struggle between threads for separate variables. The executable thread gains access to the entire environment. This feature of the implementation of threads in Python greatly simplifies the work with threads and gives a certain thread safety.

But there is a subtle point: it might seem that a multi-threaded application will run for exactly the same amount of time as a single-threaded application doing the same, or for the sum of the execution time of each thread on the CPU. But here one unpleasant effect awaits us. Consider the program:

with open ("test1.txt", "w") as fout:

for i in xrange (1000000):

print >> fout, 1


This program just writes a million lines “1” to a file and does it in ~ 0.35 seconds on my computer.

Consider another program:

from threading import Thread

def writer (filename, n):

with open (filename, "w") as fout:

for i in xrange (n):

print >> fout, 1

t1 = Thread (target = writer, args = ("test2.txt", 500000,))

t2 = Thread (target = writer, args = ("test3.txt", 500000,))

t1.start ()

t2.start ()

t1.join ()

t2.join ()


This program creates 2 threads. In each stream, it writes to a separate file half a million lines "1". In fact, the amount of work is the same as in the previous program. But over time, an interesting effect is obtained here. The program can run from 0.7 seconds to as much as 7 seconds. Why is this happening?

This is due to the fact that when a thread does not need a CPU resource, it releases the GIL, and at this moment it can try to get it, and another thread, and also the main thread. At the same time, the operating system, knowing that there are many cores, can aggravate everything by trying to distribute threads between the cores.

UPD: at the moment in Python 3.2 there is an improved implementation of the GIL, in which this problem is partially solved, in particular, due to the fact that each thread, after losing control, waits for a short period of time before it can again capture the GIL (there is good presentation in English)

“So you can't write efficient multithreaded programs in Python?” You ask. No, of course, there is a way out, and even several.

Multiprocessing applications


In order to in some sense solve the problem described in the previous paragraph, Python has a module subprocess ... We can write a program that we want to execute in a parallel thread (in fact, already a process). And run it in one or more threads in another program. This would really speed up our program, because the threads created in the GIL launcher do not pick up, but only wait for the running process to finish. However, this method has a lot of problems. The main problem is that it becomes difficult to transfer data between processes. You would have to somehow serialize objects, establish communication through PIPE or other tools, but all this inevitably carries overhead and the code becomes difficult to understand.

Another approach can help us here. Python has a multiprocessing module ... In terms of functionality, this module resembles threading ... For example, processes can be created in the same way from regular functions. The methods for working with processes are almost the same as for threads from the threading module. But it is customary to use other tools to synchronize processes and exchange data. We are talking about queues (Queue) and pipes (Pipe). However, analogs of locks, events and semaphores, which were in threading, are also here.

In addition, the multiprocessing module has a mechanism for working with shared memory. For this, the module has classes of a variable (Value) and an array (Array), which can be “shared” between processes. For the convenience of working with shared variables, you can use the manager classes. They are more flexible and easier to use, but slower. It should be noted that there is a nice opportunity to make common types from the ctypes module using the multiprocessing.sharedctypes module.

Also in the multiprocessing module there is a mechanism for creating process pools. This mechanism is very convenient to use for implementing the Master-Worker pattern or for implementing a parallel Map (which in a sense is a special case of Master-Worker).

Of the main problems with working with the multiprocessing module, it is worth noting the relative platform dependence of this module. Since work with processes is organized differently in different operating systems, some restrictions are imposed on the code. For example, Windows does not have a fork mechanism, so the process separation point must be wrapped in:

if __name__ == "__main__":


However, this design is already a good form.

What else...


There are other libraries and approaches for writing parallel applications in Python. For example, you can use Hadoop + Python or various Python MPI implementations (pyMPI, mpi4py). You can even use wrappers of existing C ++ or Fortran libraries. Here one could mention such frameworks / libraries as Pyro, Twisted, Tornado and many others. But all this is already beyond the scope of this article.

If you liked my style, then in the next article I will try to tell you how to write simple interpreters in PLY and what they can be used for.

Chapter 10.

Multithreaded applications

Multitasking in modern operating systems is taken for granted [ Before Apple OS X, Macintosh computers did not have modern multitasking operating systems. It is very difficult to properly design an operating system with full-fledged multitasking, so OS X had to be based on the Unix system.]. The user expects that when the text editor and the mail client are launched at the same time, these programs will not conflict, and when receiving e-mail, the editor will not stop working. When several programs are launched at the same time, the operating system quickly switches between programs, providing them with a processor in turn (unless, of course, multiple processors are installed on the computer). As a result, illusion running multiple programs at the same time, because even the best typist (and the fastest internet connection) can't keep up with a modern processor.

Multithreading, in a sense, can be seen as the next level of multitasking: instead of switching between different programs the operating system switches between different parts of the same program. For example, a multi-threaded email client allows you to receive new email messages while reading or composing new messages. Nowadays, multithreading is also taken for granted by many users.

VB has never had normal multithreading support. True, one of its varieties appeared in VB5 - collaborative streaming model(apartment threading). As you’ll see shortly, the collaborative model provides the programmer with some of the benefits of multithreading, but it doesn’t take full advantage of all the features. Sooner or later, you have to change from a training machine to a real one, and VB .NET became the first version of VB with support for a free multithreaded model.

However, multithreading is not one of the features that are easily implemented in programming languages ​​and easily mastered by programmers. Why?

Because in multithreaded applications, very tricky errors can occur that appear and disappear unpredictably (and such errors are the most difficult to debug).

A word of warning: multithreading is one of the hardest areas of programming. The slightest inattention leads to the appearance of elusive errors, the correction of which takes astronomical sums. For this reason, this chapter contains many bad examples - we deliberately wrote them in such a way as to demonstrate common errors. This is the safest approach to learning multithreaded programming: you should be able to spot potential problems when everything seems to be working fine at first glance, and know how to solve them. If you want to use multi-threaded programming techniques, you cannot do without it.

This chapter will lay a solid foundation for further independent work, but we will not be able to describe multithreaded programming in all its subtleties - only the printed documentation on the classes of the Threading namespace is more than 100 pages. If you want to master multithreaded programming at a higher level, refer to specialized books.

But no matter how dangerous multithreaded programming is, it is indispensable for professional solution of some problems. If your programs don't use multithreading where appropriate, users will become very frustrated and prefer another product. For example, it was only in the fourth version of the popular e-mail program Eudora that multi-threaded capabilities appeared, without which it is impossible to imagine any modern program for working with e-mail. By the time Eudora introduced multithreading support, many users (including one of the authors of this book) had switched to other products.

Finally, in .NET, single-threaded programs simply do not exist. Everything.NET programs are multithreaded because the garbage collector runs as a low-priority background process. As shown below, for serious graphical programming in .NET, proper threading helps prevent the graphical interface from locking up when the program is executing lengthy operations.

Introducing multithreading

Each program works in a specific context, describing the distribution of code and data in memory. By saving the context, the state of the program flow is actually saved, which allows you to restore it in the future and continue the program execution.

Saving context comes with a cost of time and memory. The operating system remembers the state of the program thread and transfers control to another thread. When the program wants to continue executing the suspended thread, the saved context has to be restored, which takes even longer. Therefore, multithreading should only be used when the benefits offset all the costs. Some typical examples are listed below.

  • The functionality of the program is clearly and naturally divided into several heterogeneous operations, as in the example with receiving e-mail and preparing new messages.
  • The program performs long and complex calculations, and you do not want the graphical interface to be blocked for the duration of the calculations.
  • The program runs on a multiprocessor computer with an operating system that supports the use of multiple processors (as long as the number of active threads does not exceed the number of processors, parallel execution is practically free of the costs associated with switching threads).

Before moving on to the mechanics of multithreaded programs, it is necessary to point out one circumstance that often causes confusion among beginners in the field of multithreaded programming.

A procedure, not an object, will be executed in the program stream.

It is difficult to say what is meant by the expression "object is running", but one of the authors often teaches seminars on multithreading programming and this question is asked more often than others. Perhaps someone thinks that the work of the program thread begins with a call to the New method of the class, after which the thread processes all messages passed to the corresponding object. Such representations absolutely are wrong. One object can contain several threads that execute different (and sometimes even the same) methods, while the messages of the object are transmitted and received by several different threads (by the way, this is one of the reasons that complicate multi-threaded programming: in order to debug a program, you need to find out which thread is in the given moment performs this or that procedure!).

Since threads are created from methods of objects, the object itself is usually created before the thread. After successfully creating the object, the program creates a thread, passing it the address of the object's method, and only after that gives the order to start the execution of the thread. The procedure for which the thread was created, like all procedures, can create new objects, perform operations on existing objects, and call other procedures and functions that are in its scope.

Common methods of classes can also be executed in program threads. In this case, also keep in mind another important circumstance: the thread ends with an exit from the procedure for which it was created. Normal completion of the program flow is not possible until the procedure is exited.

Threads can terminate not only naturally, but also abnormally. This is generally not recommended. See Terminating and Interrupting Streams for more information.

The core .NET features related to the use of programmatic threads are concentrated in the Threading namespace. Therefore, most multithreaded programs should start with the following line:

Imports System.Threading

Importing a namespace makes your program easier to type and enables IntelliSense technology.

The direct connection of flows with procedures suggests that in this picture are important delegates(see chapter 6). Specifically, the Threading namespace includes the ThreadStart delegate, which is typically used when starting program threads. The syntax for using this delegate looks like this:

Public Delegate Sub ThreadStart ()

Code called with the ThreadStart delegate must have no parameters or return value, so threads cannot be created for functions (which return a value) and for procedures with parameters. To transfer information from the stream, you also have to look for alternative means, since the executed methods do not return values ​​and cannot use transfer by reference. For example, if the ThreadMethod is in the WilluseThread class, then the ThreadMethod can communicate information by modifying the properties of instances of the WillUseThread class.

Application domains

.NET threads run in so-called application domains, defined in the documentation as "the sandbox in which the application runs." An application domain can be thought of as a lightweight version of Win32 processes; a single Win32 process can contain multiple application domains. The main difference between application domains and processes is that a Win32 process has its own address space (in the documentation, application domains are also compared to logical processes running inside a physical process). In NET, all memory management is handled by the runtime, so multiple application domains can run in a single Win32 process. One of the benefits of this scheme is the improved scaling capabilities of applications. Tools for working with application domains are in the AppDomain class. We recommend that you study the documentation for this class. With its help, you can get information about the environment in which your program is running. Specifically, the AppDomain class is used when performing reflection on .NET system classes. The following program lists the loaded assemblies.

Imports System.Reflection

Module Modulel

Sub Main ()

Dim theDomain As AppDomain

theDomain = AppDomain.CurrentDomain

Dim Assemblies () As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

For Each anAssembly In Assemblies

Console.WriteLinetanAssembly.Full Name) Next

Console.ReadLine ()

End Sub

End Module

Creating streams

Let's start with a rudimentary example. Let's say you want to run a procedure in a separate thread that decrements the counter value in an infinite loop. The procedure is defined as part of the class:

Public Class WillUseThreads

Public Sub SubtractFromCounter ()

Dim count As Integer

Do While True count - = 1

Console.WriteLlne ("Am in another thread and counter ="

& count)

Loop

End Sub

End Class

Since the Do loop condition is always true, you might think that nothing will interfere with the execution of the SubtractFromCounter procedure. However, in a multithreaded application, this is not always the case.

The following snippet contains the Sub Main procedure that starts the thread and the Imports command:

Option Strict On Imports System.Threading Module Modulel

Sub Main ()

1 Dim myTest As New WillUseThreads ()

2 Dim bThreadStart As New ThreadStart (AddressOf _

myTest.SubtractFromCounter)

3 Dim bThread As New Thread (bThreadStart)

4 "bThread.Start ()

Dim i As Integer

5 Do While True

Console.WriteLine ("In main thread and count is" & i) i + = 1

Loop

End Sub

End Module

Let's take a look at the most important points in sequence. First of all, the Sub Man n procedure always works in main stream(main thread). In .NET programs, there are always at least two threads running: the main thread and the garbage collection thread. Line 1 creates a new instance of the test class. In line 2, we create a ThreadStart delegate and pass the address of the SubtractFromCounter procedure of the test class instance created in line 1 (this procedure is called with no parameters). GoodBy importing the Threading namespace, the long name can be omitted. The new thread object is created on line 3. Notice the passing of the ThreadStart delegate when calling the Thread class constructor. Some programmers prefer to concatenate these two lines into one logical line:

Dim bThread As New Thread (New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Finally, line 4 "starts" the thread by calling the Start method of the Thread instance created for the ThreadStart delegate. By calling this method, we tell the operating system that the Subtract procedure should run in a separate thread.

The word "starts" in the previous paragraph is enclosed in quotation marks, because this is one of the many oddities of multithreaded programming: calling Start does not actually start the thread! It simply tells the operating system to schedule the specified thread to run, but starting directly is outside the control of the program. You won't be able to start executing threads on your own, because the operating system always controls the execution of threads. In a later section, you will learn how to use priority to make the operating system start your thread faster.

In fig. 10.1 shows an example of what can happen after starting a program and then interrupting it with the Ctrl + Break key. In our case, a new thread started only after the counter in the main thread increased to 341!

Rice. 10.1. Simple multithreaded software runtime

If the program runs for a longer period of time, the result will look something like the one shown in Fig. 10.2. We see that youcompletion of the running thread is suspended and control is transferred to the main thread again. In this case, there is a manifestation preemptive multithreading through time slicing. The meaning of this terrifying term is explained below.

Rice. 10.2. Switching between threads in a simple multithreaded program

When interrupting threads and transferring control to other threads, the operating system uses the principle of preemptive multithreading through time slicing. Time quantization also solves one of the common problems that arose before in multithreaded programs - one thread takes up all the CPU time and is not inferior to the control of other threads (as a rule, this happens in intensive cycles like the one above). To prevent exclusive CPU hijacking, your threads should transfer control to other threads from time to time. If the program turns out to be "unconscious", there is another, slightly less desirable solution: the operating system always preempts a running thread, regardless of its priority level, so that access to the processor is granted to every thread in the system.

Because the quantization schemes of all versions of Windows that run .NET have a minimum time slice for each thread, in .NET programming, the problems with CPU exclusive seizure are not so serious. On the other hand, if the .NET framework is ever adapted for other systems, this may change.

If we include the following line in our program before calling Start, then even the threads with the lowest priority will get some fraction of the CPU time:

bThread.Priority = ThreadPriority.Highest

Rice. 10.3. The thread with the highest priority usually starts faster

Rice. 10.4. The processor is also provided for lower priority threads

The command assigns the maximum priority to the new thread and decreases the priority of the main thread. From fig. 10.3 it can be seen that the new thread starts to work faster than before, but, as Fig. 10.4, the main thread also gets controllaziness (albeit for a very short time and only after a prolonged work of the flow with subtraction). When you run the program on your computers, you will get results similar to those shown in Fig. 10.3 and 10.4, but due to the differences between our systems, there will be no exact match.

The ThreadPrlority enumerated type includes values ​​for five priority levels:

ThreadPriority.Highest

ThreadPriority.AboveNormal

ThreadPrlority.Normal

ThreadPriority.BelowNormal

ThreadPriority.Lowest

Join method

Sometimes a program thread needs to be paused until another thread finishes. Let's say you want to pause thread 1 until thread 2 completes its computation. For this from stream 1 the Join method is called for stream 2. In other words, the command

thread2.Join ()

suspends the current thread and waits for thread 2 to complete. Thread 1 goes to locked state.

If you join stream 1 to stream 2 using the Join method, the operating system will automatically start stream 1 after stream 2. Note that the startup process is non-deterministic: it is impossible to say exactly how long after thread 2 ends, thread 1 will start working. There is another version of Join that returns a boolean value:

thread2.Join (Integer)

This method either waits for thread 2 to complete, or unblocks thread 1 after the specified time interval has elapsed, causing the operating system scheduler to allocate CPU time to the thread again. The method returns True if stream 2 terminates before the specified timeout interval expires, and False otherwise.

Remember the basic rule: whether thread 2 has completed or timed out, you have no control over when thread 1 is activated.

Thread names, CurrentThread and ThreadState

The Thread.CurrentThread property returns a reference to the thread object that is currently executing.

Although there is a wonderful thread window for debugging multithreaded applications in VB .NET, which is described below, we were very often helped out by the command

MsgBox (Thread.CurrentThread.Name)

Quite often it turns out that the code is being executed in a completely different thread from which it was supposed to be executed.

Recall that the term "non-deterministic scheduling of program flows" means a very simple thing: the programmer has practically no means at his disposal to influence the work of the scheduler. For this reason, programs often use the ThreadState property to return information about the current state of a thread.

Streams window

The Threads window of Visual Studio .NET is invaluable in debugging multithreaded programs. It is activated by the Debug> Windows submenu command in interrupt mode. Let's say you assigned a name to the bThread thread with the following command:

bThread.Name = "Subtracting thread"

An approximate view of the streams window after interrupting the program with the Ctrl + Break key combination (or in another way) is shown in Fig. 10.5.

Rice. 10.5. Streams window

The arrow in the first column marks the active thread returned by the Thread.CurrentThread property. The ID column contains numeric thread IDs. The next column lists the stream names (if assigned). The Location column indicates the procedure to run (for example, the WriteLine procedure of the Console class in Figure 10.5). The remaining columns contain information about priority and suspended threads (see next section).

The thread window (not the operating system!) Allows you to control the threads of your program using context menus. For example, you can stop the current thread by right-clicking on the corresponding line and choosing the Freeze command (you can resume the stopped thread later). Stopping threads is often used when debugging to prevent a malfunctioning thread from interfering with the application. In addition, the streams window allows you to activate another (not stopped) stream; to do this, right-click on the required line and select the Switch To Thread command from the context menu (or simply double-click on the thread line). As will be shown below, this is very useful in diagnosing potential deadlocks.

Suspending a stream

Temporarily unused streams can be transferred to a passive state using the Slеer method. A passive stream is also considered blocked. Of course, when a thread is put into a passive state, the rest of the threads will have more processor resources. The standard syntax for the Slеer method is as follows: Thread.Sleep (interval_in_milliseconds)

As a result of the Sleep call, the active thread becomes passive for at least a specified number of milliseconds (however, activation immediately after the specified interval has expired is not guaranteed). Please note: when calling the method, a reference to a specific thread is not passed - the Sleep method is called only for the active thread.

Another version of Sleep makes the current thread give up the rest of the allocated CPU time:

Thread.Sleep (0)

The next option puts the current thread in a passive state for an unlimited time (activation occurs only when you call Interrupt):

Thread.Slеer (Timeout.Infinite)

Since passive threads (even with an unlimited timeout) can be interrupted by the Interrupt method, which leads to the initiation of a ThreadlnterruptExcepti on exception, the Slayer call is always enclosed in a Try-Catch block, as in the following snippet:

Try

Thread.Sleep (200)

"The passive state of the thread has been interrupted

Catch e As Exception

"Other exceptions

End Try

Every .NET program runs on a program thread, so the Sleep method is also used to suspend programs (if the Threadipg namespace is not imported by the program, you must use the fully qualified name Threading.Thread. Sleep).

Terminating or interrupting program threads

A thread will automatically terminate when the method specified when the ThreadStart delegate is created, but sometimes it is necessary to terminate the method (and hence the thread) when certain factors occur. In such cases, streams usually check conditional variable, depending on the state of whicha decision is made about an emergency exit from the stream. Typically, a Do-While loop is included in the procedure for this:

Sub ThreadedMethod ()

"The program must provide means for the survey

"conditional variable.

"For example, a conditional variable can be styled as a property

Do While conditionVariable = False And MoreWorkToDo

"The main code

Loop End Sub

It takes some time to poll the conditional variable. You should only use persistent polling in a loop condition if you are waiting for a thread to terminate prematurely.

If the condition variable must be checked at a specific location, use the If-Then command in conjunction with Exit Sub inside an infinite loop.

Access to a conditional variable must be synchronized so that exposure from other threads does not interfere with its normal use. This important topic is covered in the "Troubleshooting: Synchronization" section.

Unfortunately, the code of passive (or otherwise blocked) threads is not executed, so the option with polling a conditional variable is not suitable for them. In this case, call the Interrupt method on the object variable that contains a reference to the desired thread.

The Interrupt method can only be called on threads in the Wait, Sleep, or Join state. If you call Interrupt for a thread that is in one of the listed states, then after a while the thread will start working again, and the execution environment will initiate a ThreadlnterruptedExcepti on exception in the thread. This occurs even if the thread has been made passive indefinitely by calling Thread.Sleepdimeout. Infinite). We say "after a while" because thread scheduling is non-deterministic. The ThreadlnterruptedExcepti on exception is caught by the Catch section containing the exit code from the wait state. However, the Catch section is not required to terminate the thread on an Interrupt call — the thread handles the exception as it sees fit.

In .NET, the Interrupt method can be called even for unblocked threads. In this case, the thread is interrupted at the nearest blocking.

Suspending and killing threads

The Threading namespace contains other methods that interrupt normal threading:

  • Suspend;
  • Abort.

It’s hard to say why .NET included support for these methods - when you call Suspend and Abort, the program will most likely become unstable. None of the methods allow normal deinitialization of the stream. Also, when you call Suspend or Abort, you cannot predict what state the thread will leave objects in after being suspended or aborted.

Calling Abort throws a ThreadAbortException. To help you understand why this strange exception should not be handled in programs, here is an excerpt from the .NET SDK documentation:

“... When a thread is destroyed by calling Abort, the runtime throws a ThreadAbortException. This is a special kind of exception that cannot be caught by the program. When this exception is thrown, the runtime runs all Finally blocks before terminating the thread. Because any action can take place in Finally blocks, call Join to ensure that the stream is destroyed. "

Moral: Abort and Suspend are not recommended (and if you still cannot do without Suspend, resume the suspended thread using the Resume method). You can safely terminate a thread only by polling a synchronized condition variable or by calling the Interrupt method discussed above.

Background threads (daemons)

Some threads running in the background automatically stop running when other program components stop. In particular, the garbage collector runs in one of the background threads. Background threads are usually created to receive data, but this is done only if other threads are running code that can process the received data. Syntax: stream name. IsBackGround = True

If there are only background threads left in the application, the application will automatically terminate.

A bigger example: extracting data from HTML code

We recommend using streams only when the functionality of the program is clearly divided into several operations. The HTML extractor in Chapter 9 is a good example. Our class does two things: retrieve data from Amazon and process it. This is a perfect example of a situation in which multithreaded programming is truly appropriate. We create classes for several different books and then parse the data in different streams. Creating a new thread for each book increases the efficiency of the program, because while one thread is receiving data (which may require waiting on the Amazon server), another thread will be busy processing the data that has already been received.

The multi-threaded version of this program works more efficiently than the single-threaded version only on a computer with several processors or if the reception of additional data can be effectively combined with their analysis.

As mentioned above, only procedures that have no parameters can be run in threads, so you will have to make minor changes to the program. Below is the basic procedure, rewritten to exclude parameters:

Public Sub FindRank ()

m_Rank = ScrapeAmazon ()

Console.WriteLine ("the rank of" & m_Name & "Is" & GetRank)

End Sub

Since we will not be able to use the combined field for storing and retrieving information (writing multi-threaded programs with a graphical interface is discussed in the last section of this chapter), the program stores the data of four books in an array, the definition of which begins like this:

Dim theBook (3.1) As String theBook (0.0) = "1893115992"

theBook (0.l) = "Programming VB .NET" "Etc.

Four streams are created in the same loop that AmazonRanker objects are created in:

For i = 0 To 3

Try

theRanker = New AmazonRanker (theBook (i.0). theBookd.1))

aThreadStart = New ThreadStar (AddressOf theRanker.FindRan ()

aThread = New Thread (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start () Catch e As Exception

Console.WriteLine (e.Message)

End Try

Next

Below is the complete text of the program:

Option Strict On Imports System.IO Imports System.Net

Imports System.Threading

Module Modulel

Sub Main ()

Dim theBook (3.1) As String

theBook (0.0) = "1893115992"

theBook (0.l) = "Programming VB .NET"

theBook (l.0) = "1893115291"

theBook (l.l) = "Database Programming VB .NET"

theBook (2,0) = "1893115623"

theBook (2.1) = "Programmer" s Introduction to C #. "

theBook (3.0) = "1893115593"

theBook (3.1) = "Gland the .Net Platform"

Dim i As Integer

Dim theRanker As = AmazonRanker

Dim aThreadStart As Threading.ThreadStart

Dim aThread As Threading.Thread

For i = 0 To 3

Try

theRanker = New AmazonRankerttheBook (i.0). theBook (i.1))

aThreadStart = New ThreadStart (AddressOf theRanker. FindRank)

aThread = New Thread (aThreadStart)

aThread.Name = theBook (i.l)

aThread.Start ()

Catch e As Exception

Console.WriteLlnete.Message)

End Try Next

Console.ReadLine ()

End Sub

End Module

Public Class AmazonRanker

Private m_URL As String

Private m_Rank As Integer

Private m_Name As String

Public Sub New (ByVal ISBN As String. ByVal theName As String)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN

m_Name = theName End Sub

Public Sub FindRank () m_Rank = ScrapeAmazon ()

Console.Writeline ("the rank of" & m_Name & "is"

& GetRank) End Sub

Public Readonly Property GetRank () As String Get

If m_Rank<>0 Then

Return CStr (m_Rank) Else

" Problems

End If

End Get

End Property

Public Readonly Property GetName () As String Get

Return m_Name

End Get

End Property

Private Function ScrapeAmazon () As Integer Try

Dim theURL As New Uri (m_URL)

Dim theRequest As WebRequest

theRequest = WebRequest.Create (theURL)

Dim theResponse As WebResponse

theResponse = theRequest.GetResponse

Dim aReader As New StreamReader (theResponse.GetResponseStream ())

Dim theData As String

theData = aReader.ReadToEnd

Return Analyze (theData)

Catch E As Exception

Console.WriteLine (E.Message)

Console.WriteLine (E.StackTrace)

Console. ReadLine ()

End Try End Function

Private Function Analyze (ByVal theData As String) As Integer

Dim Location As.Integer Location = theData.IndexOf (" Amazon.com

Sales Rank:") _

+ "Amazon.com Sales Rank:".Length

Dim temp As String

Do Until theData.Substring (Location.l) = "<" temp = temp

& theData.Substring (Location.l) Location + = 1 Loop

Return Clnt (temp)

End Function

End Class

Multi-threaded operations are commonly used in .NET and I / O namespaces, so the .NET Framework library provides special asynchronous methods for them. For more information on using asynchronous methods when writing multithreaded programs, see the BeginGetResponse and EndGetResponse methods of the HTTPWebRequest class.

Main hazard (general data)

So far, the only safe use case for threads has been considered - our streams did not change the general data. If you allow the change in the general data, potential errors begin to multiply exponentially and it becomes much more difficult to get rid of them for the program. On the other hand, if you prohibit modification of shared data by different threads, multithreading .NET programming will hardly differ from the limited capabilities of VB6.

We offer you a small program that demonstrates the problems that arise without going into unnecessary details. This program simulates a house with a thermostat in each room. If the temperature is 5 degrees Fahrenheit or more (about 2.77 degrees Celsius) less than the target temperature, we order the heating system to increase the temperature by 5 degrees; otherwise, the temperature rises by only 1 degree. If the current temperature is greater than or equal to the set one, no change is made. Temperature control in each room is carried out with a separate flow with a 200-millisecond delay. The main work is done with the following snippet:

If mHouse.HouseTemp< mHouse.MAX_TEMP = 5 Then Try

Thread.Sleep (200)

Catch tie As ThreadlnterruptedException

"Passive waiting has been interrupted

Catch e As Exception

"Other End Try Exceptions

mHouse.HouseTemp + - 5 "Etc.

Below is the complete source code of the program. The result is shown in Fig. 10.6: The temperature in the house has reached 105 degrees Fahrenheit (40.5 degrees Celsius)!

1 Option Strict On

2 Imports System.Threading

3 Module Modulel

4 Sub Main ()

5 Dim myHouse As New House (l0)

6 Console. ReadLine ()

7 End Sub

8 End Module

9 Public Class House

10 Public Const MAX_TEMP As Integer = 75

11 Private mCurTemp As Integer = 55

12 Private mRooms () As Room

13 Public Sub New (ByVal numOfRooms As Integer)

14 ReDim mRooms (numOfRooms = 1)

15 Dim i As Integer

16 Dim aThreadStart As Threading.ThreadStart

17 Dim aThread As Thread

18 For i = 0 To numOfRooms -1

19 Try

20 mRooms (i) = NewRoom (Me, mCurTemp, CStr (i) & "throom")

21 aThreadStart - New ThreadStart (AddressOf _

mRooms (i) .CheckTempInRoom)

22 aThread = New Thread (aThreadStart)

23 aThread.Start ()

24 Catch E As Exception

25 Console.WriteLine (E.StackTrace)

26 End Try

27 Next

28 End Sub

29 Public Property HouseTemp () As Integer

thirty . Get

31 Return mCurTemp

32 End Get

33 Set (ByVal Value As Integer)

34 mCurTemp = Value 35 End Set

36 End Property

37 End Class

38 Public Class Room

39 Private mCurTemp As Integer

40 Private mName As String

41 Private mHouse As House

42 Public Sub New (ByVal theHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mHouse = theHouse

44 mCurTemp = temp

45 mName = roomName

46 End Sub

47 Public Sub CheckTempInRoom ()

48 ChangeTemperature ()

49 End Sub

50 Private Sub ChangeTemperature ()

51 Try

52 If mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

53 Thread.Sleep (200)

54 mHouse.HouseTemp + - 5

55 Console.WriteLine ("Am in" & Me.mName & _

56 ".Current temperature is" & mHouse.HouseTemp)

57. Elself mHouse.HouseTemp< mHouse.MAX_TEMP Then

58 Thread.Sleep (200)

59 mHouse.HouseTemp + = 1

60 Console.WriteLine ("Am in" & Me.mName & _

61 ".Current temperature is" & mHouse.HouseTemp)

62 Else

63 Console.WriteLine ("Am in" & Me.mName & _

64 ".Current temperature is" & mHouse.HouseTemp)

65 "Do nothing, temperature is normal

66 End If

67 Catch tae As ThreadlnterruptedException

68 "Passive wait has been interrupted

69 Catch e As Exception

70 "Other exceptions

71 End Try

72 End Sub

73 End Class

Rice. 10.6. Multithreading issues

The Sub Main procedure (lines 4-7) creates a "house" with ten "rooms". The House class sets a maximum temperature of 75 degrees Fahrenheit (about 24 degrees Celsius). Lines 13-28 define a rather complex house constructor. The key to understanding the program are lines 18-27. Line 20 creates another room object, and a reference to the house object is passed to the constructor so that the room object can refer to it if necessary. Lines 21-23 start ten streams to adjust the temperature in each room. The Room class is defined on lines 38-73. House coxpa referenceis stored in the mHouse variable in the Room class constructor (line 43). The code for checking and adjusting the temperature (lines 50-66) looks simple and natural, but as you will soon see, this impression is deceiving! Note that this code is wrapped in a Try-Catch block because the program uses the Sleep method.

Hardly anyone would agree to live in temperatures of 105 degrees Fahrenheit (40.5 to 24 degrees Celsius). What happened? The problem is related to the following line:

If mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then

And the following happens: first, the temperature is checked by flow 1. He sees that the temperature is too low, and raises it by 5 degrees. Unfortunately, before the temperature rises, stream 1 is interrupted and control is transferred to stream 2. Stream 2 checks the same variable that has not been changed yet flow 1. Thus, flow 2 is also preparing to raise the temperature by 5 degrees, but does not have time to do this and also goes into a waiting state. The process continues until stream 1 is activated and proceeds to the next command - increasing the temperature by 5 degrees. The increase is repeated when all 10 streams are activated, and the residents of the house will have a bad time.

Solution to the problem: synchronization

In the previous program, a situation arises when the result of the program depends on the order in which the threads are executed. To get rid of it, you need to make sure that commands like

If mHouse.HouseTemp< mHouse.MAX_TEMP - 5 Then...

are fully processed by the active thread before it is interrupted. This property is called atomic shame - a block of code must be executed by each thread without interruption, as an atomic unit. A group of commands, combined into an atomic block, cannot be interrupted by the thread scheduler until it is completed. Any multithreaded programming language has its own ways of ensuring atomicity. In VB .NET, the easiest way to use the SyncLock command is to pass in an object variable when called. Make small changes to the ChangeTemperature procedure from the previous example, and the program will work fine:

Private Sub ChangeTemperature () SyncLock (mHouse)

Try

If mHouse.HouseTemp< mHouse.MAXJTEMP -5 Then

Thread.Sleep (200)

mHouse.HouseTemp + = 5

Console.WriteLine ("Am in" & Me.mName & _

".Current temperature is" & mHouse.HouseTemp)

Elself

mHouse.HouseTemp< mHouse. MAX_TEMP Then

Thread.Sleep (200) mHouse.HouseTemp + = 1

Console.WriteLine ("Am in" & Me.mName & _ ".Current temperature is" & mHouse.HomeTemp) Else

Console.WriteLineC "Am in" & Me.mName & _ ".Current temperature is" & mHouse.HouseTemp)

"Do nothing, the temperature is normal

End If Catch tie As ThreadlnterruptedException

"Passive wait was interrupted by Catch e As Exception

"Other exceptions

End Try

End SyncLock

End Sub

The SyncLock block code is executed atomically. Access to it from all other threads will be closed until the first thread releases the lock with the End SyncLock command. If a thread in a synchronized block goes into a passive wait state, the lock remains until the thread is interrupted or resumed.

Correct use of the SyncLock command keeps your program thread safe. Unfortunately, overuse of SyncLock has a negative impact on performance. Synchronizing code in a multithreaded program reduces the speed of its work by several times. Synchronize only the code you need and release the lock as soon as possible.

The base collection classes are not thread safe in multithreaded applications, but the .NET Framework includes thread safe versions of most of the collection classes. In these classes, the code of potentially dangerous methods is enclosed in SyncLock blocks. Thread-safe versions of collection classes should be used in multithreaded programs wherever data integrity is compromised.

It remains to mention that conditional variables are easily implemented using the SyncLock command. To do this, you just need to synchronize the write to the common boolean property, available for reading and writing, as it is done in the following fragment:

Public Class ConditionVariable

Private Shared locker As Object = New Object ()

Private Shared mOK As Boolean Shared

Property TheConditionVariable () As Boolean

Get

Return mOK

End Get

Set (ByVal Value As Boolean) SyncLock (locker)

mOK = Value

End SyncLock

End Set

End Property

End Class

SyncLock Command and Monitor Class

There are some subtleties involved in using the SyncLock command that were not shown in the simple examples above. So, the choice of the synchronization object plays a very important role. Try running the previous program with the SyncLock (Me) command instead of SyncLock (mHouse). The temperature rises above the threshold again!

Remember that the SyncLock command synchronizes using object, passed as a parameter, not by the code snippet. The SyncLock parameter acts as a door for accessing the synchronized fragment from other threads. The SyncLock (Me) command actually opens several different "doors", which is exactly what you were trying to avoid with synchronization. Morality:

To protect shared data in a multithreaded application, the SyncLock command must synchronize one object at a time.

Since synchronization is associated with a specific object, in some situations, it is possible to inadvertently lock other fragments. Let's say you have two synchronized methods, first and second, both of which are synchronized on the bigLock object. When thread 1 enters method first and grabs bigLock, no thread will be able to enter method second because access to it is already restricted to thread 1!

The functionality of the SyncLock command can be thought of as a subset of the functionality of the Monitor class. The Monitor class is highly customizable and can be used to solve non-trivial synchronization tasks. The SyncLock command is a close analogue of the Enter and Exi t methods of the Moni tor class:

Try

Monitor.Enter (theObject) Finally

Monitor.Exit (theObject)

End Try

For some standard operations (increasing / decreasing a variable, exchanging the contents of two variables), the .NET Framework provides the Interlocked class, whose methods perform these operations at the atomic level. Using the Interlocked class, these operations are much faster than using the SyncLock command.

Interlocking

During synchronization, the lock is set on objects, not threads, so when using different objects to block different snippets of code in programs sometimes quite non-trivial errors occur. Unfortunately, in many cases synchronization on a single object is simply unacceptable, as it will lead to blocking threads too often.

Consider the situation interlocking(deadlock) in its simplest form. Imagine two programmers at the dinner table. Unfortunately, they only have one knife and one fork for two. Assuming you need both a knife and a fork to eat, two situations are possible:

  • One programmer manages to grab a knife and fork and starts eating. When he is full, he puts the dinner set aside, and then another programmer can take them.
  • One programmer takes the knife and the other takes the fork. Neither can start eating unless the other gives up his appliance.

In a multithreaded program, this situation is called mutual blocking. The two methods are synchronized on different objects. Thread A captures object 1 and enters the program portion protected by this object. Unfortunately, for it to work, it needs access to code protected by another Sync Lock with a different sync object. But before it has time to enter a fragment that is synchronized by another object, stream B enters it and captures this object. Now thread A cannot enter the second fragment, thread B cannot enter the first fragment, and both threads are doomed to wait indefinitely. No thread can continue to run because the required object will never be freed.

Diagnosis of deadlocks is complicated by the fact that they can occur in relatively rare cases. It all depends on the order in which the scheduler allocates CPU time to them. It is possible that in most cases, synchronization objects will be captured in a non-deadlocked order.

The following is an implementation of the deadlock situation just described. After a short discussion of the most fundamental points, we will show how to identify a deadlock situation in the thread window:

1 Option Strict On

2 Imports System.Threading

3 Module Modulel

4 Sub Main ()

5 Dim Tom As New Programmer ("Tom")

6 Dim Bob As New Programmer ("Bob")

7 Dim aThreadStart As New ThreadStart (AddressOf Tom.Eat)

8 Dim aThread As New Thread (aThreadStart)

9 aThread.Name = "Tom"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Dim bThread As New Thread (bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start ()

14 bThread.Start ()

15 End Sub

16 End Module

17 Public Class Fork

18 Private Shared mForkAvaiTable As Boolean = True

19 Private Shared mOwner As String = "Nobody"

20 Private Readonly Property OwnsUtensil () As String

21 Get

22 Return mOwner

23 End Get

24 End Property

25 Public Sub GrabForktByVal a As Programmer)

26 Console.Writel_ine (Thread.CurrentThread.Name & _

"trying to grab the fork.")

27 Console.WriteLine (Me.OwnsUtensil & "has the fork."). ...

28 Monitor.Enter (Me) "SyncLock (aFork)"

29 If mForkAvailable Then

30 a.HasFork = True

31 mOwner = a.MyName

32 mForkAvailable = False

33 Console.WriteLine (a.MyName & "just got the fork.waiting")

34 Try

Thread.Sleep (100) Catch e As Exception Console.WriteLine (e.StackTrace)

End Try

35 End If

36 Monitor.Exit (Me)

End SyncLock

37 End Sub

38 End Class

39 Public Class Knife

40 Private Shared mKnifeAvailable As Boolean = True

41 Private Shared mOwner As String = "Nobody"

42 Private Readonly Property OwnsUtensi1 () As String

43 Get

44 Return mOwner

45 End Get

46 End Property

47 Public Sub GrabKnifetByVal a As Programmer)

48 Console.WriteLine (Thread.CurrentThread.Name & _

"trying to grab the knife.")

49 Console.WriteLine (Me.OwnsUtensil & "has the knife.")

50 Monitor.Enter (Me) "SyncLock (aKnife)"

51 If mKnifeAvailable Then

52 mKnifeAvailable = False

53 a.HasKnife = True

54 mOwner = a.MyName

55 Console.WriteLine (a.MyName & "just got the knife.waiting")

56 Try

Thread.Sleep (100)

Catch e As Exception

Console.WriteLine (e.StackTrace)

End Try

57 End If

58 Monitor.Exit (Me)

59 End Sub

60 End Class

61 Public Class Programmer

62 Private mName As String

63 Private Shared mFork As Fork

64 Private Shared mKnife As Knife

65 Private mHasKnife As Boolean

66 Private mHasFork As Boolean

67 Shared Sub New ()

68 mFork = New Fork ()

69 mKnife = New Knife ()

70 End Sub

71 Public Sub New (ByVal theName As String)

72 mName = theName

73 End Sub

74 Public Readonly Property MyName () As String

75 Get

76 Return mName

77 End Get

78 End Property

79 Public Property HasKnife () As Boolean

80 Get

81 Return mHasKnife

82 End Get

83 Set (ByVal Value As Boolean)

84 mHasKnife = Value

85 End Set

86 End Property

87 Public Property HasFork () As Boolean

88 Get

89 Return mHasFork

90 End Get

91 Set (ByVal Value As Boolean)

92 mHasFork = Value

93 End Set

94 End Property

95 Public Sub Eat ()

96 Do Until Me.HasKnife And Me.HasFork

97 Console.Writeline (Thread.CurrentThread.Name & "is in the thread.")

98 If Rnd ()< 0.5 Then

99 mFork.GrabFork (Me)

100 Else

101 mKnife.GrabKnife (Me)

102 End If

103 Loop

104 MsgBox (Me.MyName & "can eat!")

105 mKnife = New Knife ()

106 mFork = New Fork ()

107 End Sub

108 End Class

The main procedure Main (lines 4-16) creates two instances of the Programmer class and then starts two threads to execute the critical Eat method of the Programmer class (lines 95-108), described below. The Main procedure sets the names of the threads and sets them up; probably everything that happens is understandable and without comment.

The code for the Fork class looks more interesting (lines 17-38) (a similar Knife class is defined in lines 39-60). Lines 18 and 19 specify the values ​​of the common fields, by which you can find out if the plug is currently available, and if not, who is using it. The ReadOnly property OwnUtensi1 (lines 20-24) is intended for the simplest transfer of information. Central to the Fork class is the GrabFork “grab the fork” method, defined in lines 25-27.

  1. Lines 26 and 27 simply print debug information to the console. In the main code of the method (lines 28-36), access to the fork is synchronized by objectbelt Me. Since our program only uses one fork, sync over Me ensures that no two threads can grab it at the same time. The Slee "p command (in the block starting on line 34) simulates the delay between grabbing a fork / knife and starting a meal. Note that the Sleep command does not unlock objects and only speeds up deadlocks!
    However, of most interest is the code of the Programmer class (lines 61-108). Lines 67-70 define a generic constructor to ensure that there is only one fork and knife in the program. The property code (lines 74-94) is simple and requires no comment. The most important thing happens in the Eat method, which is executed by two separate threads. The process continues in a loop until some stream captures the fork along with the knife. On lines 98-102, the object randomly grabs the fork / knife using the Rnd call, which is what causes the deadlock. The following happens:
    The thread that executes the Eat method of the Tot is invoked and enters the loop. He grabs the knife and goes into a waiting state.
  2. The thread executing Bob's Eat method invokes and enters the loop. It cannot grab the knife, but it grabs the fork and goes into a standby state.
  3. The thread that executes the Eat method of the Tot is invoked and enters the loop. It tries to grab the fork, but the fork is already grabbed by Bob; the thread goes into a waiting state.
  4. The thread executing Bob's Eat method invokes and enters the loop. He tries to grab the knife, but the knife is already captured by the object Thoth; the thread goes into a waiting state.

All this continues indefinitely - we are faced with a typical situation of deadlock (try running the program, and you will see that no one is able to eat this way).
You can also see if a deadlock has occurred in the threads window. Run the program and interrupt it with the Ctrl + Break keys. Include the Me variable in the viewport and open the streams window. The result looks something like the one shown in Fig. 10.7. From the figure, you can see that Bob's thread has grabbed a knife, but it has no fork. Right-click in the Threads window on the Tot line and select Switch to Thread from the context menu. The viewport shows that the Thoth stream has a fork, but no knife. Of course, this is not one hundred percent proof, but such behavior at least makes one suspect that something was wrong.
If the option with synchronization by one object (as in the program with an increase in the -temperature in the house) is not possible, to prevent mutual locks, you can number the synchronization objects and always capture them in a constant order. Let's continue the dining programmer analogy: if the thread always takes the knife first and then the fork, there will be no problems with deadlocking. The first stream that grabs the knife will be able to eat normally. Translated into the language of program streams, this means that the capture of object 2 is possible only if object 1 is first captured.

Rice. 10.7. Analysis of deadlocks in the thread window

Therefore, if we remove the call to Rnd on line 98 and replace it with the snippet

mFork.GrabFork (Me)

mKnife.GrabKnife (Me)

deadlock disappears!

Collaborate on data as it is created

In multithreaded applications, it is common for threads to not only work with shared data, but also wait for it to appear (that is, thread 1 must create data before thread 2 can use it). Since the data is shared, access to it needs to be synchronized. It is also necessary to provide means for notifying waiting threads about the appearance of ready data.

This situation is usually called the supplier / consumer problem. The thread is trying to access data that does not yet exist, so it must transfer control to another thread that creates the required data. The problem is solved with the following code:

  • Thread 1 (consumer) wakes up, enter a synchronized method, looks for data, doesn't find it, and goes into a wait state. Preliminarilyphysically, he must remove the blocking so as not to interfere with the work of the supplying thread.
  • Thread 2 (provider) enters a synchronized method freed by thread 1, creates data for stream 1 and somehow notifies stream 1 about the presence of data. It then releases the lock so that thread 1 can process the new data.

Do not try to solve this problem by constantly invoking thread 1 and checking the condition of the condition variable, the value of which is> set by thread 2. This decision will seriously affect the performance of your program, since in most cases thread 1 will be invoked for no reason; and thread 2 will wait so often that it will run out of time to create data.

Provider / consumer relationships are very common, so special primitives are created for such situations in multithreaded programming class libraries. In NET, these primitives are called Wait and Pulse-PulseAl 1 and are part of the Monitor class. Figure 10.8 illustrates the situation we are about to program. The program organizes three thread queues: a wait queue, a blocking queue, and an execution queue. The thread scheduler does not allocate CPU time to threads that are in the waiting queue. For a thread to be allocated time, it must move to the execution queue. As a result, the application's work is organized much more efficiently than when polling a conditional variable.

In pseudocode, the data consumer idiom is formulated as follows:

"Entry into a synchronized block of the following type

While no data

Go to the waiting queue

Loop

If there is data, process it.

Leave synchronized block

Immediately after the Wait command is executed, the thread is suspended, the lock is released, and the thread enters the waiting queue. When the lock is released, the thread in the execution queue is allowed to run. Over time, one or more blocked threads will create the data necessary for the operation of the thread that is in the waiting queue. Since data validation is performed in a loop, the transition to using the data (after the loop) occurs only when there is data ready for processing.

In pseudocode, the data provider idiom looks like this:

"Entering a synchronized view block

While data is NOT needed

Go to the waiting queue

Else Produce Data

When the data is ready, call Pulse-PulseAll.

to move one or more threads from the blocking queue to the execution queue. Leave a synchronized block (and return to the run queue)

Suppose our program simulates a family with one parent who makes money and a child who spends this money. When the money is overit turns out that the child has to wait for the arrival of a new amount. The software implementation of this model looks like this:

1 Option Strict On

2 Imports System.Threading

3 Module Modulel

4 Sub Main ()

5 Dim theFamily As New Family ()

6 theFamily.StartltsLife ()

7 End Sub

8 End fjodule

9

10 Public Class Family

11 Private mMoney As Integer

12 Private mWeek As Integer = 1

13 Public Sub StartltsLife ()

14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)

16 Dim aThread As New Thread (aThreadStart)

17 Dim bThread As New Thread (bThreadStart)

18 aThread.Name = "Produce"

19 aThread.Start ()

20 bThread.Name = "Consume"

21 bThread. Start ()

22 End Sub

23 Public Property TheWeek () As Integer

24 Get

25 Return mweek

26 End Get

27 Set (ByVal Value As Integer)

28 mweek - Value

29 End Set

30 End Property

31 Public Property OurMoney () As Integer

32 Get

33 Return mMoney

34 End Get

35 Set (ByVal Value As Integer)

36 mMoney = Value

37 End Set

38 End Property

39 Public Sub Produce ()

40 Thread.Sleep (500)

41 Do

42 Monitor.Enter (Me)

43 Do While Me.OurMoney> 0

44 Monitor.Wait (Me)

45 Loop

46 Me.OurMoney = 1000

47 Monitor.PulseAll (Me)

48 Monitor.Exit (Me)

49 Loop

50 End Sub

51 Public Sub Consume ()

52 MsgBox ("Am in consume thread")

53 Do

54 Monitor.Enter (Me)

55 Do While Me.OurMoney = 0

56 Monitor.Wait (Me)

57 Loop

58 Console.WriteLine ("Dear parent I just spent all your" & _

money in week "& TheWeek)

59 TheWeek + = 1

60 If TheWeek = 21 * 52 Then System.Environment.Exit (0)

61 Me.OurMoney = 0

62 Monitor.PulseAll (Me)

63 Monitor.Exit (Me)

64 Loop

65 End Sub

66 End Class

The StartltsLife method (lines 13-22) prepares to start the Produce and Consume streams. The most important thing happens in the Produce streams (lines 39-50) and Consume (lines 51-65). The Sub Produce procedure checks the availability of money, and if there is money, it goes to the waiting queue. Otherwise, the parent generates money (line 46) and notifies the objects in the waiting queue about a change in the situation. Note that the call to Pulse-Pulse All takes effect only when the lock is released with the Monitor.Exit command. Conversely, the Sub Consume procedure checks the availability of money, and if there is no money, notifies the expecting parent about it. Line 60 simply terminates the program after 21 conditional years; calling System. Environment.Exit (0) is the .NET analogue of the End command (the End command is also supported, but unlike System. Environment. Exit, it does not return an exit code to the operating system).

Threads that are put on the waiting queue must be freed by other parts of your program. It is for this reason that we prefer to use PulseAll over Pulse. Since it is not known in advance which thread will be activated when Pulse 1 is called, if there are relatively few threads in the queue, you can call PulseAll just as well.

Multithreading in graphics programs

Our discussion of multithreading in GUI applications begins with an example that explains what multithreading in GUI applications is for. Create a form with two buttons Start (btnStart) and Cancel (btnCancel), as shown in Fig. 10.9. Clicking the Start button generates a class that contains a random string of 10 million characters and a method to count the occurrences of the letter "E" in that long string. Note the use of the StringBuilder class for more efficient creation of long strings.

Step 1

Thread 1 notices that there is no data for it. It calls Wait, releases the lock, and goes to the wait queue.



Step 2

When the lock is released, thread 2 or thread 3 leaves the block queue and enters a synchronized block, acquiring the lock

Step3

Let's say thread 3 enters a synchronized block, creates data, and calls Pulse-Pulse All.

Immediately after it exits the block and releases the lock, thread 1 is moved to the execution queue. If thread 3 calls Pluse, only one enters the execution queuethread, when Pluse All is called, all threads go to the execution queue.



Rice. 10.8. Provider / consumer problem

Rice. 10.9. Multithreading in a simple GUI application

Imports System.Text

Public Class RandomCharacters

Private m_Data As StringBuilder

Private mjength, m_count As Integer

Public Sub New (ByVal n As Integer)

m_Length = n -1

m_Data = New StringBuilder (m_length) MakeString ()

End Sub

Private Sub MakeString ()

Dim i As Integer

Dim myRnd As New Random ()

For i = 0 To m_length

"Generate a random number between 65 and 90,

"convert it to uppercase

"and attach to the StringBuilder object

m_Data.Append (Chr (myRnd.Next (65.90)))

Next

End Sub

Public Sub StartCount ()

GetEes ()

End Sub

Private Sub GetEes ()

Dim i As Integer

For i = 0 To m_length

If m_Data.Chars (i) = CChar ("E") Then

m_count + = 1

End If Next

m_CountDone = True

End Sub

Public Readonly

Property GetCount () As Integer Get

If Not (m_CountDone) Then

Return m_count

End If

End Get End Property

Public Readonly

Property IsDone () As Boolean Get

Return

m_CountDone

End Get

End Property

End Class

There is very simple code associated with the two buttons on the form. The btn-Start_Click procedure instantiates the above RandomCharacters class, which encapsulates a string with 10 million characters:

Private Sub btnStart_Click (ByVal sender As System.Object.

ByVal e As System.EventArgs) Handles btnSTart.Click

Dim RC As New RandomCharacters (10000000)

RC.StartCount ()

MsgBox ("The number of es is" & RC.GetCount)

End Sub

The Cancel button displays a message box:

Private Sub btnCancel_Click (ByVal sender As System.Object._

ByVal e As System.EventArgs) Handles btnCancel.Click

MsgBox ("Count Interrupted!")

End Sub

When the program is run and the Start button is pressed, it turns out that the Cancel button is not responding to user input because the continuous loop prevents the button from handling the event it receives. This is unacceptable in modern programs!

There are two possible solutions. The first option, well known from previous VB versions, dispenses with multithreading: the DoEvents call is included in the loop. In NET this command looks like this:

Application.DoEvents ()

In our example, this is definitely not desirable - who wants to slow down a program with ten million DoEvents calls! If you instead allocate the loop to a separate thread, the operating system will switch between threads and the Cancel button will remain functional. The implementation with a separate thread is shown below. To clearly show that the Cancel button works, when we click it, we simply terminate the program.

Next step: Show Count button

Let's say you decided to show your creative imagination and give the form the look shown in fig. 10.9. Please note: the Show Count button is not yet available.

Rice. 10.10. Locked Button Form

A separate thread is expected to do the count and unlock the unavailable button. This can of course be done; moreover, such a task arises quite often. Unfortunately, you won't be able to act in the most obvious way - link the secondary thread to the GUI thread by keeping a link to the ShowCount button in the constructor, or even using a standard delegate. In other words, never do not use the option below (basic erroneous lines are in bold).

Public Class RandomCharacters

Private m_0ata As StringBuilder

Private m_CountDone As Boolean

Private mjength. m_count As Integer

Private m_Button As Windows.Forms.Button

Public Sub New (ByVa1 n As Integer, _

ByVal b As Windows.Forms.Button)

m_length = n - 1

m_Data = New StringBuilder (mJength)

m_Button = b MakeString ()

End Sub

Private Sub MakeString ()

Dim I As Integer

Dim myRnd As New Random ()

For I = 0 To m_length

m_Data.Append (Chr (myRnd.Next (65.90)))

Next

End Sub

Public Sub StartCount ()

GetEes ()

End Sub

Private Sub GetEes ()

Dim I As Integer

For I = 0 To mjength

If m_Data.Chars (I) = CChar ("E") Then

m_count + = 1

End If Next

m_CountDone = True

m_Button.Enabled = True

End Sub

Public Readonly

Property GetCount () As Integer

Get

If Not (m_CountDone) Then

Throw New Exception ("Count not yet done") Else

Return m_count

End If

End Get

End Property

Public Readonly Property IsDone () As Boolean

Get

Return m_CountDone

End Get

End Property

End Class

It is likely that this code will work in some cases. However:

  • The interaction of the secondary thread with the thread creating the GUI cannot be organized obvious means.
  • Never do not modify items in graphics programs from other program streams. All changes should only occur in the thread that created the GUI.

If you break these rules, we we guarantee that subtle, subtle bugs will occur in your multi-threaded graphics programs.

It will also fail to organize the interaction of objects using events. The 06-event worker runs on the same thread that the RaiseEvent was called so events won't help you.

Still, common sense dictates that graphical applications must provide a means of modifying elements from another thread. In the NET Framework, there is a thread-safe way to call methods of GUI applications from another thread. A special type of Method Invoker delegate from the System.Windows namespace is used for this purpose. Forms. The following snippet shows a new version of the GetEes method (changed lines in bold):

Private Sub GetEes ()

Dim I As Integer

For I = 0 To m_length

If m_Data.Chars (I) = CChar ("E") Then

m_count + = 1

End If Next

m_CountDone = True Try

Dim mylnvoker As New Methodlnvoker (AddressOf UpDateButton)

myInvoker.Invoke () Catch e As ThreadlnterruptedException

"Failure

End Try

End Sub

Public Sub UpDateButton ()

m_Button.Enabled = True

End Sub

Inter-thread calls to the button are made not directly, but through Method Invoker. The .NET Framework guarantees that this option is thread safe.

Why are there so many problems with multithreaded programming?

Now that you have some understanding of multithreading and the potential problems associated with it, we decided that it would be appropriate to answer the question in the heading of this subsection at the end of this chapter.

One of the reasons is that multithreading is a non-linear process, and we are used to a linear programming model. At first, it is difficult to get used to the very idea that program execution can be interrupted randomly, and control will be transferred to other code.

However, there is another, more fundamental reason: these days programmers too rarely program in assembler, or at least look at the disassembled output of the compiler. Otherwise, it would be much easier for them to get used to the idea that dozens of assembly instructions can correspond to one command of a high-level language (such as VB .NET). The thread can be interrupted after any of these instructions, and therefore in the middle of a high-level command.

But that's not all: modern compilers optimize program performance, and computer hardware can interfere with memory management. As a consequence, the compiler or hardware can, without your knowledge, change the order of commands specified in the source code of the program [ Many compilers optimize cyclic copying of arrays like for i = 0 to n: b (i) = a (i): ncxt. The compiler (or even a specialized memory manager) can simply create an array and then fill it with a single copy operation instead of copying individual elements many times!].

Hopefully, these explanations will help you better understand why multithreaded programming causes so many problems - or at least less surprise at the strange behavior of your multithreaded programs!

An example of building a simple multi-threaded application.

Born on the cause of a lot of questions about building multithreaded applications in Delphi.

The purpose of this example is to demonstrate how to properly build a multi-threaded application, with the removal of long-term work in a separate thread. And how in such an application to ensure the interaction of the main thread with the worker for transferring data from the form (visual components) to the stream and vice versa.

The example does not claim to be complete, it only demonstrates the simplest ways of interaction between threads. Allowing the user to "quickly blind" (who would know how much I hate it) a properly working multithreaded application.
Everything in it is commented in detail (in my opinion), but if you have any questions, ask.
But once again I warn you: Streams are not easy... If you have no idea how it all works, then there is a huge danger that often everything will work fine for you, and sometimes the program will behave more than strange. The behavior of an incorrectly written multithreaded program is highly dependent on a large number of factors that sometimes cannot be reproduced during debugging.

So an example. For convenience, I have placed both the code and attached the archive with the module and form code

unit ExThreadForm;

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

// constants used when transferring data from a stream to a form using
// send window messages
const
WM_USER_SendMessageMetod = WM_USER + 10;
WM_USER_PostMessageMetod = WM_USER + 11;

type
// description of the class of the thread, a descendant of tThread
tMyThread = class (tThread)
private
SyncDataN: Integer;
SyncDataS: String;
procedure SyncMetod1;
protected
procedure Execute; override;
public
Param1: String;
Param2: Integer;
Param3: Boolean;
Stopped: Boolean;
LastRandom: Integer;
IterationNo: Integer;
ResultList: tStringList;

Constructor Create (aParam1: String);
destructor Destroy; override;
end;

// class description of the form using the stream
TForm1 = class (TForm)
Label1: TLabel;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
procedure btnStartClick (Sender: TObject);
procedure btnStopClick (Sender: TObject);
private
(Private declarations)
MyThread: tMyThread;
procedure EventMyThreadOnTerminate (Sender: tObject);
procedure EventOnSendMessageMetod (var Msg: TMessage); message WM_USER_SendMessageMetod;
procedure EventOnPostMessageMetod (var Msg: TMessage); message WM_USER_PostMessageMetod;

Public
(Public declarations)
end;

var
Form1: TForm1;

{
Stopped - Demonstrates the transfer of data from a form to a stream.
Additional synchronization is not required, since it is simple
a one-word type, and is written with only one thread.
}

procedure TForm1.btnStartClick (Sender: TObject);
begin
Randomize (); // ensuring randomness in the sequence by Random () - has nothing to do with the flow

// Create an instance of the stream object, passing it an input parameter
{
ATTENTION!
The stream constructor is written in such a way that the stream is created
suspended because it allows:
1. Control the moment of its launch. This is almost always more convenient because
allows you to set up a stream even before starting, pass it input
parameters, etc.
2. Because the link to the created object will be saved in the form field, then
after self-destruction of the thread (see below) which when the thread is running
may occur at any time, this link will become invalid.
}
MyThread: = tMyThread.Create (Form1.Edit1.Text);

// However, since the thread was created suspended, then on any errors
// during its initialization (before starting), we must destroy it ourselves
// for what we use try / except block
try

// Assigning a thread termination handler in which we will receive
// the results of the work of the stream, and "overwrite" the link to it
MyThread.OnTerminate: = EventMyThreadOnTerminate;

// Since the results will be collected in OnTerminate, i.e. before self-destruction
// the stream then we will take off the worries of destroying it
MyThread.FreeOnTerminate: = True;

// An example of passing input parameters through the fields of the stream object, at the point
// instantiate when not already running.
// Personally, I prefer to do this through the parameters of the overridden
// constructor (tMyThread.Create)
MyThread.Param2: = StrToInt (Form1.Edit2.Text);

MyThread.Stopped: = False; // a kind of parameter, too, but changing in
// thread running time
except
// since the thread has not been started yet and will not be able to self-destruct, we will destroy it "manually"
FreeAndNil (MyThread);
// and then let the exception be handled as usual
raise;
end;

// Since the thread object has been successfully created and configured, it's time to start it
MyThread.Resume;

ShowMessage ("Stream started");
end;

procedure TForm1.btnStopClick (Sender: TObject);
begin
// If the thread instance still exists, then ask it to stop
// And, exactly "ask". In principle, we can also "force", but it will
// extremely emergency option, requiring a clear understanding of all this
// streaming kitchen. Therefore, it is not considered here.
if Assigned (MyThread) then
MyThread.Stopped: = True
else
ShowMessage ("The thread is not running!");
end;

procedure TForm1.EventOnSendMessageMetod (var Msg: TMessage);
begin
// method for processing a synchronous message
// in WParam the address of the tMyThread object, in LParam the current value of the LastRandom of the thread
with tMyThread (Msg.WParam) do begin
Form1.Label3.Caption: = Format ("% d% d% d",);
end;
end;

procedure TForm1.EventOnPostMessageMetod (var Msg: TMessage);
begin
// method for handling an asynchronous message
// in WParam the current IterationNo value, in LParam the current LastRandom value of the stream
Form1.Label4.Caption: = Format ("% d% d",);
end;

procedure TForm1.EventMyThreadOnTerminate (Sender: tObject);
begin
// IMPORTANT!
// The method for handling the OnTerminate event is always called in the context of the main
// thread - this is guaranteed by the tThread implementation. Therefore, in it you can freely
// use any properties and methods of any objects

// Just in case, make sure that the object instance still exists
if not Assigned (MyThread) then Exit; // if it is not there, then there is nothing to do

// get the results of the work of the thread of the instance of the thread object
Form1.Memo1.Lines.Add (Format ("The stream ended with the result% d",));
Form1.Memo1.Lines.AddStrings ((Sender as tMyThread) .ResultList);

// Destroy the reference to the stream object instance.
// Since our thread is self-destructing (FreeOnTerminate: = True)
// then after the OnTerminate handler completes, the stream object instance will be
// destroyed (Free) and all references to it will become invalid.
// So as not to accidentally run into such a link, overwrite MyThread
// Once again, I will note - we will not destroy the object, but only overwrite the link. An object
// destroy itself!
MyThread: = Nil;
end;

constructor tMyThread.Create (aParam1: String);
begin
// Create an instance of the SUSPENDED stream (see the comment when instantiating)
inherited Create (True);

// Create internal objects (if necessary)
ResultList: = tStringList.Create;

// Get initial data.

// Copy the input data passed through the parameter
Param1: = aParam1;

// An example of receiving input data from VCL components in the constructor of a stream object
// This is acceptable in this case, since the constructor is called in the context
// main thread. Therefore, VCL components can be accessed here.
// But, I don't like this, because I think it's bad when the thread knows something
// about some form there. But, what can't you do for demonstration.
Param3: = Form1.CheckBox1.Checked;
end;

destructor tMyThread.Destroy;
begin
// destruction of internal objects
FreeAndNil (ResultList);
// destroy the base tThread
inherited;
end;

procedure tMyThread.Execute;
var
t: Cardinal;
s: String;
begin
IterationNo: = 0; // counter of results (cycle number)

// In my example, the body of the thread is a loop that ends
// or by an external "request" to terminate passed through the variable parameter Stopped,
// either just by doing 5 loops
// It's more pleasant for me to write this through an "eternal" loop.

While True do begin

Inc (IterationNo); // next cycle number

LastRandom: = Random (1000); // key number - to demonstrate the transfer of parameters from the stream to the form

T: = Random (5) +1; // time for which we will fall asleep if we are not completed

// Dumb work (depending on the input parameter)
if not Param3 then
Inc (Param2)
else
Dec (Param2);

// Form an intermediate result
s: = Format ("% s% 5d% s% d% d",
);

// Add an intermediate result to the list of results
ResultList.Add (s);

//// Examples of passing an intermediate result to a form

//// Passing through a synchronized method - the classic way
//// Flaws:
//// - the method being synchronized is usually a method of the stream class (to access
//// to the fields of the stream object), but, to access the form fields, it must
//// "know" about it and its fields (objects), which is usually not very good with
//// point of view of the organization of the program.
//// - the current thread will be suspended until execution is complete
//// synchronized method.

//// Advantages:
//// - standard and versatile
//// - in a synchronized method you can use
//// all fields of the stream object.
// first, if necessary, you need to save the transmitted data in
// special fields of the object object.
SyncDataN: = IterationNo;
SyncDataS: = "Sync" + s;
// and then provide a synchronized method call
Synchronize (SyncMetod1);

//// Sending via synchronous message sending (SendMessage)
//// in this case, data can be passed both through message parameters (LastRandom),
//// and through the fields of the object, passing the address of the instance in the message parameter
//// of the stream object - Integer (Self).
//// Flaws:
//// - the thread must know the handle of the form window
//// - as with Synchronize, the current thread will be suspended until
//// finishing the message processing by the main thread
//// - requires a significant amount of CPU time for each call
//// (to switch threads) therefore very frequent call is undesirable
//// Advantages:
//// - as with Synchronize, when processing a message, you can use
//// all fields of the stream object (if, of course, its address was passed)


//// start the thread.
SendMessage (Form1.Handle, WM_USER_SendMessageMetod, Integer (Self), LastRandom);

//// Transfer via asynchronous message sending (PostMessage)
//// Since in this case, by the time the message is received by the main thread,
//// the sending stream may have finished, passing the instance address
//// Stream object is invalid!
//// Flaws:
//// - the thread must know the handle of the form window;
//// - due to asynchrony, data transfer is possible only through parameters
//// messages, which significantly complicates the transfer of data with the size
//// more than two machine words. It is convenient to use for passing Integer, etc.
//// Advantages:
//// - unlike the previous methods, the current thread will NOT
//// is paused and will immediately resume execution
//// - unlike a synchronized call, a message handler
//// is a form method that must have knowledge of the stream object,
//// or know nothing about the stream at all if the data is transmitted only
//// via message parameters. That is, the thread may not know anything about the form.
//// generally - only her Handle, which can be passed as a parameter before
//// start the thread.
PostMessage (Form1.Handle, WM_USER_PostMessageMetod, IterationNo, LastRandom);

//// Check for possible completion

// Check for completion by parameter
if Stopped then Break;

// Check for completion on occasion
if IterationNo> = 10 then Break;

Sleep (t * 1000); // Fall asleep for t seconds
end;
end;

procedure tMyThread.SyncMetod1;
begin
// this method is called through the Synchronize method.
// That is, despite the fact that it is a method of the tMyThread thread,
// it runs in the context of the main thread of the application.
// Therefore, he can do anything, well, or almost everything :)
// But remember, it is not worth "messing around" here for a long time

// The passed parameters, we can extract from the special fields, where we have them
// saved before calling.
Form1.Label1.Caption: = SyncDataS;

// either from other fields of the stream object, for example, reflecting its current state
Form1.Label2.Caption: = Format ("% d% d",);
end;

In general, the example was preceded by my following reasoning on the topic ...

Firstly:
The MOST IMPORTANT rule of multithreaded programming in Delphi is:
In the context of a non-main thread, you cannot access the properties and methods of forms, and indeed all components that "grow" from tWinControl.

This means (somewhat simplified) that neither in the Execute method inherited from TThread, nor in other methods / procedures / functions called from Execute, it is forbidden directly access any properties and methods of visual components.

How to do it right.
There are no uniform recipes. More precisely, there are so many and different options that, depending on the specific case, you need to choose. Therefore, they refer to the article. Having read and understood it, the programmer will be able to understand and how best to do it in a particular case.

In short on your fingers:

Most often, a multithreaded application becomes either when it is necessary to do some kind of long-term work, or when it is possible to simultaneously do several things that do not heavily load the processor.

In the first case, the implementation of work inside the main thread leads to a "slowdown" of the user interface - while the work is being done, the message loop is not executed. As a result, the program does not respond to user actions, and the form is not drawn, for example, after the user moves it.

In the second case, when the work involves an active exchange with the outside world, then during the forced "downtime". While waiting for receiving / sending data, you can do something else in parallel, for example, again send / receive data.

There are other cases, but less often. However, it doesn't matter. Now is not about that.

Now, how is it all written. Naturally, a certain most frequent case, somewhat generalized, is considered. So.

The work carried out in a separate thread, in the general case, has four entities (I don’t know how to call it more precisely):
1. Initial data
2. Actually the work itself (it may depend on the initial data)
3. Intermediate data (for example, information about the current state of work execution)
4. Output data (result)

Most often, visual components are used to read and display most of the data. But, as mentioned above, you cannot directly access the visual components from the stream. How to be?
The Delphi developers suggest using the Synchronize method of the TThread class. Here I will not describe how to apply it - there is the aforementioned article for this. Let me just say that its application, even the correct one, is not always justified. There are two problems:

First, the body of a method called via Synchronize is always executed in the context of the main thread, and therefore, while it is being executed, the window message loop is again not executed. Therefore, it must be executed quickly, otherwise, we will get all the same problems as with a single-threaded implementation. Ideally, a method called via Synchronize should generally only be used to access properties and methods of visual objects.

Secondly, executing a method through Synchronize is an "expensive" pleasure due to the need for two switches between threads.

Moreover, both problems are interconnected, and cause a contradiction: on the one hand, to solve the first, it is necessary to "grind" the methods called through Synchronize, and on the other, they then often have to be called, losing precious processor resources.

Therefore, as always, it is necessary to approach reasonably, and for different cases, use different ways of interaction of the flow with the outside world:

Initial data
All data that is transferred to the stream, and does not change during its operation, must be transferred even before it starts, i.e. when creating a stream. To use them in the body of a thread, you need to make a local copy of them (usually in the fields of the TThread descendant).
If there is initial data that can change while the thread is running, then such data must be accessed either through synchronized methods (methods called through Synchronize), or through the fields of the thread object (descendant of TThread). The latter requires a certain amount of caution.

Intermediate and output data
Here, again, there are several ways (in order of my preference):
- The method of asynchronous sending messages to the main application window.
It is usually used to send messages about the progress of the process to the main application window, with the transfer of a small amount of data (for example, percentage of completion)
- Method of synchronously sending messages to the main application window.
Usually used for the same purposes as asynchronous sending, but allows you to transfer a larger amount of data, without creating a separate copy.
- Synchronized methods, if possible, combining the transfer of as much data as possible into one method.
Can also be used to retrieve data from a form.
- Through the fields of the stream object, providing mutually exclusive access.
More details can be found in the article.

Eh. It didn't work out for a short time