.Net developer, data scientist
В многопоточном программировании поиск ошибок и багов может быть проблематичным по сравнению с синхронным однопоточным программированием. И среди всех множества возможных ошибок отдельной группой являются ошибки типа live-lock, dead-lock и starvation. Симптомом этих ошибок является якобы зависание приложения и невозможность выполнения программной логики после определенной точки в коде. Рассмотрим каждую из этих ситуаций и попытаемся их понять.
Deadlock рассмотрим на примере 2 потоков: А и Б. Потоку А для выполнение своих заданий необходимо выполнение некоторых заданий в потоке Б. С другой стороны потоку Б для выполнения некоторых своих заданий необходимо выполнение некоторых заданий в потоке А. Получается что в некоторых ситуациях поток А ждет поток Б, а поток Б в свою очередь ожидает поток А. Иногда такое ожидание может наступать одновременно:
Во время ожидания ничего не происходит, это просто ожидание, однако в текущей ситуации такое ожидание будет продолжаться бесконечно: поток А никогда не дождется окончания операций потока Б, потому что поток Б не может дождаться окончания операций потока А. Это был классический пример dealock’а. Далее в видео будет представлен практический пример как dealock выглядит в коде.
Livelock – очень похож на deadlock, с той лишь разницей, что во время livelock’а потоки во время ожидания выполняют какие-то операции. Однако эти операции не имеют практического смысла, так как потоки несмотря на выполняемые операции и так не могут завершить свои задания. Также далее в видео будет практический пример livelock’а.
Starvation – то есть «голод» - описывает ситуацию, когда несмотря на отсутствие deadlock’ов и livelock’ов, поток не может получить доступ к определенному ресурсу и завершить свою работу. Например, есть поток, который очень часто получает доступ к эксклюзивному ресурсу и долго выполняет свои операции используя эксклюзивный ресурс. Другие потоки, несмотря на отсутствие lock’ов не могут получить доступ к эксклюзивному ресурсу и выполнить свою работу. В этом случае имеет место явление старвации, то есть «голода ресурсов».
Пример кода:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LockExample
{
class Program
{
static void Main(string[] args)
{
/*
var syncObject1 = new object();
var syncObject2 = new object();
var a = new A();
var b = new B();
Task.Run(() => a.PerformOperation(syncObject1, syncObject2));
Task.Run(() => b.PerformOperation(syncObject1, syncObject2));
*/
/*
var c = new C();
var d = new D();
var e = new E();
Task.Run(() => c.PerformOperation(e));
Task.Run(() => d.PerformOperation(e));
*/
Console.ReadLine();
/*
* Класс А
* лок 1
* лок 2
*
* Класс В
* лок 2
* лок 1
*
* Класс С
* условие Е1
* условие Е2 - если не выполняется подожди и попрбуй еще раз
*
* Класс D
* условие Е2
* условие Е1 - если не выполняется подожди и попрбуй еще раз
*
* Класс Е - класс с условиями
*/
}
}
class A
{
public void PerformOperation(object syncObject1, object syncObject2)
{
Console.WriteLine("Класс А, перед lock 1");
lock (syncObject1)
{
Console.WriteLine("Класс А, lock 1");
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
Console.WriteLine("Класс А, перед lock 2");
lock(syncObject2)
{
Console.WriteLine("Класс А, lock 2");
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
}
Console.WriteLine("Класс А, после lock 2");
}
Console.WriteLine("Класс А, после lock 1");
}
}
class B
{
public void PerformOperation(object syncObject1, object syncObject2)
{
Console.WriteLine("Класс B, перед lock 2");
lock(syncObject2)
{
Console.WriteLine("Класс B, lock 2");
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
Console.WriteLine("Класс B, перед lock 1");
lock(syncObject1)
{
Console.WriteLine("Класс B, lock 1");
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
}
Console.WriteLine("Класс B, после lock 1");
}
Console.WriteLine("Класс B, после lock 2");
}
}
class C
{
public void PerformOperation(E e)
{
while (true)
{
Console.WriteLine("Класс C, перед can procceed 1");
if (e.CanProcceed1)
{
Console.WriteLine("Класс C, can procceed 1");
e.CanProcceed1 = false;
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
while (true)
{
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
Console.WriteLine("Класс C, перед can procceed 2");
if (e.CanProcceed2)
{
e.CanProcceed2 = false;
Console.WriteLine("Класс C, can procceed 2");
break;
}
Console.WriteLine("Класс C, после can procceed 2");
}
e.CanProcceed2 = true;
}
Console.WriteLine("Класс C, после can procceed 1");
if (e.CanProcceed2)
{
e.CanProcceed1 = true;
break;
}
}
Console.WriteLine("Класс C, конец");
}
}
class D
{
public void PerformOperation(E e)
{
while (true)
{
Console.WriteLine("Класс D, перед can procceed 2");
if (e.CanProcceed2)
{
Console.WriteLine("Класс D, can procceed 2");
e.CanProcceed2 = false;
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
while (true)
{
//симуляция выполнения какого-то задания
Thread.Sleep(1000);
Console.WriteLine("Класс D, перед can procceed 1");
if (e.CanProcceed1)
{
e.CanProcceed1 = false;
Console.WriteLine("Класс D, can procceed 1");
break;
}
Console.WriteLine("Класс D, после can procceed 1");
}
e.CanProcceed1 = true;
}
Console.WriteLine("Класс D, после can procceed 2");
if (e.CanProcceed1)
{
e.CanProcceed2 = true;
break;
}
}
Console.WriteLine("Класс D, конец");
}
}
class E
{
public bool CanProcceed1 { get; set; } = true;
public bool CanProcceed2 { get; set; } = true;
}
}