Взаимная
блокировка
В процессе
синхронизации блокировка
устанавливается для объектов, а не
потоков, поэтому при использовании разных
объектов для блокировки разных фрагментов
кода в программах иногда возникают
весьма нетривиальные ошибки. К
сожалению, во многих случаях
синхронизация по одному объекту
просто недопустима, поскольку она
приведет к слишком частой
блокировке потоков.
Рассмотрим
ситуацию взаимной блокировки (deadlock)
в простейшем виде. Представьте себе
двух программистов за обеденным
столом. К сожалению, на двоих у них
только один нож и одна вилка. Если
предположить, что для еды нужны и
нож и вилка, возможны две ситуации:
В многопоточной
программе подобная ситуация
называется взаимной блокировкой. Два
метода синхронизируются по разным
объектам. Поток А захватывает
объект 1 и входит во фрагмент
программы, защищенный этим
объектом. К сожалению, для работы
ему необходим доступ к коду,
защищенному другим блоком Sync Lock с
другим объектом синхронизации. Но
прежде, чем он успевает войти во
фрагмент, синхронизируемый другим
объектом, в него входит поток В и
захватывает этот объект. Теперь
поток А не может войти во второй
фрагмент, поток В не может войти в
первый фрагмент, и оба потока
обречены на бесконечное ожидание.
Ни один поток не может продолжить
работу, поскольку необходимый для
этого объект так и не будет
освобожден.
Диагностика
взаимных блокировок затрудняется
тем, что они могут возникать в отно-сительно
редких случаях. Все зависит от того,
в каком порядке планировщик
выделит им процессорное время.
Вполне возможно, что в большинстве
случаев объекты синхронизации
будут захватываться в порядке, не
приводящем к взаимной блокировке.
Ниже приведена
реализация только что описанной
ситуации взаимной блокировки.
После краткого обсуждения наиболее
принципиальных моментов мы покажем,
как опознать ситуацию взаимной
блокировки в окне потоков:
1 Option Strict On
2 Imports System.Threading
3 Module Modulel
4 Sub Main()
5 Dim Tom As New
Programmer( "Tom")
6 Dim Bob As New
Programmer( "Bob")
7 Dim aThreadStart
As New ThreadStart(AddressOf Tom.Eat)
8 Dim aThread As New
Thread(aThreadStart)
9 aThread.Name=
"Tom"
10 Dim bThreadStart
As New ThreadStarttAddressOf Bob.Eat)
11 Dim bThread As
New Thread(bThreadStart)
12 bThread.Name =
"Bob"
13 aThread.Start()
14 bThread.Start()
15 End Sub
16 End Module
17 Public Class Fork
18 Private Shared
mForkAvaiTable As Boolean = True
19 Private Shared
mOwner As String = "Nobody"
20 Private Readonly
Property OwnsUtensil() As String
21 Get
22 Return mOwner
23 End Get
24 End Property
25 Public Sub
GrabForktByVal a As Programmer)
26 Console.Writel_ine(Thread.CurrentThread.Name &_
"trying to grab
the fork.")
27 Console.WriteLine(Me.OwnsUtensil
& "has the fork.") . .
28 Monitor.Enter(Me)
'SyncLock (aFork)'
29 If mForkAvailable
Then
30 a.HasFork = True
31 mOwner = a.MyName
32 mForkAvailable =
False
33 Console.WriteLine(a.MyName&"just
got the fork.waiting")
34 Try
Thread.Sleep(100) Catch e As Exception Console.WriteLine (e.StackTrace)
End Try
35 End If
36 Monitor.Exit(Me)
End SyncLock
37 End Sub
38 End Class
39 Public Class
Knife
40 Private Shared
mKnifeAvailable As Boolean = True
41 Private Shared
mOwner As String ="Nobody"
42 Private Readonly
Property OwnsUtensi1() As String
43 Get
44 Return mOwner
45 End Get
46 End Property
47 Public Sub
GrabKnifetByVal a As Programmer)
48 Console.WriteLine(Thread.CurrentThread.Name & _
"trying to grab
the knife.")
49 Console.WriteLine(Me.OwnsUtensil
& "has the knife.")
50 Monitor.Enter(Me)
'SyncLock (aKnife)'
51 If
mKnifeAvailable Then
52 mKnifeAvailable =
False
53 a.HasKnife = True
54 mOwner = a.MyName
55 Console.WriteLine(a.MyName&"just
got the knife.waiting")
56 Try
Thread.Sleep(100)
Catch e As Exception
Console.WriteLine (e.StackTrace)
End Try
57 End If
58 Monitor.Exit(Me)
59 End Sub
60 End Class
61 Public Class
Programmer
62 Private mName As
String
63 Private Shared
mFork As Fork
64 Private Shared
mKnife As Knife
65 Private mHasKnife
As Boolean
66 Private mHasFork
As Boolean
67 Shared Sub New()
68 mFork = New Fork()
69 mKnife = New
Knife()
70 End Sub
71 Public Sub New(ByVal
theName As String)
72 mName = theName
73 End Sub
74 Public Readonly
Property MyName() As String
75 Get
76 Return mName
77 End Get
78 End Property
79 Public Property
HasKnife() As Boolean
80 Get
81 Return mHasKnife
82 End Get
83 Set(ByVal Value
As Boolean)
84 mHasKnife = Value
85 End Set
86 End Property
87 Public Property
HasFork() As Boolean
88 Get
89 Return mHasFork
90 End Get
91 Set(ByVal Value
As Boolean)
92 mHasFork = Value
93 End Set
94 End Property
95 Public Sub Eat()
96 Do Until Me.HasKnife
And Me.HasFork
97 Console.Writeline(Thread.CurrentThread.Name&"is
in the thread.")
98 If Rnd() < 0.5
Then
99 mFork.GrabFork(Me)
100 Else
101 mKnife.GrabKnife(Me)
102 End If
103 Loop
104 MsgBox(Me.MyName
& "can eat!")
105 mKnife = New
Knife()
106 mFork= New Fork()
107 End Sub
108 End Class
Основная
процедура Main (строки 4-16) создает два
экземпляра класса Programmer и затем
запускает два потока для
выполнения критического метода Eat
класса Programmer (строки 95-108),
описанного ниже. Процедура Main
задает имена потоков и занускает их;
вероятно, все происходящее понятно
и без комментариев.
Интереснее
выглядит код класса Fork (строки 17-38) (аналогичный
класс Knife определяется в строках 39-60).
В строках 18 и 19 задаются значения
общих полей, по которым можно
узнать, доступна ли в данный момент
вилка, и если нет — кто ею
пользуется. ReadOnly-свойство OwnUtensi1 (строки
20-24) предназначено для простейшей
передачи информации. Центральное
место в классе Fork занимает метод «захвата
вилки» GrabFork, определяемый в строках
25-27.
- Строки
26 и 27 просто выводят на консоль
отладочную информацию. В
основном коде метода (строки 28-36)
доступ к вилке
синхронизируется по объектной
переменной Me. Поскольку в нашей
программе используется только
одна вилка, синхронизация по Me
гарантирует, что два потока не
смогут одновременно захватить
ее. Команда Slee'p (в блоке,
начинающемся в строке 34)
имитирует задержку между
захватом вилки/ножа и началом
еды. Учтите, что команда Sleep не
снимает блокировку с объектов
и лишь ускоряет возникновение
взаимной блокировки!
Однако наибольший интерес представляет код класса Programmer (строки 61-108). В строках 67-70 определяется общий конструктор, что гарантирует наличие в программе только одной вилки и ножа. Код свойств (строки 74-94) прост и не требует комментариев. Самое главное происходит в методе Eat, выполняемом двумя отдельными потоками. Процесс продолжается в цикле до тех пор, пока какой-либо поток не захватит вилку вместе с ножом. В строках 98-102 объект случайным образом захватывает вилку/нож, используя вызов Rnd, — именно это и порождает взаимную блокировку. Происходит следующее:
Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он захватывает нож и переходит в состояние ожидания.
- Поток,
выполняющий метод Eat объекта Bob,
активизируется и входит в цикл.
Он не может захватить нож, но
захватывает вилку и переходит
в состояние ожидания.
- Поток,
выполняющий метод Eat объекта
Тот, активизируется и входит в
цикл. Он пытается захватить
вилку, однако вилка уже
захвачена объектом Bob; поток
переходит в состояние ожидания.
- Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он пытается захватить нож, однако нож уже захвачен объектом Тот; поток переходит в состояние ожидания.
Все
это продолжается до бесконечности
— перед нами типичная ситуация
взаимной блокировки (попробуйте
запустить программу, и вы убедитесь
в том, что поесть так никому и не
удается).
О возникновении взаимной
блокировки можно узнать и в окне
потоков. Запустите программу и
прервите ее клавишами Ctrl+Break.
Включите в окно просмотра
переменную Me и откройте окно
потоков. Результат выглядит
примерно так, как показано на рис. 10.7.
Из рисунка видно, что поток Bob
захватил нож, но вилки у него нет.
Щелкните правой кнопкой мыши в окне
потоков на строке Тот и выберите в
контекстном меню команду Switch to Thread.
Окно просмотра показывает, что у
потока Тот имеется вилка, но нет
ножа. Конечно, это не является
стопроцентным доказательством, но
подобное поведение по крайней мере
заставляет заподозрить неладное.
Если вариант с
синхронизацией по одному объекту (как
в программе с повышением -температуры
в доме) невозможен, для
предотвращения взаимных
блокировок можно пронумеровать
объекты синхронизации и всегда
захватывать их в постоянном
порядке. Продолжим аналогию с
обедающими программистами: если
поток всегда сначала берет нож, а
потом вилку, проблем с взаимной
блокировкой не будет. Первый поток,
захвативший нож, сможет нормально
поесть. В переводе на язык
программных потоков это означает,
что захват объекта 2 возможен лишь
при условии предварительного
захвата объекта 1.
Рис. 10.7.
Анализ взаимной блокировки в окне
потоков
Следовательно,
если убрать вызов Rnd в строке 98 и
заменить его фрагментом
mFork.GrabFork(Me)
mKnife.GrabKnife(Me)
взаимная
блокировка исчезает!