Menu
Is free
registration
home  /  Multimedia/ Multithreaded programs with examples. Eight Simple Rules for Developing Multithreaded Applications

Multithreaded programs with examples. Eight Simple Rules for Developing Multithreaded Applications

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 are trying 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 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, then you will also need to take measures against deadlocks, dynamic deadlocks, block 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.

end of file. This way, log entries made by different processes are never mixed. More modern Unix systems provide a special syslog service (3C) for logging.

Advantages:

  1. Ease of development. In fact, we run many copies of a single threaded application and they run independently of each other. It is possible not to use any specific multithreaded API and means of interprocess communication.
  2. High reliability. Abnormal termination of any of the processes does not affect the rest of the processes in any way.
  3. Good portability. The application will work on any multitasking OS
  4. High security. Different application processes can run on behalf of different users. Thus, you can implement the principle of least privilege, when each of the processes has only those rights that are necessary for him to work. Even if an error is found in some of the processes that allows remote code execution, an attacker will only be able to obtain the access level with which this process was executed.

Disadvantages:

  1. Not all applications can be provided in this way. For example, this architecture is suitable for a server serving static HTML pages, but not at all for a database server and many application servers.
  2. Creating and destroying processes is an expensive operation, so this architecture is not optimal for many tasks.

Unix systems take a number of steps to make creating a process and starting a new program in a process as cheap as possible. However, you need to understand that creating a thread within an existing process will always be cheaper than creating a new process.

Examples: apache 1.x (HTTP server)

Multiprocessing Applications Communicating over Sockets, Pipes, and System V IPC Message Queues

The listed means of IPC (Interprocess communication) refer to the so-called means of harmonic interprocess communication. They allow you to organize the interaction of processes and threads without using shared memory. Programming theorists are very fond of this architecture because it virtually eliminates many of the options for competition errors.

Advantages:

  1. Relative ease of development.
  2. High reliability. Abnormal termination of one of the processes causes the pipe or socket to close, and in the case of message queues, messages stop entering or retrieving from the queue. The rest of the application's processes can easily detect this error and recover from it, perhaps (but not necessarily) simply restarting the failed process.
  3. Many of these applications (especially socket-based ones) are easily redesigned to run in a distributed environment, where different components of the application run on different machines.
  4. Good portability. The application will work on most multitasking operating systems, including older Unix systems.
  5. High security. Different application processes can run on behalf of different users. Thus, you can implement the principle of least privilege, when each of the processes has only those rights that are necessary for him to work.

Even if an error is found in some of the processes that allows remote code execution, an attacker will only be able to obtain the access level with which this process was executed.

Disadvantages:

  1. This architecture is not easy to design and implement for all applications.
  2. All of the listed types of IPC tools assume serial data transmission. If random access to shared data is required, this architecture is inconvenient.
  3. Transferring data through a pipe, socket, and message queue requires system calls to be executed and data is double-copied - first from the original process's address space to the kernel's address space, then from the kernel's address space to memory target process... These are expensive operations. When transferring large amounts of data, this can become a serious problem.
  4. Most systems have limits on the total number of pipes, sockets, and IPC facilities. For example, Solaris allows a maximum of 1,024 open pipes, sockets, and files per process by default (due to the limitations of the select system call). The Solaris architectural limit is 65,536 pipes, sockets, and files per process.

    The limit on the total number of TCP / IP sockets is no more than 65536 per network interface (due to the format of TCP headers). System V IPC message queues are located in the kernel address space, so there are hard limits on the number of queues in the system and on the amount and number of messages simultaneously queued.

  5. Creating and destroying a process, and switching between processes are expensive operations. This architecture is not optimal in all cases.

Shared memory multiprocessing applications

The shared memory can be System V IPC shared memory and file-to-memory mapping. To synchronize access, you can use System V IPC semaphores, mutexes and POSIX semaphores, and when mapping files to memory, capturing sections of the file.

