Опубликован: 05.03.2013 | Доступ: свободный | Студентов: 2195 / 622 | Длительность: 07:41:00
Лекция 2:

Проблемы разработки параллельных приложений

< Лекция 1 || Лекция 2: 12 || Лекция 3 >

Проблемы синхронизации

Решение проблемы гонки данных требует применения средств синхронизации, позволяющих обеспечить взаимно-исключительный доступ к критической секции. Применение синхронизации гарантирует получение корректных результатов, но снижает быстродействие приложения. Чем больше размер критических секций в приложении, тем больше доля последовательного выполнения и ниже эффективность от распараллеливания. Для повышения быстродействия размер критической секции должен быть предельно минимальным – только те операторы, последовательность которых не должна прерываться другим потоком.

В следующем фрагменте каждый поток сохраняет в разделяемом массиве data данные из файла. Для обеспечения согласованного доступа к разделяемому ресурсу используются средства синхронизации.

 // Поток №1 
 <Вход в критическую секцию> 
   StreamReader sr = File.OpenText("file" + ThreadNum); 
   newValue = GetValue(sr); 
   data[cur_index] = newValue; 
   cur_index++; 
   sr.Close(); 
 <Выход из критической секции> 
  

Размер критической секции в этом фрагменте неоправдано большой. Действия по подготовке данных для сохранения (открытие файла, уникального для каждого потока; поиск и чтение необходимой информации) могут быть вынесены за критическую секцию:

  // Поток №1 
 StreamReader sr = File.OpenText("file" + ThreadNum); 
 newValue = GetValue(sr); 
 <Вход в критическую секцию> 
   data[cur_index] = newValue; 
   cur_index++; 
 <Выход из критической секции> 
 sr.Close(); 
  

Современные платформы для параллельного программирования, в том числе и среда Framework .NET, предлагают широкий выбор средств синхронизации. В каждой задаче применение того или иного инструмента синхронизации будет более эффективным. Например, для многопоточного увеличения разделяемого счетчика могут использоваться средства организации взаимно-исключительного доступа (объекты Monitor, Mutex), сигнальные сообщения (AutoResetEvent, ManualResetEventSlim), двоичные семафоры (Semaphore). Но максимально эффективным будет использование неблокирующих атомарных операторов (Interlocked.Increment).

Для работы с динамическими списками можно использовать как обычные коллекции с теми или иными средствами синхронизации, так и конкурентные коллекции с встроенной неблокирующей синхронизацией.

Проблемы кэшируемой памяти

Наличие кэшируемой памяти увеличивает быстродействие обработки данных, но усложняет работу системы при многопоточной обработке. Неоптимальная работа с кэшируемой памятью может сильно снизить эффективность параллельной обработки.

Кэш-память каждого процессора (ядра процессора) наполняется данными, необходимыми для работы потока, выполняющегося на этом процессоре. Если потоки работают с общими данными, то на аппаратном уровне должна обеспечиваться согласованность содержимого кэш-памяти. Изменение общей переменной в одном потоке, сигнализирует о недействительности значения переменной, загруженной в кэш-память другого процессора. При этом необходимо сохранить значение переменной в оперативной памяти и обновить кэш-память других процессоров. Большая интенсивность изменений общих переменных, которые используются в нескольких потоках, приводит к большому числу ошибок кэш-памяти (кэш-промахи) и увеличению накладных расходов, связанных с обновлением кэш-памяти.

Распространенной проблемой кэш-памяти является так называемое ложное разделение данных (false sharing). Проблема связана с тем, что потоки работают с разными переменными, которые в оперативной памяти расположены физически близко. Дело в том, что в кэш-память загружается не конкретная переменная, а блок памяти (строка кэша), содержащая необходимую переменную. Размер строки кэша может составлять 64, 128, 512 байт. Если в одной строке кэша расположены несколько переменных, используемых в разных потоках, то в кэш-память каждого процессора будет загружена одна и та же строка. При изменении в одном потоке своей переменной, содержимое кэш-памяти других процессоров считается недействительным и требует обновления.

В качестве иллюстрации проблемы ложного разделения рассмотрим следующий фрагмент. В программе объявлена структура, содержащая несколько полей.

 struct data 
 { 
   int x; 
   int y; 
 }   
  

Первый поток работает только с полем x, второй поток работает только с полем y. Таким образом, разделения данных и проблемы гонки данных между потоками нет. Но последовательное расположение в памяти структуры data, приводит к тому, что в кэш-память одного и другого процессора загружается строка размером 64 байт, содержащая значения и поля x (4 байта), и поля y (4 байта). При изменении поля в одном потоке происходит обновление строки кэша в другом потоке.

 // Поток №1 
 for(int i=0; i<N; i++) 
   data1.x++; 
     
 // Поток №2 
 for(int i=0; i<N; i++) 
   data1.y++; 
     
  

Чтобы избежать последовательного расположения полей x и y в памяти, можно использовать дополнительные промежуточные поля.

Другой подход заключается в явном выравнивании полей в памяти с помощью атрибута FieldOffsetAttribute, который определен в пространстве System.Runtime.InteropServices:

 
 // Явное выравнивание в памяти 
 [StructLayout(LayoutKind.Explicit)] 
 struct data 
 { 
   [FieldOffset(0)] public int x; 
   [FieldOffset(64)] public int y; 
 } 
  

При достаточно большом значении N, разница в быстродействии кода с разделением кэша и без разделения может достигать 1.5 – 2 раз.

Все же самым эффективным решением при независимой обработке полей структуры будет применение локальных переменных внутри каждого потока. Разница в быстродействии может достигать нескольких десятков.

Модели параллельных приложений

Существуют следующие распространенные модели параллельных приложений:

  • модель делегирования ("управляющий-рабочий");
  • сеть с равноправными узлами;
  • конвейер;
  • модель "производитель-потребитель"

Каждая модель характеризуется собственной декомпозицией работ, которая определяет, кто отвечает за порождение подзадач и при каких условиях они создаются, какие информационные зависимости между подзадачами существуют.

Модель Описание
Модель делегирования Центральный поток ("управляющий") создает "рабочие" потоки и назначает каждому из них задачу. Управляющий поток ожидает завершения работы потоков, собирает результаты.
Модель с равноправными узлами Все потоки имеют одинаковый рабочий статус.
Конвейер Конвейерный подход применяется для поэтапной обработки потока входных данных.
Модель "производитель-потребитель" Частный случай конвейера с одним производителем и одним потребителем.
< Лекция 1 || Лекция 2: 12 || Лекция 3 >