end

def join

 list.each { |t| t.join if t != Thread.current }

end

def kill

 list.each { |t| t.kill }

end

end

13.2. Синхронизация потоков

Почему необходима синхронизация? Потому что из-за «чередования» операций доступ к переменным и другим сущностям может осуществляться в порядке, который не удается установить путем чтения исходного текста отдельных потоков. Два и более потоков, обращающихся к одной и той же переменной, могут взаимодействовать между собой непредвиденными способами, и отлаживать такую программу очень трудно.

Рассмотрим простой пример:

x = 0

t1 = Thread.new do

 1.upto(1000) do

  x = x + 1

 end

end

t2 = Thread.new do

 1.upto(1000) do

  x = x + 1

 end

end

t1.join

t2.join

puts x

Сначала переменная x равна 0. Каждый поток увеличивает ее значение на тысячу раз. Логика подсказывает, что в конце должно быть напечатано 2000.

Но фактический результат противоречит логике. На конкретной машине было напечатано значение 1044. В чем дело?

Мы предполагали, что инкремент целого числа — атомарная (неделимая) операция. Но это не так. Рассмотрим последовательность выполнения приведенной выше программы. Поместим поток t1 слева, а поток t2 справа. Каждый квант времени занимает одну строчку и предполагается, что к моменту, когда был сделан этот мгновенный снимок, переменная x имела значение 123.

t1                            t2

--------------------------    -----------------------------

Прочитать значение x (123)

                              Прочитать значение x (123)

Увеличить значение на 1 (124)

                              Увеличить значение на 1 (124)

Записать результат в x

                              Записать результат в x

Ясно, что каждый поток увеличивает на 1 то значение, которое видит. Но не менее ясно и то, что после увеличения на 1 обоими потоками x оказалось равно всего 124.

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

13.2.1. Синхронизация с помощью критических секций

Простейший способ синхронизации дают критические секции. Когда поток входит в критическую секцию программы, гарантируется, что никакой другой поток не войдет в нее, пока первый не выйдет.

Если акцессору Thread.critical присвоить значение true, то выполнение других потоков не будет планироваться. В следующем примере мы переработали код предыдущего, воспользовавшись акцессором critical для определения критической области, которая защищает уязвимые участки программы.

x = 0

t1 = Thread.new do

 1.upto(1000) do

  Thread.critical = true

  x = x + 1

  Thread.critical = false

 end

end

t2 = Thread.new do

 1.upto(1000) do

  Thread.critical = true

  x = x + 1

  Thread.critical = false

 end

end

t1.join

t2.join

puts x

Теперь последовательность выполнения изменилась; взгляните, в каком порядке работают потоки t1 и t2. (Конечно, вне того участка, где происходит увеличение переменной, потоки могут чередоваться более-менее случайным образом.)

t1                            t2

----------------------------- -----------------------------

Прочитать значение x (123)

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату