Common Intermediate Language и системное программирование в Microsoft.Net. Макаров А.В - 138 стр.

UptoLike

Обычно объекты не имеют сопоставленной с ними SyncBlock запи-
си, однако она автоматически выделяется при первом использовании
монитора.
Класс Monitor, определенный в пространстве имен System.Threading,
предлагает несколько статических методов для работы с записями синхро-
низации. Методы Enter и Exit являются наиболее применяемыми и соот-
ветствуют функциям EnterCriticalSection и LeaveCriticalSection опера-
ционной системы. Аналогично критическим секциям Win32 API, монито-
ры могут использоваться одним потоком рекурсивно. Еще несколько ме-
тодов класса Monitor Wait, Pulse и PulseAll – позволяют при необходи-
мости временно разрешить доступ к объекту другому потоку, ожидающему
его освобождения, не покидая критической секции.
Продолжим рассмотрение примера с многопоточным умножением
матриц. Помимо уже рассмотренной проблемы с назначением полос, в
процедуре потока есть еще одно некорректное место – прибавление нако-
плением к элементу результирующей матрицы произведения двух элемен-
тов исходной матрицы:
public static void ThreadProc()
{
int i,j,k, from, to;
from = (Interlocked.Increment(ref m_stripused)–1)
* m_stripsize;
to = from + m_stripsize;
if ( to > m_size ) to = m_size;
for ( i = 0; i < m_size; i++ ) {
for ( j = 0; j < m_size; j++ ) {
for ( k = from; k < to; k++ )
m_C[i,j] += m_A[i,k] * m_B[k,j];
}
}
}
Так как эта операция выполняется не атомарно, то вполне может быть
так, что один поток считывает значение m_C[i,j], прибавляет к нему вели-
чину m_A[i,k] * m_B[k,j] и, прежде чем успевает записать в m_C[i,j] резуль-
тат сложения, прерывается другим потоком. Второй поток успевает изме-
нить величину m_C[i,j], потом первый снова пробуждается и записывает
значение, вычисленное для предыдущего состояния элемента m_C[i,j], – то
есть некорректную величину. Собственно говоря, именно эта ситуация и
приводит к ошибкам, которые можно наблюдать в исходном примере.
Ситуацию можно исправить, используя синхронизацию при доступе
к элементу m_C[i,j] с помощью мониторов:
Разработка параллельных приложений для ОС Windows
263
Для этого с каждым объектом ссылочного типа сопоставляется запись
SyncBlock, являющаяся, по сути, аналогом структуры CRITICAL_SECTION в
Win32 API. Добавление такой записи к каждому объекту в управляемой ку-
че чересчур накладно, особенно если учесть, что используются они отно-
сительно редко. Поэтому все записи SyncBlock выносятся в отдельный
кэш, а в информацию об объекте включается ссылка на запись кэша
(см. рис. 7.2). Такой прием позволяет, с одной стороны, содержать кэш
синхронизирующих записей минимального размера, а с другой – любому
объекту при необходимости можно сопоставить запись.
Рис. 7.2. Использование кэша SyncBlock записей объектами управля-
емой кучи
262
CIL и системное программирование в Microsoft .NET
Управляемая куча
Объект i
Информация об объекте
...
Данные объекта
Индекс в SyncBlock кэше
Внутренние данные CLR
...
Кэш SyncBlock записей
SyncBlock[0] (пусто)
SyncBlock[1] (используется)
SyncBlock[2] (пусто)
SyncBlock[3] (пусто)
SyncBlock[4] (используется)
SyncBlock[5] (пусто)
...
Объект k
Информация об объекте
...
Данные объекта
...
Индекс в SyncBlock кэше
Объект j
Информация об объекте
...
Данные объекта
...
Индекс в SyncBlock (пусто)
Информация о типах, таблицы
указателей на методы и т.п.
262                               CIL и системное программирование в Microsoft .NET   Разработка параллельных приложений для ОС Windows                      263


