Лабораторная работа №1
Deadlock’и и все, что с ними связано.
1) Понятие дедлока
2) Попытка реализации «хрестоматийного» примера дедлока
3) Вариант решения данной проблемы? Способы синхронизации потоков
4) А теперь сами, построение примера дедлока и попытка решения проблемы.
1) Deadlock - случай, когда два (реже более) потоков ждут высвобождения некоторого ресурса, при этом, ни один из них не может отпустить этот ресурс. По классике, схема дедлока получается в случае, если есть 2 потока, которые работают асинхронно и 2 критических ресурса, над которыми ведется работа. Наиболее ярким примером, в таком случае, будет следующая схема
Есть потоки А и Б, которые независимо друг от друга обрабатывают 2 файла. Для большей простоты задачи будем считать, что один поток переписывает информацию из 1го файла во второй (по строкам) второй поток, соответственно, делает то же самое, но информацию переносит из второго файла в первый.
Считаем, что для выполнения своей деятельности поток должен получить эксклюзивный доступ к файлу (даже на чтение). Таким образом, очень скоро может сложиться ситуация, при которой 1й файл заблокирован потоком А, второй файл заблокирован потоком Б. При этом, оба потока ждут, когда второй ресурс (второй файл, в который будет проводиться запись) будет освобожден.
Продолжая данную логику получается, что первый поток ждет, когда работу заверши второй поток, при этом, второй поток ждет завершения работы первого. Логично предположить, что в данном случае ситуация «мирного» разрешения данной ситуации не может быть.
Собственно, классический пример реализации дедлока может быть выполнен гораздо более просто, дабы не плодить много лишнего кода, приведенная в примере ситуация может быть достаточно легко воспроизведена с помощью 2х подтоков и 2х объектов локирования. В данном случае, можно считать, что каждый из объектов локирования является файлом, чтобы совсем было как в примере выше. Тогда «хрестоматийный» пример реализации того, чего реализовывать не стоит выглядит так:
|
static object object1 = new object();
static object object2 = new object();
public static void ObliviousFunction()
{
lock (object1)
{
Thread.Sleep(1000); // Wait for the blind to lead
lock (object2)
{
}
}
}
public static void BlindFunction()
{
lock (object2)
{
Thread.Sleep(1000); // Wait for oblivion
lock (object1)
{
}
}
}
static void Main()
{
Thread thread1 = new Thread((ThreadStart)ObliviousFunction);
Thread thread2 = new Thread((ThreadStart)BlindFunction);
thread1.Start();
thread2.Start();
while (true)
{
// Stare at the two threads in deadlock.
}
}
Собственно, Задание №1
Объясните, почему тут возникает «неприятная» ситуация с локированием одного потока другим?
Варианты решения проблемы:
Monitor – самое простое и тривиальное решение в данном случае. Очевидно, что использовать из всех возможностей класса Monitor можно метод TryEnter, который позволяет получать эксклюзивную блокировку объекта с некоторой задержкой. Пример кода, реализующего ту же функциональность, приведен ниже. Если скомпилировать и запустить этот код, приложение вызовет обе функции.
using System;
using System.Threading;
namespace ConsoleApplication7
{
public class TestClass
{
private static object object1 = new object();
private static object object2 = new object();
public static void ObliviousFunction()
{
Monitor.TryEnter(object1, 300);
try
{
Thread.Sleep(1000); // Wait for the blind to lead
Monitor.TryEnter(object2, 300);
try
{
Console.WriteLine("ObliviousFunction call");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
|
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
public static void BlindFunction()
{
Monitor.TryEnter(object2, 300);
try
{
Thread.Sleep(1000); // Wait for oblivion
Monitor.TryEnter(object1, 300);
try
{
Console.WriteLine("BlindFunction");
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static void Main()
{
Thread thread1 = new Thread(ObliviousFunction);
Thread thread2 = new Thread(BlindFunction);
thread1.Start();
thread2.Start();
while (true)
{
// Stare at the two threads in deadlock.
}
}
}
}
Задание №2. Разобраться, почему в данном случае все работает корректно, объяснить преподавателю, что в этот раз стало лучше.
Другой вариант решения той же проблемы – использование Semaphore для ожидания окончания блокировки критической секции. Данный объект позволяет ограничивать максимальное количество потоков в пуле. Остальные потоки, работающие с пулом, будут ждать освобождения блокировки.
Пример реализации Semaphore приведен ниже.
using System; using System.Threading; namespace ConsoleApplication7{ public class TestClass { private static object object1 = new object(); private static object object2 = new object(); private static Semaphore _pool; public static void ObliviousFunction() { Console.WriteLine("ObliviousFunction - start"); _pool.WaitOne(); Console.WriteLine("ObliviousFunction - semaphore enter"); lock(object1) { Thread.Sleep(1000); // Wait for the blind to lead lock(object2) { Console.WriteLine("ObliviousFunction call"); } } Console.WriteLine("ObliviousFunction - work done"); _pool.Release(); Console.WriteLine("ObliviousFunction - semaphore exit"); } public static void BlindFunction() { Console.WriteLine("BlindFunction - start"); _pool.WaitOne(); Console.WriteLine("BlindFunction - semaphore enter"); lock(object2) { Thread.Sleep(1000); // Wait for oblivion lock(object1) { Console.WriteLine("BlindFunction"); } } Console.WriteLine("BlindFunction - work done"); _pool.Release(); Console.WriteLine("BlindFunction - semaphore exit"); } private static void Main() { _pool = new Semaphore(1, 1); Thread thread1 = new Thread((ThreadStart)ObliviousFunction); Thread thread2 = new Thread((ThreadStart)BlindFunction); thread1.Start(); thread2.Start(); while (true) { // Stare at the two threads in deadlock. } } }}
|
Скрин работы приложения после добавления синхронизации:
Как видно из рисунка – оба потока одновременно запускаются (start). Казалось бы – не миновать беды, но срабатывает Semaphore, ограничивая количество потоков в пуле. Из рисунка видно, что второй поток начинает работу в критической секции сразу после того, как первый поток заканчивает работ.
Задание №3. Объяснить, почему сейчас снова стало все хорошо.
Практическая работа.
Реализовать приложение, которое будет в 50 потоков записывать данные в две коллекции. Обе коллекции содержат имена потоков. В качестве имени потока использовать индекс, полученный при запуске.
Объяснить, зачем и почему были использованы механизмы локирования в рамках выполнения данной задачи. Объяснить, что, в данном случае является критической секцией.