Лаб_СПО_3_2015

advertisement
Лабораторная работа №3
Синхронизация потоков в Windows 2000/ХР/7
Цель: Научиться использовать функции Win32 API предназначенные для синхронизации
потоков в Windows.
1. Теория синхронизации
1.1. Цели и средства синхронизации
Одним из преимуществ использования нескольких потоков в приложении является
асинхронное выполнение каждого потока. В приложениях Windows это позволяет
выполнять длительные задачи в фоновом режиме, при этом окно приложения и элементы
управления остаются активными. Для серверных приложений многопоточность
обеспечивает возможность обработки каждого входящего запроса в отдельном потоке.
Однако очень сложно с полной определенностью сказать, на каком этапе выполнения
будет находиться процесс в определенный момент времени. Моменты прерывания
потоков, время нахождения их в очередях к разделяемым ресурсам, порядок выбора
потоков для выполнения — все эти события являются результатом стечения многих
обстоятельств и могут быть интерпретированы как случайные. В лучшем случае можно
оценить вероятностные характеристики вычислительного процесса, например вероятность
его завершения за данный период времени.
Вследствие того, что потоки асинхронны, доступ к ресурсам, таким как дескрипторы
файлов, сетевые подключения и память, должен быть скоординирован. Иначе два или
более потоков могут получить доступ к одному и тому же ресурсу одновременно, причем
один поток не будет учитывать действия другого. В результате данные могут быть
повреждены непредсказуемым образом. Это справедливо как по отношению к потокам
одного процесса, выполняющим общий программный код, так и по отношению к потокам
разных процессов, каждый из которых выполняет собственную программу.
Потоки должны взаимодействовать друг с другом в двух основных случаях:


совместно используя разделяемый ресурс (чтобы не нарушить его);
когда нужно уведомлять другие потоки о завершении каких-либо операций.
Любое взаимодействие процессов или потоков связано с их синхронизацией, которая
заключается в согласовании их скоростей путем приостановки потока до наступления
некоторого события и последующей его активизации при наступлении этого события.
Временная приостановка (блокирование) – основной способ координации, или
синхронизации действий потоков. Ожидание эксклюзивной блокировки – это одна из
причин, по которым поток может блокироваться. Другая причина – если поток
приостанавливается (Sleep) на заданный промежуток времени:
Синхронизация лежит в основе любого взаимодействия потоков, связано ли это
взаимодействие с разделением ресурсов или с обменом данными. Например, потокполучатель должен обращаться за данными только после того, как они помещены в буфер
потоком-отправителем. Если же поток-получатель обратился к данным до момента их
поступления в буфер, то он должен быть приостановлен.
Ежесекундно в системе происходят сотни событий, связанных с распределением и
освобождением ресурсов, и ОС должна иметь надежные и производительные средства,
которые бы позволяли ей синхронизировать потоки с происходящими в системе
событиями.
Для синхронизации потоков прикладных программ программист может использовать как
собственные средства и приемы синхронизации, так и средства операционной системы.
Например, два потока одного прикладного процесса могут координировать свою работу с
помощью доступной для них обоих глобальной логической переменной, которая
устанавливается в единицу при осуществлении некоторого события, например выработки
одним потоком данных, нужных для продолжения работы другого. Однако во многих
случаях более эффективными или даже единственно возможными являются средства
синхронизации, предоставляемые операционной системой в форме системных вызовов.
Так, потоки, принадлежащие разным процессам, не имеют возможности вмешиваться
каким-либо образом в работу друг друга. Без посредничества операционной системы они
не могут приостановить друг друга или оповестить о произошедшем событии. Средства
синхронизации используются операционной системой не только для синхронизации
прикладных процессов, но и для ее внутренних нужд.
1.2. Синхронизация потоков одного процесса
1.2.1. Критические секции
Критическая секция (critical section) — это участок кода, требующий монопольного
доступа к каким-то общим данным. Она позволяет сделать так, чтобы единовременно
только один поток получал доступ к критическому ресурсу. Естественно, система может в
любой момент вытеснить Ваш поток и подключить к процессору другой, но ни один из
потоков, которым нужен занятый Вами ресурс, не получит процессорное время до тех
пор, пока Ваш поток не выйдет за границы критической секции.
Выражение lock используется для того, чтобы выполнение блока кода не прерывалось
кодом, выполняемым в других потоках. Для этого нужно получить взаимоисключающую
блокировку для данного объекта на время выполнения блока кода.
Оператор lock получает объект в качестве аргумента, и за ним следует блок кода, который
должен выполняться одновременно только в одном потоке. Например:
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Process()
{
lock (lockThis)
{
// Работа с критическим ресурсом.
}
}
}
Пример.
Рассмотрим следующий класс:
class ThreadUnsafe
{
static int val1, val2;
static void Go()
{
if (val2 != 0)
Console.WriteLine(val1 / val2);
val2 = 0;
}
}
Он не является безопасным: если бы метод Go вызывался двумя потоками одновременно,
можно было бы получить ошибку деления на 0, так как переменная val2 могла быть
установлена в 0 в одном потоке, в то время когда другой поток находился бы между if и
Console.WriteLine.
Вот как при помощи блокировки можно решить эту проблему:
class ThreadSafe
{
static object locker = new object();
static int val1, val2;
static void Go()
{
lock (locker)
{
if (val2 != 0)
Console.WriteLine(val1 / val2);
val2 = 0;
}
}
}
Только один поток может единовременно заблокировать объект синхронизации (в данном
случае locker), а все другие конкурирующие потоки будут приостановлены, пока
блокировка не будет снята. Если за блокировку борются несколько потоков, они ставятся
в очередь ожидания – "ready queue" – и обслуживаются, как только это становится
возможным, по принципу “первым пришел – первым обслужен”. Эксклюзивная
блокировка, как уже говорилось, обеспечивает последовательный доступ к тому, что она
защищает, так что выполняемые потоки уже не могут наложиться друг на друга. В данном
случае мы защитили логику внутри метода Go, так же, как и поля val1 и val2.
Для того чтобы убедиться в том, что монопольный доступ к критическому ресурсу
необходим, попробуйте закомментировать оператор lock в исходном коде
рассматриваемой здесь программы. При ее последующем выполнении вы получите
совершенно другой результат.
1.2.2. Блокировки и атомарность
Если группа переменных всегда читается и записывается в пределах одной блокировки,
можно сказать, что переменные читаются и пишутся атомарно. Предположим, что поля x
и y всегда читаются и пишутся с блокировкой на объекте locker:
lock (locker)
{
if (x != 0)
y /= x;
}
Можно сказать, что доступ к x и y атомарный, так как данный кусок кода не может быть
прерван действиями другого потока, которые бы изменили x, y или результат операции.
Невозможно получить ошибку деления на ноль, если обращение к x и y производится в
эксклюзивной блокировке.
Блокировка сама по себе очень быстра: она требует десятков наносекунд, если собственно
блокирования не происходит. Если требуется блокирование, то последующее
переключение задач занимает уже микросекунды или даже миллисекунды на
перепланировку потоков.
При неправильном использовании у блокировки могут быть и негативные последствия –
уменьшение возможности параллельного исполнения потоков, взаимоблокировки, гонки
блокировок. Возможности для параллельного исполнения уменьшаются, когда слишком
много кода помещено в конструкцию lock, заставляя другие потоки простаивать все
время, пока этот код исполняется. Взаимоблокировка наступает, когда каждый из двух
потоков ожидает на блокировке другого потока и, таким образом, ни тот, ни другой не
могут двинуться дальше. Гонкой блокировок называется ситуация, когда любой из двух
потоков может первым получить блокировку, однако программа ломается, если первым
это сделает “неправильный” поток.
Оператор lock языка C# фактически является синтаксическим сокращением для вызовов
методов Monitor.Enter и Monitor.Exit в рамках блоков try-finally. Вот во что
фактически разворачивается реализация метода Go из предыдущего примера:
Monitor.Enter(locker);
try
{
if (val2 != 0)
Console.WriteLine(val1 / val2);
val2 = 0;
}
finally { Monitor.Exit(locker); }
Вызов Monitor.Exit без предшествующего вызова Monitor.Enter для того же объекта
синхронизации вызовет исключение.
также предоставляет метод TryEnter, позволяющий задать время ожидания в
миллисекундах или в виде TimeSpan. Метод возвращает true, если блокировка была
получена, и false, если блокировка не была получена за заданное время. TryEnter может
также быть вызван без параметров и в этом случае возвращает управление немедленно.
Monitor
А главный недостаток оператора lock — с его помощью нельзя синхронизировать потоки
в разных процессах.
2. Практическое задание
2.1. Проанализируйте работу программы.
Данная программа выполняет генерацию массива чисел заданного размера и его
сортировку. Обоснуйте необходимость синхронизации потоков при работе с массивом.
using System;
using
using
using
using
System.Collections.Generic;
System.Linq;
System.Text;
System.Threading;
namespace LockKeywordTask
{
class Program
{
private static object syncToken = new object();
static void Main(string[] args)
{
int[] array = CreateArray();
Console.WriteLine("Исходный массив:");
DisplayArray(array);
// Создание потоков для сортировки и отображения массива
Thread sortingThread = new Thread(SortingThreadMain);
Thread displayingThread = new Thread(DisplayingThreadMain);
sortingThread.Start(array);
Thread.Sleep(200);
displayingThread.Start(array);
}
// Точка входа потока сортировки
static void SortingThreadMain(object data)
{
// Проверка передачи корректных данных
int[] array = data as int[];
if (array == null)
{
return;
}
// Вход в критическую секцию
lock (syncToken)
{
Console.WriteLine("Сортировка....");
for (int i = 0; i < array.Length; i++)
{
for (int j = 0; j < array.Length - i - 1; j++)
{
int tmp;
if (array[j] > array[j + 1])
{
tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
Thread.Sleep(200);
}
}
}
}
}
// Точка входа потока вывода массива
private static void DisplayingThreadMain(object data)
{
int[] array = data as int[];
if (array == null)
{
return;
}
// Вход в критическую секцию
lock (syncToken)
{
DisplayArray(array);
Console.ReadKey();
}
}
// Создание массива и заполнение случайными числами от -99 до 99.
private static int[] CreateArray()
{
Console.WriteLine("Введите размерность массива");
string arraySizeString = Console.ReadLine();
int arraySize;
if (!int.TryParse(arraySizeString, out arraySize))
{
arraySize = 0;
}
int[] array = new int[arraySize];
Random rand = new Random();
for (int i = 0; i < arraySize; i++)
{
array[i] = -1 * rand.Next(99) + rand.Next(99);
}
return array;
}
private static void DisplayArray(int[] array)
{
for (int i = 0; i < array.Length; i++)
{
Console.Write(array[i] + " ");
}
Console.WriteLine();
}
}
}
2.2. Индивидуальное задание
Написать программу для консольного процесса, который состоит из двух потоков: main и
worker (см. варианты). Синхронизацию потоков выполнить с помощью оператора lock.
Поток main должен выполнить следующие действия:
1. Создать массив целых чисел, размерность и элементы которого вводятся с
консоли.
2. Создать поток worker.
3. Найти минимальный и максимальный элементы массива и вывести их на
консоль. После каждого сравнения элементов «спать» 7 миллисекунд.
4. Дождаться завершения потока worker.
5. Вывести его на консоль значение, полученное потоком worker.
6. Завершить работу.
Поток worker должен выполнить следующие действия:
1. Найти значение (согласно варианту). После каждой операции с элементом
«спать» 12 миллисекунд.
2. Завершить свою работу.
Индивидуальные варианты:
1. Поток worker должен найти значение суммы четных элементов массива.
2. Поток worker должен найти значение количества четных элементов массива.
3. Поток worker должен найти значение количества нечетных элементов массива.
4. Поток worker должен найти значение суммы нечетных элементов массива.
5. Поток worker должен найти среднее значение четных элементов массива.
6. Поток worker должен найти среднее значение нечетных элементов массива.
7.
Поток worker должен найти среднее значение положительных элементов массива.
8. Поток worker должен найти среднее значение отрицательных элементов массива.
9. Поток worker должен найти значение суммы положительных элементов массива.
10. Поток worker должен найти значение количества положительных элементов
массива.
11. Поток worker должен найти значение количества нулевых элементов массива.
12. Поток worker должен найти значение количества ненулевых элементов массива.
13. Поток worker должен найти значение суммы элементов массива с четными
индексами.
14. Поток worker должен найти значение суммы элементов массива с четными
индексами.
15. Поток worker должен найти значение количества отрицательных элементов
массива.
3. Контрольные вопросы
1. В каком случае необходима синхронизация потоков?
2. Каким образом можно обеспечить корректную работу потоков с общим ресурсом?
3. Что такое критическая секция?
4. В чем достоинства и недостатки оператора lock?
Download