Для этого с каждым объектом ссылочного типа сопоставляется запись                          Обычно объекты не имеют сопоставленной с ними SyncBlock запи-
SyncBlock, являющаяся, по сути, аналогом структуры CRITICAL_SECTION в                 си, однако она автоматически выделяется при первом использовании
Win32 API. Добавление такой записи к каждому объекту в управляемой ку-                монитора.
че чересчур накладно, особенно если учесть, что используются они отно-                     Класс Monitor, определенный в пространстве имен System.Threading,
сительно редко. Поэтому все записи SyncBlock выносятся в отдельный                    предлагает несколько статических методов для работы с записями синхро-
кэш, а в информацию об объекте включается ссылка на запись кэша                       низации. Методы Enter и Exit являются наиболее применяемыми и соот-
(см. рис. 7.2). Такой прием позволяет, с одной стороны, содержать кэш                 ветствуют функциям EnterCriticalSection и LeaveCriticalSection опера-
синхронизирующих записей минимального размера, а с другой – любому                    ционной системы. Аналогично критическим секциям Win32 API, монито-
объекту при необходимости можно сопоставить запись.                                   ры могут использоваться одним потоком рекурсивно. Еще несколько ме-
                                                                                      тодов класса Monitor – Wait, Pulse и PulseAll – позволяют при необходи-
            Управляемая куча                           Внутренние данные CLR          мости временно разрешить доступ к объекту другому потоку, ожидающему
                                                                                      его освобождения, не покидая критической секции.
      Объект i                                  Информация о типах, таблицы                Продолжим рассмотрение примера с многопоточным умножением
       Информация об объекте                    указателей на методы и т.п.           матриц. Помимо уже рассмотренной проблемы с назначением полос, в
                                                                                      процедуре потока есть еще одно некорректное место – прибавление нако-
        Индекс в SyncBlock кэше                                                       плением к элементу результирующей матрицы произведения двух элемен-
                                                                                      тов исходной матрицы:
       ...
       Данные объекта                                                                       public static void ThreadProc()
                                                                                            {
                                                                                              int     i,j,k, from, to;
      Объект j                                  Кэш SyncBlock записей                         from = (Interlocked.Increment(ref m_stripused)–1)
       Информация об объекте                         SyncBlock[0] (пусто)                        * m_stripsize;
                                                     SyncBlock[1] (используется)              to = from + m_stripsize;
       Индекс в SyncBlock (пусто)                    SyncBlock[2] (пусто)                     if ( to > m_size ) to = m_size;
                                                     SyncBlock[3] (пусто)                     for ( i = 0; i < m_size; i++ ) {
       ...                                           SyncBlock[4] (используется)                 for ( j = 0; j < m_size; j++ ) {
       Данные объекта                                SyncBlock[5] (пусто)                          for ( k = from; k < to; k++ )
       ...                                           ...                                             m_C[i,j] += m_A[i,k] * m_B[k,j];
                                                                                                 }
                                                                                              }
      Объект k
                                                                                            }
       Информация об объекте                                                                Так как эта операция выполняется не атомарно, то вполне может быть
                                                                                      так, что один поток считывает значение m_C[i,j], прибавляет к нему вели-
        Индекс в SyncBlock кэше
                                               ...                                    чину m_A[i,k] * m_B[k,j] и, прежде чем успевает записать в m_C[i,j] резуль-
       ...                                                                            тат сложения, прерывается другим потоком. Второй поток успевает изме-
       Данные объекта                                                                 нить величину m_C[i,j], потом первый снова пробуждается и записывает
       ...                                                                            значение, вычисленное для предыдущего состояния элемента m_C[i,j], – то
                                                                                      есть некорректную величину. Собственно говоря, именно эта ситуация и
                                                                                      приводит к ошибкам, которые можно наблюдать в исходном примере.
       Рис. 7.2. Использование кэша SyncBlock записей объектами управля-                    Ситуацию можно исправить, используя синхронизацию при доступе
       емой кучи                                                                      к элементу m_C[i,j] с помощью мониторов: