Взаимная блокировка

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

Рассмотрим ситуацию взаимной блокировки (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.

  1. Строки 26 и 27 просто выводят на консоль отладочную информацию. В основном коде метода (строки 28-36) доступ к вилке синхронизируется по объектной переменной Me. Поскольку в нашей программе используется только одна вилка, синхронизация по Me гарантирует, что два потока не смогут одновременно захватить ее. Команда Slee'p (в блоке, начинающемся в строке 34) имитирует задержку между захватом вилки/ножа и началом еды. Учтите, что команда Sleep не снимает блокировку с объектов и лишь ускоряет возникновение взаимной блокировки!
    Однако наибольший интерес представляет код класса Programmer (строки 61-108). В строках 67-70 определяется общий конструктор, что гарантирует наличие в программе только одной вилки и ножа. Код свойств (строки 74-94) прост и не требует комментариев. Самое главное происходит в методе Eat, выполняемом двумя отдельными потоками. Процесс продолжается в цикле до тех пор, пока какой-либо поток не захватит вилку вместе с ножом. В строках 98-102 объект случайным образом захватывает вилку/нож, используя вызов Rnd, — именно это и порождает взаимную блокировку. Происходит следующее:
    Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он захватывает нож и переходит в состояние ожидания.
  2. Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он не может захватить нож, но захватывает вилку и переходит в состояние ожидания.
  3. Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он пытается захватить вилку, однако вилка уже захвачена объектом Bob; поток переходит в состояние ожидания.
  4. Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он пытается захватить нож, однако нож уже захвачен объектом Тот; поток переходит в состояние ожидания.

Все это продолжается до бесконечности — перед нами типичная ситуация взаимной блокировки (попробуйте запустить программу, и вы убедитесь в том, что поесть так никому и не удается).
О возникновении взаимной блокировки можно узнать и в окне потоков. Запустите программу и прервите ее клавишами Ctrl+Break. Включите в окно просмотра переменную Me и откройте окно потоков. Результат выглядит примерно так, как показано на рис. 10.7. Из рисунка видно, что поток Bob захватил нож, но вилки у него нет. Щелкните правой кнопкой мыши в окне потоков на строке Тот и выберите в контекстном меню команду Switch to Thread. Окно просмотра показывает, что у потока Тот имеется вилка, но нет ножа. Конечно, это не является стопроцентным доказательством, но подобное поведение по крайней мере заставляет заподозрить неладное.
Если вариант с синхронизацией по одному объекту (как в программе с повышением -температуры в доме) невозможен, для предотвращения взаимных блокировок можно пронумеровать объекты синхронизации и всегда захватывать их в постоянном порядке. Продолжим аналогию с обедающими программистами: если поток всегда сначала берет нож, а потом вилку, проблем с взаимной блокировкой не будет. Первый поток, захвативший нож, сможет нормально поесть. В переводе на язык программных потоков это означает, что захват объекта 2 возможен лишь при условии предварительного захвата объекта 1.

Рис. 10.7. Анализ взаимной блокировки в окне потоков

Следовательно, если убрать вызов Rnd в строке 98 и заменить его фрагментом

mFork.GrabFork(Me)

mKnife.GrabKnife(Me)

взаимная блокировка исчезает!