Иллюстрированный самоучитель по VB.NET

       

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


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

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

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

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


    Содержание раздела