|
Модераторы: Snowy, bartram, MetalFan, bems, Poseidon, Riply |
|
Петрович |
|
||||||||||||
Эксперт Профиль Группа: Участник Клуба Сообщений: 1000 Регистрация: 2.12.2003 Где: Москва Репутация: 15 Всего: 55 |
Глава 12. Еще о возможностях синхронизации в Win32.
Содержание:
Повышенная эффективность с помощью операций взаимоблокировки (interlocked). Стандартные примитивы синхронизации могут ввести излишние ограничения для простых многопоточных систем, особенно для потоков, которые интенсивно синхронизируются друг с другом. Одна из возможных альтернатив - использование interlocked операций. Interlocked (взаимосвязанные, взаимоблокировочные) операции первоначально были задуманы как механизм синхронизации низкого уровня для симметричных многопроцессорных систем с разделяемой памятью. Во многопроцессорных системах общая память представляет собой чрезвычайно эффективный путь обмена данными между процессами и потоками. Необходимо обеспечить методы решения проблем атомарности, когда два или более процессоров пытаются использовать один и тот же участок памяти. Почти все современные процессоры поддерживают соответствующие операции взаимоблокировки. Эти такие операции, посредством которыхо процессор может прочитать значение из памяти, модифицировать его, а затем записать атомарно, причем гарантируется, что другие процессоры не получит доступ к тому же участку памяти, а процессор, выполняющий операцию, не будет прерван. Win32 обеспечивает следующие операции взаимоблокировки:
Поток захватывает замок, если при выполнении приращения InterlockedIncrement значение Lock будет нулевым. Если эта величина больше нуля, то замок захвачен другим потоком, и требуется новая попытка. Вызов Sleep нужен, чтобы один поток не ждал впустую слишком долго, пока поток с более низким приоритетом удерживает замок. Для простых планировщиков, если приоритеты потоков равны, то вызов Sleep может и не потребоваться. Операция блокировки необходима, поскольку если поток выполняет чтение значения из памяти, увеличение его, сравнение, а затем записывает назад, то два потока могут захватить замок одновременно. Излишние действия почти исключены, поскольку лишь несколько инструкций CPU требуется для входа и выхода из замка, а потоку не приходится ждать. Если потокам нужно ожидать существенное время, то процессор работает впустую, так что это подходит только для создания небольших критических секций. Замки полезны для реализации критических секций, которые сами являются частью структур синхронизации. Коллективные данные внутри примитивов синхронизации или планировщиков часто защищены блокировкой подобного типа: часто это необходимо, поскольку примитивы синхронизации уровня OС не могут быть использованы, чтобы осуществлять примитивы синхронизации уровня OС. У такой блокировки имеются те же проблемы с конкуренцией потоков, как и у мьютексов, но отличие состоит в том, что зацикливание происходит не путем замораживания (deadlock), а динамически (livelock). Это несколько худшая ситуация, потому что хотя "блокированные" потоки не выполняют полезного кода, они работают в бесконечном цикле, расходуя время процессора и понижая производительность всей системы. Замки нельзя использовать как семафоры, чтобы "приостанавливать" потоки. Атомарность ниоткуда. При достаточной аккуратности возможно создать замок, который является атомарным, вообще не прибегая к взаимоблокировке, при условии, что прерывания будут происходить только между инструкциями CPU. Рассмотрим код
Сначала обратим внимание на код на Паскале, чтобы понять основную идею. У нас есть замок - целое число в памяти. При попытке войти в замок мы сначала увеличиваем значение в памяти. Затем читаем значение из памяти в локальную переменную, и проверяем, как и раньше, больше ли оно нуля. Если это так, то кто-то еще обладает этим замком, и мы возвращаемся к началу, в противном случае мы захватываем замок. Самое важное в этом наборе операций то, что при определенных условиях переключение потоков может произойти в любой момент времени, но потокобезопасность все же сохранится. Первое приращение замка является косвенным приращением регистра. Значение всегда находится в памяти, и приращение атомарно. Затем мы читаем значение замка в локальную переменную. Это действие не атомарное. Значение, прочитанное в локальную переменную, может отличаться от результата приращения. Тем не менее, хитрость состоит в том, что поскольку приращение выполняется перед действием чтения, происходящие конфликты потоков всегда будут приводить к слишком высокому прочитанному значению, а не к слишком низкому: в результате конфликтов потоков можно узнать, свободен ли замок. Часто полезно писать подобные действия на ассемблере, чтобы быть полностью уверенным, что правильные значения останутся в памяти, а не кешируются в регистрах. Компилятор Delphi (по крайней мере Delphi 4), при передаче замка как var-параметра, и с использованием локальной переменной, генерирует корректный код, который будет работать на однопроцессорных машинах. На многопроцессорных машинах косвенные приращения и декременты регистра не атомарны. Эта проблема решена в ассемблерной версии кода путем добавления префикса lock перед инструкциями, которые имеют дело с замком. Этот префикс указывает процессору исключительно заблокировать шину памяти на все время выполнения инструкции, таким образом этими операции становятся атомарными. Плохо только, что хотя это и теоретически правильно, виртуальная машина Win32 не позволяет процессам пользовательского уровня исполнять инструкции с префиксом lock. Программисты, предполагающие действительно применять этот механизм, должны использовать его только в коде с привилегиями нулевого кольца (ring 0). Другая проблема состоит в том, что поскольку эта версия блокировки не вызывает Sleep, потоки способны монополизировать процессор, пока они ожидают снятия блокировки, а это гарантирует зависание машины. Счетчики событий и секвенсоры. Одна из альтернатив для семафоров - использовать два новых вида примитивов: eventcounts и sequencers. Оба они содержат счетчики, но, в отличие от семафоров, с момента их создания счетчики неограниченно возрастают. Некоторым очень нравится идея того, что можно различить 32-е и 33-е появление события в системе. Значения этих счетчиков сделаны доступными для использующих их потоков, и могут быть использованы процессами для упорядочения операций. Счетчики событий поддерживают три действия:
Теперь довольно легко использовать счетчики событий и секвенсоры для осуществления всех операций, которые можно выполнять с применением семафоров: Реализация взаимного исключения
Ограниченный буфер с одним поставщиком данных и одним потребителем
Ограниченный буфер с произвольным числом поставщиков и потребителей
Одно из преимуществ этого типа примитива синхронизации состоит в том, что операции Advance и Ticket могут быть очень просто реализованы с использованием взаимоблокирующих инструкций сравнения и обмена. Оставим это читателю как несколько более трудное упражнение. Другие возможности синхронизации Win32. Waitable timers (ожидаемые таймеры). В Windows NT и Win2K имеются объекты ожидаемых таймеров. Они позволяют потоку или набору потоков определенное время ждать объекта таймера. Таймеры можно использовать для освобождения одного или некоторого числа потоков на повременной основе; разновидность контроля потоков данных. Вдобавок, задержка, обеспечиваемая ожидаемыми таймерами, может быть установлена с очень высокой точностью: наименьшее ее возможное значение около 100 наносекунд, что делает эти таймеры предпочтительнее использования Sleep, если поток следует приостановить на определенное время. MessageWaits. Когда приложения Delphi ожидают выхода из потоков, главный поток VCL постоянно блокирован. Потенциально эта ситуация может вызывать проблемы, потому что поток VCL не может обрабатывать сообщения. Win32 предоставляет функцию MsgWaitForMultipleObjects для преодоления этих проблем. Поток, осуществляющий ожидание сообщения, блокируется до тех пор, пока или объект синхронизации не перейдет в сигнализированное состояние, или сообщение не будет помещено в очередь сообщений потока. Это означает, что вы можете заставить главный поток VCL ожидать рабочие потоки,и в то же время позволить ему отвечать на сообщения Windows. Хорошую статью по данной теме можно найти по адресу: http://www.midnightbeach.com/jon/pubs/MsgWaits/MsgWaits.html -------------------- Все знать невозможно, но хочется |
||||||||||||
|
|||||||||||||
Петрович |
|
||||||||||||||||||||||||
Эксперт Профиль Группа: Участник Клуба Сообщений: 1000 Регистрация: 2.12.2003 Где: Москва Репутация: 15 Всего: 55 |
Chapter 13. Using threads in conjunction with the BDE, Exceptions and DLLs.
In this chapter:
DLL's and Multiprocess programming. Dynamic link libraries, or DLL's allow a programmer to share executable code between several processes. They are commonly used to provide shared library code. for several programs. Writing code for DLL's is in most respects similar to writing code for executables. Despite this, the shared nature of DLL's means that programmers familiar with multithreading often use them to provide system wide services: that is code which affects several processes that have the DLL loaded. In this chapter, we will look at how to write code for DLL's that operates across more than one process. Thread and process scope. A single threaded DLL. Global variables in DLL's have process wide scope. This means that if two separate processes have a DLL loaded, all the global variables in the DLL are local to that process. This is not limited to variables in the users code: it also includes all global variables in the Borland run time libraries, and any units used by code in the DLL. This has the advantage that novice DLL programmers can treat DLL programming in the same way as executable programming: if a DLL contains a global variable, then each process has its own copy. Furthermore, this also means that if a DLL is invoked by a processes which contain only one thread, then no special techniques are required: the DLL need not be thread safe, since all the processes have completely isolated incarnations of the DLL. We can demonstrate this with a simple DLL which does nothing but store an integer.
It exports a couple of functions that enable an application to read and write the value of that integer. We can then write a simple test application which uses this DLL.
If several copies of the application are executed, one notes that each application uses its own integer, and no interference exists between any of the applications. Writing a multithreaded DLL. Writing a multithreaded DLL is mostly the same as writing multithreaded code in an application. The behaviour of multiple threads inside the DLL is the same as the behaviour of multiple threads in a particular application. As always, there are a couple of pitfalls for the unwary: The main pitfall one can fall into is the behaviour of the Delphi memory manager. By default, the Delphi memory manager is not thread safe. This is for efficiency reasons: if a program only ever contains one thread, then it is pure wasted overhead to include synchronization in the memory manager. The Delphi memory manager can be made thread safe by setting the IsMultiThread variable to true. This is done automatically for a given module if a descendant class of TThread is created. The problem is that an executable and the DLL consist of two separate modules, each with their own copy of the Delphi memory manager. Thus, if an executable creates several threads, its memory manager is multithreaded. However, if those two threads call a DLL loaded by the executable, the DLL memory manager is not aware of the fact that it is being called by multiple threads. This can be solved by setting the IsMultiThread variable. It is best to set this by using the DLL entry point function, covered later. The second pitfall occurs as a result of the same problem; that of having two separate memory managers. Memory allocated by the Delphi memory manager that is passed from the DLL to the executable cannot be allocated in one and disposed of in the other. This occurs most often with long strings, but can occur with memory allocated using New or GetMem, and disposed using Dispose or FreeMem. The solution in this case is to include ShareMem, a unit which keeps the two memory managers in step using techniques discussed later. DLL Set-up and Tear down. Mindful of the fact that DLL programmers often need to be aware of how many threads and processes are active in a DLL at any given time, the Win32 system architects provide a method for DLL programmers to keep track of thread and process counts in a DLL. This method is known as the DLL Entry Point Function. In an executable, the entry point (as specified in the module header) indicates where program execution should start. In a DLL, it points to a function that is executed whenever an executable loads or unloads the DLL, or whenever an executable that is currently using the DLL creates or destroys a thread. The function takes a single integer argument which can be one of the following values:
Pitfall 1: The Delphi encapsulation of the Entry Point Function. Delphi uses the DLL entry point function to manage initialization and finalization of units within a DLL as well as execution of the main body of DLL code. The DLL writer can put a hook into the Delphi handling by assigning an appropriate function to the variable DLLProc. The default Delphi handling works as follows:
In case the reader is still confused, I'll present an example. Here is a modified DLL with a function that displays a message.
that contains a unit
As you can see, the main body, unit initialization and DLL entry point hooks all contain "ShowMessage" calls which enable one to trace what is going on. In order to test this DLL, here is a test application. It consists of a form with a button on.
When the button is clicked, a thread is created, which calls the procedure in the DLL, and then destroys itself.
So, what happens when we run the program?
Writing a multiprocess DLL. Armed with a knowledge of how to use the entry point function, we will now write a multiprocess DLL. This DLL will store some information on a system wide basis using memory shared between processes. It is worth remembering that when code accesses data shared between processes, the programmer must provide appropriate synchronization. Just as multiple threads in a single process are not inherently synchronized, so the main threads in different processes are also not synchronized. We will also look at some subtleties which occur when trying to use the entry point function to keep track of global threads. This DLL will share a single integer between processes, as well as keeping a count of the number of processes and threads in the DLL at any one time. It consists of a header file:
shared between the DLL and applications that use the DLL, and the DLL project file
Before we look more closely at the code, it's worth reviewing some Win32 behaviour. Global named objects. The Win32 API allows the programmer to create various objects. For some of these objects, they may be created either anonymously, or with a certain name. Objects created anonymously are, on the whole, limited to use by a single process, the exception being that they may be inherited by child processes. Objects created with a name can be shared between processes. Typically, one process will create the object, specifying a name for that object, and other processes will open a handle to that object by specifying its name. The delightful thing about named objects is that handles to these objects are reference counted throughout the system. That is, several processes can acquire handles to an object, and when all the handles to that object are closed, the object itself is destroyed, and not before. This includes the situation where an application crashes: typically windows does a good job of cleaning up unused handles after a crash. The DLL in detail. Our DLL uses this property to maintain a memory mapped file. Normally, memory mapped files are used to create an area of memory which is a mirror image of a file on disk. This has many useful applications, not least "on demand" paging in of executable images from disk. For this DLL however, a special case is used whereby a memory mapped file is created with no corresponding disk image. This allows the programmer to allocate a section of memory which is shared between several processes. This is surprisingly efficient: once the mapping is set up, no memory copying is done between processes. Once the memory mapped file has been set up, a global named mutex is used to synchronize access to that portion of memory. DLL Initialization. Initialization consists of four main stages:
In the second stage the area of shared memory is set up. Since we have already set up the global mutex, it is used when setting up the file mapping. A view of the "file" is mapped, which maps the (virtual) file into the address space of the calling process. We also check whether we happened to be the process that originally created the file mapping, and if this is the case, then we zero out the data in our mapped view. This is why the procedure is wrapped in a mutex: CreateFileMapping has the same nice atomicity properties as CreateMutex, ensuring that race conditions on handles will never occur. In the general case, however, the same is not necessarily true for the data in the mapping. If the mapping had a backing file, then we might be able to assume validity of the shared data at start-up. For virtual mappings this is not assured. In this case we need to initialize the data in the mapping atomically with setting up a handle to the mapping, hence the mutex. In the third stage, we perform our first manipulation on the globally shared data, by incrementing the process and thread counts, since the execution of the main body of the DLL is consistent with the addition of another thread and process to those using the DLL. Note that the AtomicIncThreadCount procedure increments both the local and global threads counts whilst both the global mutex and process local critical section have been acquired. This ensures that multiple threads from the same process see a fully consistent view of both counts. In the final stage, the DLLProc is hooked, thus ensuring that the creation and destruction of other threads in the process is monitored, and the final exit of the process is also registered. An application using the DLL. A simple application that uses the DLL is presented here. It consists of the global shared unit
, a unit containing the main form
and a subsidiary unit containing a simple thread
Five buttons exist on the form, allowing the user to read the data contained in the DLL, increment, decrement and set the shared integer, and create one or more threads within the application, just to verify that local thread counts work. As expected, the thread counts increment whenever a new copy of the application is executed, or one of the applications creates a thread. Note that the thread need not directly use the DLL in order for the DLL to be informed of its presence. Pitfall 2: Thread context in Entry Point Functions. Instead of using a simple application, let's try one that does something more advanced. In this situation, the DLL is loaded manually by the application programmer, instead of being automatically loaded. This is possible by replacing the previous form unit with this one
An extra button is added which loads the DLL, and sets up the procedure addressed manually. Try running the program, creating several threads and then loading the DLL. You should find that the DLL no longer correctly keeps track of the number of threads in the various processes that use it. Why is this? The Win32 help file states that when using the entry point function with the arguments DLL_THREAD_ATTACH and DLL_THREAD_DETACH: "DLL_THREAD_ATTACH Indicates that the current process is creating a new thread. When this occurs, the system calls the entry-point function of all DLLs currently attached to the process. The call is made in the context of the new thread. DLLs can use this opportunity to initialize a TLS slot for the thread. A thread calling the DLL entry-point function with the DLL_PROCESS_ATTACH value does not call the DLL entry-point function with the DLL_THREAD_ATTACH value. Note that a DLL's entry-point function is called with this value only by threads created after the DLL is attached to the process. When a DLL is attached by LoadLibrary, existing threads do not call the entry-point function of the newly loaded DLL." It drives the point home by also stating: "DLL_THREAD_DETACH Indicates that a thread is exiting cleanly. If the DLL has stored a pointer to allocated memory in a TLS slot, it uses this opportunity to free the memory. The operating system calls the entry-point function of all currently loaded DLLs with this value. The call is made in the context of the exiting thread. There are cases in which the entry-point function is called for a terminating thread even if the DLL never attached to the thread.
Exception Handling. When writing robust applications, the programmer should always be prepared for things to go wrong. The same is true for multithreaded programming. Most of the examples presented in this tutorial have been relatively simple, and exception handling has mostly been omitted for clarity. In real world applications, this is likely to be unacceptable. Recall that threads have their own call stack. This means that an exception in a thread does not fall through the standard VCL exception handling mechanisms. Instead of raising a user-friendly dialog box, and an unhandled exception in a thread will terminate the application. As a result of this, the execute method of a thread is one of the few places where it can be useful to create an exception handler that catches all exceptions. Once an exception has been caught in a thread, dealing with it is also slightly different from ordinary VCL handling. It may not always be appropriate to show a dialog box. Quite often, a valid tactic is to let the thread communicate the fact that a failure has occurred to the main VCL thread, using whatever communication mechanisms are in place, and then let the VCL thread decide what to do. This is particularly useful if the VCL thread has created the child thread to perform a particular operation. Despite this, there are some situations in threads where dealing with error cases can be particularly difficult. Most of these situations occur when using threads to perform continuous background operations. Recalling chapter 10, the BAB has a couple of threads that forward read and write operations from the VCL thread to a blocking buffer. If an error occurs in either of these threads, the error may show no clear causal relationship with any particular operation in the VCL thread, and it may be difficult to communicate failure instantly back to the VCL thread. Not only this, but an exception in either of these threads is likely to break them out of the read or write loop that they are in, raising the difficult question of whether these threads can be usefully restarted. About the best that can be done is to set some state indicating that all future operations should be failed, forcing the main thread to destroy and re-initialize the buffer. The best solution is to include the possibility of such problems into the original application design, and to determine best effort recovery attempts that may be made. The BDE. In Chapter 7, I indicated that one potential solution to locking problems is to put shared data in a database, and use the BDE to perform concurrency control. The programmer should note that each thread must maintain a separate database connection for this to work properly. Hence, each thread should use a separate TSession object to manage its connection to the database. Each application has a TSessionList component called Sessions to enable this to be done easily. Detailed explanation of multiple sessions is beyond the scope of this document. -------------------- Все знать невозможно, но хочется |
||||||||||||||||||||||||
|
|||||||||||||||||||||||||
Петрович |
|
||||
Эксперт Профиль Группа: Участник Клуба Сообщений: 1000 Регистрация: 2.12.2003 Где: Москва Репутация: 15 Всего: 55 |
Chapter 14. A real world problem, and its solution.
In this chapter:
The problem. Over the past couple of years I have been writing a distributed raytracer. This uses TCP/IP to send descriptions of scenes to be rendered across a network from a central server to a collection of clients. The clients render the image, and then return the data to the server. Some beta testers were interested in trying the program out, but mentioned that they did not have a TCP/IP stack loaded on their machine. I decided that it would be useful to write some code that emulated TCP sockets, allowing communication between two applications (both client and server) on the local machine. Various potential solutions were investigated. The most promising at first seemed to be to use named pipes. Unfortunately a problem soon cropped up: The protocols I was using on top of TCP/IP assumed that connection semantics could be performed on a strictly peer to peer basis: either program could initiate a connection to the other, and either program could disconnect at any time. Both connection and disconnection were perfectly symmetrical: The protocols used on top of TCP performed a three way handshake over and above that performed at the TCP layer to negotiate whether a connection could be closed, and that having occured, either end could close the connection. Unfortunately, named pipes did not provide the correct disconnection semantics, and they did not cope well with various error situations. The solution. I do not intend to explain the solution in detail, but more advanced readers may find the code interesting reading. In the end, I decided to use shared memory for data transfer, and to implement all synchronisation from the ground up. The solution was implemented in 3 stages.
The pipe DLL and interface files. MCHPipe.dpr:
mchpipeinterface2.pas
Это сообщение отредактировал(а) Петрович - 1.8.2005, 13:22 -------------------- Все знать невозможно, но хочется |
||||
|
|||||
Петрович |
|
||||||||||
Эксперт Профиль Группа: Участник Клуба Сообщений: 1000 Регистрация: 2.12.2003 Где: Москва Репутация: 15 Всего: 55 |
mchpipetypes.pas
This DLL is similar to the bounded buffer example found in chapter 9. Looking back on this code, I can only presume that I'd written it after a couple of weeks frantic hacking in C at work, because it's far more convoluted than it needs to be. One point of interest is that the semaphores used for blocking operations do not assume that the bounded buffers are any particular size; instead state is kept on whether the reader or writer threads are blocked or not. The reader and writer threads. mchpipethreads.pas
mchmemorystream.pas
The pipe threads are exactly analogous to the reader and writer threads in the BAB in chapter 10. Notifications are not used for write operations, instead, the writer thread buffers the data internally. This was allowable given the semantics of higher layer protocols. A socket based interface. mchpipesocket.pas
mchpipetypes.pas
This DLL is similar to the bounded buffer example found in chapter 9. Looking back on this code, I can only presume that I'd written it after a couple of weeks frantic hacking in C at work, because it's far more convoluted than it needs to be. One point of interest is that the semaphores used for blocking operations do not assume that the bounded buffers are any particular size; instead state is kept on whether the reader or writer threads are blocked or not. Это сообщение отредактировал(а) Петрович - 31.7.2005, 22:33 -------------------- Все знать невозможно, но хочется |
||||||||||
|
|||||||||||
Петрович |
|
||||||
Эксперт Профиль Группа: Участник Клуба Сообщений: 1000 Регистрация: 2.12.2003 Где: Москва Репутация: 15 Всего: 55 |
The reader and writer threads.
mchpipethreads.pas
mchpipetypes.pas
This DLL is similar to the bounded buffer example found in chapter 9. Looking back on this code, I can only presume that I'd written it after a couple of weeks frantic hacking in C at work, because it's far more convoluted than it needs to be. One point of interest is that the semaphores used for blocking operations do not assume that the bounded buffers are any particular size; instead state is kept on whether the reader or writer threads are blocked or not. The reader and writer threads. mchpipethreads.pas
-------------------- Все знать невозможно, но хочется |
||||||
|
|||||||
Правила форума "Delphi: WinAPI и системное программирование" | |
|
Запрещено: 1. Публиковать ссылки на вскрытые компоненты 2. Обсуждать взлом компонентов и делиться вскрытыми компонентами
Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Snowy, bartram, MetalFan, bems, Poseidon, Rrader, Riply. |
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей) | |
0 Пользователей: | |
« Предыдущая тема | Delphi: WinAPI и системное программирование | Следующая тема » |
|
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности Powered by Invision Power Board(R) 1.3 © 2003 IPS, Inc. |