- •Лабораторная работа № 1 Основы системы программирования Ruby
- •Порядок выполнения работы
- •Содержание отчета
- •Варианты заданий
- •5 . Случайный спуск по дереву
- •6. Пересечение множеств
- •7. Удаление повторяющихся чисел
- •8. Проверка транзитивности
- •Лабораторная работа № 2 Разделение ресурсов
- •Порядок выполнения работы
- •Содержание отчета
- •Методические указания
- •Варианты задач Вариант 1
- •Вариант 2
- •Вариант 3
- •Вариант 4
- •Лабораторная работа № 3 Каркасы программных систем
- •Порядок выполнения работы
- •Содержание отчета
- •Пример программы на fxruby
7. Удаление повторяющихся чисел
Имеется неупорядоченный набор чисел. Предполагается, что все значения чисел в наборе различны. Требуется проверить справедливость данного утверждения и, если оно не справедливо, удалить из набора повторяющиеся числа.
8. Проверка транзитивности
Задан набор пар натуральных чисел, причем пары (a, b) и (b, a) считаются различными. Проверить, выполняется ли для вхождения чисел в данный набор свойство транзитивности: если набор содержит пары (a, b) и (b, c), то он обязательно содержит и пару (a, c).
Лабораторная работа № 2 Разделение ресурсов
Цель работы – приобретение навыков организации доступа нескольких параллельных процессов к общим ресурсам.
Порядок выполнения работы
Ознакомиться с методическими указаниями.
Подготовить параллельные алгоритмы решения задачи приведенной в конце описания (вариант определяется номером бригады). Согласовать формализацию с преподавателем (содержание формализации см. ниже). При решении использовать:
семафор (в качестве семафоров использовать http://www.imasy.or.jp/~fukumoto/ruby/semaphore.rb);
монитор.
Реализовать алгоритмы на языке Ruby.
Содержание отчета
1. Постановка задачи
2. Формализация решения
2.1. Типы потоков и их количество
2.2. Условия ожидания потоков каждого типа
2.3. Назначение и число семафоров
2.4. Методы монитора (название, назначение, параметры).
2.5. Указать классическую задачу, на которую похожа данная.
2.6. Случайные задержки, используемые программе.
3. Сопоставление эффективности мониторов и семафоров.
4. Выводы
5. Приложения
5.1. Результаты тестирования
5.2. Листинги программ
Методические указания
Процессы называются параллельными, если они существуют одновременно. Среди процессов принято выделять потоки – это процессы, имеющие общую область памяти (разделяемые переменные). Создание нескольких потоков управления реализуется либо с помощью обращения к операционной системе, либо с помощью средств системы программирования.
Не всегда следует разрабатывать параллельные программы. Основными причинами, по которым следует разрабатывать параллельные алгоритмы, являются:
неудовлетворительная скорость работы последовательного алгоритма, при наличии многопроцессорной системы;
исходная задача лучше формализуется в виде набора параллельных процессов.
Большинство процессов являются асинхронными, т.е. им необходимо периодически синхронизироваться и взаимодействовать друг с другом. Как правило, нельзя делать предположений об относительной скорости асинхронных процессов. Поэтому необходимы специальные способы синхронизации процессов. Наиболее распространенным примитивом низкого уровня для синхронизации является семафор.
Семафор представляет собой защищенную переменную, т.е. из программы пользователя над семафором нельзя совершать обычные арифметические операции или присваивать ему какое-либо значение. Допустима команда инициализации семафора целым неотрицательным числом. Доступ к нему разрешен только через операции P(S), V(S):
P(S) = IF S > 0 THEN S := S - 1
ELSE ждать операции V(s);
V(S) = IF есть ждущие процессы на S THEN разрешить одному из процессов продолжить выполнение
ELSE S := S + 1;
Выполнить любую из данных операций над конкретным семафором в один момент времени может только один процесс, т.е. выполняется правило взаимного исключения. Обычно процессы, ждущие на семафоре, организованы согласно дисциплине FIFO [1].
Когда процесс производит обращение к разделяемым данным, то говорят, что он находится на своем критическом участке. Процессы, находящиеся вне своих критических участков, не должны препятствовать другим процессам входить в их собственные критические участки.
При разработке параллельных алгоритмов следует:
обеспечить взаимное исключение при доступе к разделяемым ресурсам;
не допускать взаимной блокировки процессов, т.е. ситуации, когда ни один из процессов не может продолжить свою работу;
избегать режима активного ожидания;
избегать ситуаций бесконечного откладывания момента входа процессов в их критические участки;
помещать в область взаимного исключения минимально необходимое число операторов.
Системное программное обеспечение, должно обеспечить разделение вычислительной системы между многими процессами, выполняющими непредсказуемые запросы на выделение ресурсов. Некорректные алгоритмы разделения ресурсов могут приводить к тупикам, неопределенному ожиданию, сбоям в работе. Поэтому, особое внимание разработчика должно быть направлено на алгоритмы выделения ресурсов различных видов (оперативная память, жесткие диски, магнитные ленты, терминалы и т.д.). Чтобы упростить свою задачу, разработчик должен конструировать отдельные планировщики для каждого класса ресурса. Каждый планировщик будет состоять из некоторого количества локальной управленческой информации, вместе с некоторыми процедурами и функциями, которые вызываются программами, желающими получить и освободить ресурсы. Такое собрание связанных данных и процедур известно как монитор. В параллельном программировании дается следующее определение:
Монитор – это набор разделяемых переменных и повторно входимых процедур доступа к ним, которым процессы пользуются в режиме разделения, причем в каждый момент им может пользоваться только один процесс.
Только один процесс в каждый момент времени может войти в процедуру монитора, а любой следующий запрос должен быть задержан до тех пор, пока предыдущий запрос не закончится. Процедуры, локальные в мониторе, не должны обращаться к любым нелокальным переменным. Переменные монитора должны быть недоступны извне монитора. Эти требования позволяют избавиться от значительной части ошибок в параллельном программировании уже на этапе компиляции.
Монитор, динамически выделяющий ресурс, иногда должен задерживать процесс, желающий получить ресурс, который в настоящее время недоступен, и возобновлять этот процесс после того, как другой процесс освободит данный ресурс. Для этого существуют операции, вызываемые в процедуре монитора:
операция ожидания - блокирует вызывающий процесс;
операция сигнализации - заставляет один из ожидающих процессов возобновиться.
Если нет ожидающих процессов, операция сигнализации не имеет никакого эффекта. Чтобы давать возможность другим программам освободить ресурсы в течение ожидания, операция ожидания должна оставить возможность прерывания, иначе это предотвратило бы вход в процедуру освобождения.
За операцией сигнализации должно следовать завершение текущей процедуры и возобновление ожидающего процесса, без возможности вмешательства вызова процедуры от третьего процесса. Это - единственный способ гарантировать для ожидающего процесса, что он сможет получить ресурс, только что освобожденный сигнализирующим процессом без риска, что другой процесс вклинится в монитор и захватит ресурс вместо него.
Часто, может быть более одной причины для ожидания. Поэтому в мониторе существует особый тип переменной, называемый "условием" и тот, кто пишет монитор, должен объявить переменную типа условия для каждой причины, по которой процессу, возможно, придется ждать. Тогда операции ожидания и сигнализации должны в качестве аргумента получать переменную условия.
Переменная - условие не имеет никакого сохраняемого значения, доступного для процесса. Практически, переменная условия будет представлена очередью процессов, которые в настоящее время ожидают на условии.
Примеры решения классических задач (постановка задач см. лекции)
Производители и потребители
require 'semaphore'
t0 = Time.now
buf = Array.new(5)
tp = []
tc = []
c = 20
s1 = Semaphore.new(buf.size)
s2 = Semaphore.new(0)
m = Semaphore.new(1)
producer = Thread.new do
i = 0
c.times do
d = rand(10)
tp << d
puts "produce #{d} i=#{i}"
s1.P
m.P
buf[i] = d
m.V
s2.V
i = ( i + 1 ) % buf.size
end
end
consumer = Thread.new do
i = 0
c.times do
s2.P
m.P
d = buf[i]
m.V
s1.V
tc << d
puts "consume #{d} i=#{i}"
i = ( i + 1 ) % buf.size
end
end
producer.join
consumer.join
puts tp.join(',')
puts tc.join(',')
puts tp == tc
puts Time.now - t0
Писатели и читатели
require 'monitor'
class RWControl
include MonitorMixin
def initialize
@can_read = new_cond
@can_write = new_cond
@num_readers = 0
@is_writing = false
super()
end
def begin_reading
synchronize do
@can_read.wait if @is_writing or @can_write.count_waiters > 0
@num_readers += 1
@can_read.signal
end
end
def end_reading
synchronize do
@num_readers -= 1
@can_write.signal if @num_readers == 0
end
end
def begin_writing
synchronize do
@can_write.wait if @is_writing or @num_readers > 0
@is_writing = true
end
end
def end_writing
synchronize do
@is_writing = false
if @can_read.count_waiters > 0
@can_read.signal
else
@can_write.signal
end
end
end
end
m = RWControl.new
50.times do
Thread.new do
sleep rand * 2
m.begin_reading
puts Thread.current.to_s + ' reading'
sleep rand
m.end_reading
end
end
10.times do
Thread.new do
sleep rand * 2
m.begin_writing
puts Thread.current.to_s + ' writing'
sleep rand * 2
m.end_writing
end
end
Thread.list.each { |t| t.join unless t == Thread.main }
Философы
require 'semaphore'
N = 5
class Table
def initialize( n )
@forks = Array.new( n ) { Semaphore.new(1) }
@m = Semaphore.new( n - 1 )
end
def take_left(i)
@m.P
@forks[ i - 1 ].P
end
def take_right(i)
@forks[ i ].P
end
def put_left(i)
@m.V
@forks[ i - 1 ].V
end
def put_right(i)
@forks[ i ].V
end
end
table = Table.new(N)
N.times do |i|
Thread.new(i) do |k|
loop do
puts "thinking #{k}"; sleep rand
table.take_left(k); sleep 2*rand
table.take_right(k)
puts "#{k} eating"; sleep rand
table.put_left(k)
table.put_right(k)
end
end
end
Thread.list.each{ |t| t.join if t != Thread.current }
require 'monitor'
class Table
include MonitorMixin
def initialize( n )
super()
@c = Array.new( n ) { new_cond }
@forks = Array.new( n ) { 2 }
end
def take_both(i)
synchronize do
@c[i].wait if @forks[i] < 2
@forks[i-1] -= 1
@forks[(i+1)%@c.size] -= 1
end
end
def put_both(i)
synchronize do
@forks[i-1] += 1
@forks[(i+1)%@c.size] += 1
@c[i-1].signal if @forks[i-1] == 2
@c[(i+1)%@c.size].signal if @forks[(i+1)%@c.size] == 2
end
end
end
table = Table.new(N)
N.times do |i|
Thread.new(i) do |k|
loop do
puts "thinking #{k}"; sleep rand
table.take_both(k);
puts "#{k} eating"; sleep rand
table.put_both(k)
end
end
end
Thread.list.each{ |t| t.join if t != Thread.current }