Advantages:

  1. Efficient random access to shared data. This architecture is suitable for implementing database servers.
  2. High tolerance. Can be ported to any operating system that supports or emulates System V IPC.
  3. Relatively high security. Different application processes can run on behalf of different users. Thus, you can implement the principle of least privilege, when each of the processes has only those rights that are necessary for him to work. However, the separation of access levels is not as strict as in the previously considered architectures.

Disadvantages:

  1. The relative complexity of the development. Access synchronization errors — so-called contention errors — are very difficult to detect when testing.

    This can result in a 3 to 5x increase in total development cost over single threaded or simpler multitasking architectures.

  2. Low reliability. Abnormal termination of any of the application's processes can leave (and often leaves) shared memory in an inconsistent state.

    This often causes the rest of the application to crash. Some applications, such as Lotus Domino, intentionally kill server-wide processes when any of them abnormally terminate.

  3. Creating and destroying a process and switching between them are expensive operations.

    Therefore, this architecture is not optimal for all applications.

  4. Under certain circumstances, the use of shared memory can lead to escalation of privileges. If an error is found in one of the processes that leads to remote code execution, it is highly likely that an attacker will be able to use it to remotely execute code in other processes of the application.

    That is, in a worst-case scenario, an attacker can gain the access level corresponding to the highest of the access levels of the application processes.

  5. Shared memory applications must run on the same physical computer, or at least on machines that have shared RAM. In fact, this limitation can be circumvented, for example by using memory-mapped shared files, but this introduces significant overhead.

In fact, this architecture combines the disadvantages of multiprocessing and multithreaded applications proper. However, a number of popular applications developed in the 80s and early 90s, before Unix standardized multithreaded APIs, use this architecture. These are many database servers, both commercial (Oracle, DB2, Lotus Domino), as well as freeware, modern versions of Sendmail and some other mail servers.

Multi-threaded applications proper

Threads or threads of an application run within a single process. The entire address space of a process is shared between threads. At first glance, it seems that this allows you to organize interaction between threads without any special APIs at all. In reality, this is not the case - if several threads work with a shared data structure or system resource, and at least one of the threads modifies this structure, then at some points in time the data will be inconsistent.

Therefore, threads must use special means to organize interaction. The most important tools are the mutual exclusion primitives (mutexes and read / write locks). Using these primitives, the programmer can ensure that no threads access shared resources while they are in an inconsistent state (this is called mutual exclusion). System V IPC, only those structures that are allocated in the shared memory segment are shared. Regular variables and normally allocated dynamic data structures are different for each process). Shared data access errors - competition errors - are very difficult to detect during testing.

  • The high cost of developing and debugging applications due to clause 1.
  • Low reliability. Destruction of data structures, such as through buffer overflows or pointer errors, affects all threads in a process and usually results in an abnormal termination of the entire process. Other fatal errors, such as division by zero in one of the threads, also usually cause all threads in the process to crash.
  • Low security. All application threads run in one process, that is, on behalf of the same user and with the same access rights. It is impossible to implement the principle of the minimum necessary privileges, the process must be executed on behalf of the user who can perform all the operations required by all threads of the application.
  • The creation of a thread is still quite an expensive operation. For each thread, its own stack is necessarily allocated, which by default occupies 1 megabyte of RAM on 32-bit architectures and 2 megabytes on 64-bit architectures, and some other resources. Therefore, this architecture is not optimal for all applications.
  • The inability to run the application on a multi-machine computing system. The techniques mentioned in the previous section, such as mapping shared files to memory, are not applicable to a multithreaded program.
  • In general, we can say that multithreaded applications have almost the same advantages and disadvantages as multiprocessing applications that use shared memory.

    However, the cost of running a multithreaded application is lower, and the development of such an application is in some respects easier than an application based on shared memory. Therefore, in recent years, multi-threaded applications have become more and more popular.

    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 the intricacies - 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 in a 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 allocated to each thread, in .NET programming, the problems with CPU exclusive grabs 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, there is often a situation where threads 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 invoke 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. Nevertheless:

    • 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!

    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 also 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.

    Clay Breshears

    Introduction

    Intel's multithreading implementation methods include four main phases: analysis, design and implementation, debugging, and performance tuning. This is the approach used to create a multi-threaded application from sequential code. Working with software during the first, third and fourth stages is covered quite widely, while the information on the implementation of the second step is clearly insufficient.

    Many books have been published on parallel algorithms and parallel computing. However, these publications mainly cover message passing, distributed memory systems, or theoretical parallel computing models that are sometimes inapplicable to real multicore platforms. If you are ready to get serious about multithreading programming, you will probably need knowledge about developing algorithms for these models. Of course, the use of these models is rather limited, so many software developers may have to implement them in practice.

    It is no exaggeration to say that the development of multithreaded applications is, first of all, a creative activity, and only then a scientific activity. In this article, you will learn about eight easy rules to help you expand your base of concurrent programming practices and improve the efficiency of threading your applications.

    Rule 1. Select the operations performed in the program code independently of each other

    Parallel processing applies only to those operations in sequential code that are performed independently of each other. A good example of how independent actions lead to a real single result is building a house. It involves workers of many specialties: carpenters, electricians, plasterers, plumbers, roofers, painters, bricklayers, gardeners, etc. Of course, some of them cannot start working before others have finished their activities (for example, roofers will not start work until the walls are built, and painters will not paint these walls if they are not plastered). But in general, we can say that all people involved in the construction act independently of each other.

    Consider another example - the work cycle of a DVD rental shop that receives orders for certain films. Orders are distributed among the employees of the point who are looking for these films in the warehouse. Naturally, if one of the workers takes a disc from the warehouse on which a film with Audrey Hepburn was recorded, this will in no way affect another worker looking for another action movie with Arnold Schwarzenegger, and even less will it affect their colleague, who is in search of discs with new season of the series "Friends". In our example, we believe that all the problems associated with the lack of films in stock were resolved before the orders arrived at the rental point, and the packaging and shipping of any order will not affect the processing of others.

    In your work, you will probably come across computations that can only be processed in a specific sequence, and not in parallel, since different iterations or steps of the loop depend on each other and must be performed in a strict order. Let's take a living example from the wild. Imagine a pregnant deer. Since bearing a fetus lasts an average of eight months, then, whatever one may say, a fawn will not appear in a month, even if eight reindeer become pregnant at the same time. However, eight reindeer at the same time would do their job perfectly if harnessed to all of them in Santa's sleigh.

    Rule 2. Apply parallelism with a low level of detail

    There are two approaches to parallel partitioning of sequential program code: bottom-up and top-down. First, at the stage of code analysis, code segments (so-called "hot" spots) are determined, which take up a significant part of the program execution time. Separating these code segments in parallel (if possible) will provide the maximum performance gain.

    The bottom-up approach implements multithreaded processing of code hot spots. If parallel splitting of the found points is not possible, you should examine the application call stack to determine other segments that are available for parallel splitting and take a long time to complete. Let's say you're working on an application for compressing graphics. Compression can be implemented using several independent parallel streams that process individual segments of the image. However, even if you managed to implement multithreading "hotspots", do not neglect the analysis of the call stack, as a result of which you can find segments available for parallel splitting at a higher level of the program code. This way, you can increase the granularity of the parallel processing.

    In the top-down approach, the work of the program code is analyzed, and its individual segments are highlighted, the execution of which leads to the completion of the entire task. If there is no clear independence of the main code segments, analyze their constituent parts to find independent computations. By analyzing the program code, you can determine the code modules that consume the most CPU time. Let's look at how to implement threading in a video encoding application. Parallel processing can be implemented at the lowest level - for independent pixels of one frame, or at a higher level - for groups of frames that can be processed independently of other groups. If an application is being written to process multiple video files at the same time, parallel splitting at this level may be even easier, and the detail will be the lowest.

    The granularity of parallel computing refers to the amount of computation that must be performed before synchronizing between threads. In other words, the less frequently synchronization occurs, the lower the granularity. Fine-grained threading computations can cause the system overhead of threading to outweigh the useful computations performed by those threads. The increase in the number of threads with the same amount of computation complicates the processing process. Low-granularity multithreading introduces less system latency and has more potential for scalability, which can be achieved with additional threads. To implement low-granularity parallel processing, it is recommended to use a top-down approach and thread at a high level in the call stack.

    Rule 3. Build scalability into your code to improve performance as the number of cores grows.

    Not so long ago, in addition to dual-core processors, quad-core ones appeared on the market. Moreover, Intel has already announced a processor with 80 cores, capable of performing a trillion floating point operations per second. Since the number of cores in processors will only grow over time, your code must have adequate potential for scalability. Scalability is a parameter by which one can judge the ability of an application to adequately respond to changes such as an increase in system resources (number of cores, memory size, bus frequency, etc.) or an increase in the amount of data. With the number of cores in future processors increasing, write scalable code that will increase in performance by increasing system resources.

    To paraphrase one of the laws of C. Northecote Parkinson, we can say that "data processing takes up all available system resources." This means that as computing resources increase (for example, the number of cores), all of them are most likely to be used to process data. Let's go back to the video compression application discussed above. The addition of additional cores to the processor is unlikely to affect the size of the frames processed - instead, the number of threads processing the frame will increase, which will lead to a decrease in the number of pixels per stream. As a result, due to the organization of additional streams, the amount of service data will increase, and the degree of parallelism granularity will decrease. Another more likely scenario is an increase in the size or number of video files that need to be encoded. In this case, the organization of additional streams that will process larger (or additional) video files will allow the entire volume of work to be divided directly at the stage where the increase took place. In turn, an application with such capabilities will have a high potential for scalability.

    Designing and implementing parallel processing using data decomposition provides increased scalability compared to using functional decomposition. The number of independent functions in the program code is most often limited and does not change during the execution of the application. Since each independent function is allocated a separate thread (and, accordingly, a processor core), then with an increase in the number of cores, additionally organized threads will not cause an increase in performance. So, parallel partitioning models with data decomposition will provide increased potential for scalability of the application due to the fact that the amount of processed data will increase with the number of processor cores.

    Even if the program code is threading independent functions, it is possible to use additional threads that are launched when the input load increases. Let's go back to the house building example discussed above. The construction's peculiar purpose is to complete a limited number of independent tasks. However, if you are instructed to build twice as many floors, you will probably want to hire additional workers in some specialties (painters, roofers, plumbers, etc.). Consequently, you need to develop applications that can adapt to the data decomposition resulting from increased workload. If your code implements functional decomposition, consider organizing additional threads as the number of processor cores increases.

    Rule 4. Use thread-safe libraries

    If you might need a library to handle data hot spots in your code, be sure to consider using out-of-the-box functions instead of your own code. In short, do not try to reinvent the wheel by developing code segments whose functions are already provided in optimized procedures from the library. Many libraries, including the Intel® Math Kernel Library (Intel® MKL) and Intel® Integrated Performance Primitives (Intel® IPP), already contain multi-threaded functionality optimized for multi-core processors.

    It is worth noting that when using procedures from the multithreaded libraries, you need to make sure that calling one or another library will not affect the normal operation of threads. That is, if procedure calls are made from two different threads, correct results should be returned from each call. If procedures refer to shared library variables and update them, a data race may occur, which will adversely affect the reliability of the calculation results. To work correctly with threads, the library procedure is added as new (that is, it does not update anything other than local variables) or synchronized to protect access to shared resources. Conclusion: before using any third-party library in your program code, read the documentation attached to it to make sure it works correctly with streams.

    Rule 5. Use an Appropriate Multithreading Model

    Suppose that functions from the multithreaded libraries are clearly not enough for the parallel division of all suitable code segments, and you had to think about the organization of threads. Don't rush to create your own (cumbersome) thread structure if the OpenMP library already contains all the functionality you need.

    The downside of explicit multithreading is the impossibility of precise thread control.

    If you only need parallel separation of resource-intensive loops, or the additional flexibility that explicit threads provide is secondary to you, then in this case it makes no sense to do extra work. The more complex the implementation of multithreading, the greater the likelihood of errors in the code and the more difficult its subsequent revision.

    The OpenMP library is focused on data decomposition and is especially well suited for threading loops working with large amounts of information. Despite the fact that only data decomposition is applicable to some applications, it is necessary to take into account additional requirements (for example, of the employer or customer), according to which the use of OpenMP is unacceptable and it remains to implement multithreading using explicit methods. In this case, OpenMP can be used for preliminary threading to estimate the potential performance gains, scalability, and approximate effort that would be required to subsequently split the code by explicitly multithreading.

    Rule 6. The result of the program code should not depend on the sequence of execution of parallel threads

    For sequential program code, it is enough to simply define an expression that will be executed after any other expression. In multi-threaded code, the order of execution of threads is not defined and depends on the instructions of the operating system scheduler. Strictly speaking, it is almost impossible to predict the sequence of threads that will be launched to perform an operation, or to determine which thread will be launched by the scheduler at a later time. Prediction is primarily used to reduce the latency of an application, especially when running on a platform with a processor with fewer cores than the number of organized threads. If a thread is blocked because it needs access to an area not written to the cache, or because it needs to execute an I / O request, the scheduler will suspend it and start the thread ready to start.

    Data race situations are an immediate result of uncertainty in thread execution scheduling. It may be wrong to assume that some thread will change the value of a shared variable before another thread reads that value. With a good luck, the order of execution of threads for a particular platform will remain the same across all launches of the application. However, the smallest changes in the state of the system (for example, the location of data on the hard disk, the speed of memory, or even a deviation from the nominal frequency of the AC power supply network) can provoke a different order of execution of threads. Thus, for program code that works correctly only with a certain sequence of threads, problems associated with "data race" situations and deadlocks are likely.

    From the point of view of performance gain, it is preferable not to restrict the order of execution of threads. A strict sequence of execution of streams is allowed only in case of emergency, determined by a predetermined criterion. In the event of such a circumstance, the threads will be launched in the order specified by the provided synchronization mechanisms. For example, imagine two friends reading a newspaper spread out on a table. First, they can read at different speeds, and second, they can read different articles. And here it doesn't matter who reads the spread of the newspaper first - in any case, he will have to wait for his friend before turning the page. At the same time, there are no restrictions on the time and order of reading articles - friends read at any speed, and synchronization between them occurs immediately when turning the page.

    Rule 7. Use local stream storage. Assign locks to specific data areas as needed

    Synchronization inevitably increases the load on the system, which in no way speeds up the process of obtaining the results of parallel computations, but ensures their correctness. Yes, synchronization is necessary, but it shouldn't be overused. To minimize synchronization, local storage of streams or allocated memory areas (for example, array elements marked with identifiers of the corresponding streams) is used.

    The need to share temporary variables by different threads is rare. Such variables must be declared or allocated locally to each thread. Variables whose values ​​are intermediate results of the execution of threads must also be declared local to the corresponding threads. Synchronization is required to sum these intermediate results in a shared memory area. To minimize the potential stress on the system, it is preferable to update this common area as little as possible. For explicit multithreading methods, there are thread local storage APIs that ensure the integrity of local data from the start of execution of one multithreaded segment of code until the start of the next segment (or during the processing of one call to a multithreaded function until the next execution of the same function).

    If it is not possible to store streams locally, access to shared resources is synchronized using various objects, such as locks. In this case, it is important to correctly assign locks to specific data blocks, which is easiest to do if the number of locks is equal to the number of data blocks. A single locking mechanism that synchronizes access to multiple areas of memory is used only when all these areas are constantly in the same critical section of the program code.

    What to do if you need to synchronize access to a large amount of data, for example, to an array of 10,000 elements? Providing a single lock for the entire array is definitely a bottleneck in the application. Do you really have to organize locking for each element separately? Then, even if 32 or 64 parallel threads will access the data, you will have to prevent access conflicts to a fairly large memory area, and the probability of such conflicts is 1%. Fortunately, there is a kind of golden mean, the so-called "modulo locks". If N modulo locks are used, each will synchronize access to the Nth part of the shared data area. For example, if two such locks are organized, one of them will prevent access to even elements of the array, and the other - to odd ones. In this case, threads, referring to the required element, determine its parity and set the appropriate lock. The number of locks modulo is selected taking into account the number of threads and the probability of simultaneous access by several threads to the same memory area.

    Note that the simultaneous use of several locking mechanisms is not allowed to synchronize access to one memory area. Let us recall Segal's law: “A person who has one watch knows for sure what time it is. A person who has a few watches is not sure of anything. " Let's assume that two different locks control access to a variable. In this case, the first lock can be used by one segment of the code, and the second by another segment. Then the threads executing these segments will find themselves in a race situation for the shared data they are accessing at the same time.

    Rule 8. Change the software algorithm if required to implement multithreading

    The criterion for evaluating the performance of applications, both sequential and parallel, is the execution time. As an estimate of the algorithm, an asymptotic order is suitable. This theoretical metric is almost always useful for evaluating the performance of an application. That is, all other things being equal, an application with a growth rate of O (n log n) (quicksort) will run faster than an application with a growth rate of O (n2) (selective sort), although the results of these applications are the same.

    The better the asymptotic order of execution, the faster the parallel application runs. However, even the most efficient sequential algorithm cannot always be split into parallel streams. If a program's hot spot is too difficult to split, and there is no way to multithread at a higher level of the hot spot call stack, you should first consider using a different sequential algorithm that is easier to split than the original one. Of course, there are other ways to prepare your code for threading.

    As an illustration of the last statement, consider the multiplication of two square matrices. Strassen's algorithm has one of the best asymptotic execution orders: O (n2.81), which is much better than the O (n3) order of the ordinary triple nested loop algorithm. According to Strassen's algorithm, each matrix is ​​divided into four sub-matrices, after which seven recursive calls are made to multiply n / 2 × n / 2 sub-matrices. To parallelize recursive calls, you can create a new thread that will sequentially perform seven independent multiplications of submatrices until they reach the specified size. In this case, the number of threads will grow exponentially, and the granularity of the computations performed by each newly formed thread will increase with decreasing size of the submatrices. Let's consider another option - organizing a pool of seven threads working simultaneously and performing one multiplication of submatrices. Upon termination of the thread pool, the Strassen method is recursively called to multiply the submatrices (as in the sequential version of the program code). If the system running such a program has more than eight processor cores, some of them will be idle.

    The matrix multiplication algorithm is much easier to parallelize using a nested ternary loop. In this case, data decomposition is applied, in which matrices are divided into rows, columns or submatrices, and each of the threads performs certain calculations. The implementation of such an algorithm is carried out using OpenMP pragmas inserted at some level of the loop, or by explicitly organizing threads that perform matrix division. The implementation of this simpler sequential algorithm will require much less modifications in the program code, compared to the implementation of the multithreaded Strassen algorithm.

    So, now you know eight simple rules for effectively converting sequential code to parallel. By following these guidelines, you will be able to create multithreaded solutions significantly faster, with increased reliability, optimal performance, and fewer bottlenecks.

    To return to the multithreaded programming tutorials web page, go to