Pull to refresh

Удивительная страна Oz, или как принять данные при помощи send

Reading time 15 min
Views 1.8K
Довольно давно, собирая информацию по средствам параллельного программирования, наткнулся я на элегантный (другими словами сложно описать ощущения) язык Oz http://www.mozart-oz.org. Язык тогда показался мне достойным того, чтобы представить его Habraсообществу. И вот, у меня появилось время и причины это сделать.

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

send(socket; buffer; flag) = (if (flag == RECV) (recv(socket; buffer)) or (realsend(socket; buffer))).

Речь идёт именно о том, что отправка и получение данных осуществляются одной и той же последовательностью операций виртуальной машины Oz. Естественно, достигается это за счёт особых абстракций для работы с данными и с параллельными процессами. Описанию этих абстракций и посвящён этот текст, потому как на мой взгляд — они неплохо позволяют почувствовать особенности Oz. Конечно, Oz больше, чем изложенное ниже, но, как мне кажется, тайна хитрого send — материал подходящий для первого знакомства с этим языком и для получения от него удовольствия.



0. Введение


Начать можно с текста программы, реализующей конструкцию Port, через которую передаются данные при помощи процедуры send. В эту же программ включён пример использования Port. Исходный код (имя собственное в этом тексте):

 
	declare Port in 
	 
	local PortTag NewPort IsPort Send in 
		PortTag = {NewName} 
	 
		fun {NewPort FS} 
			local S C in 
				C = {NewCell S} 
				FS = !!S 
				{NewChunk port(PortTag: C)} 
			end 
		end 
	 
		fun {IsPort ?P} 
			{Chunk.hasFeature P PortTag} 
		end 
	 
		proc {Send P M} 
			local Ms Mr in 
				{Exchange P.PortTag Ms Mr} 
				Ms = M | !!Mr 
			end 
		end 
	 
		Port = port 
		( 
			new: NewPort 
			is: IsPort 
			send: Send 
		) 
	end 
	 
	declare NewQueueServer in 
	 
	fun {NewQueueServer} 
		local Given GivePort Taken TakePort Join in 
			GivePort = {Port.new Given} 
			TakePort = {Port.new Taken} 
	 
			proc {Join Xs Ys} 
				local Xr Yr X Y in 
					Xs = X | Xr 
					Ys = Y | Yr 
					X = Y 
					{Join Xr Yr} 
				end 
			end 
	 
			thread {Join Given Taken} end 
		 
			queue 
			( 
				put: proc {$ X} {Port.send GivePort X} end 
				get: proc {$ X} {Port.send TakePort X} end 
			) 
		end 
	end 
	 
	declare Q in 
	 
	thread Q = {NewQueueServer} end 
	 
	{Q.put 1} 
	{Browse {Q.get $}} 
	{Browse {Q.get $}} 
	{Q.put 2} 


Как видно, никакого подвоха нет. Обе функции queueserver'а — put и get — реализованы при помощи процедуры Send, семантика которой соответствует её названию. Этот код мог бы быть и короче — в Oz достаточно синтаксического сахара, который не используется выше с целью сократить объём необходимых пояснений.

Для понимания того, как этот код работает, нужно разобраться с (1) моделью исполнения программ на Oz: переменными, нитями, оператором выравнивания (=); (2) процедурами; (3) ячейками (cells), областями памяти (chunks) и будущими (futures).

1. Переменные, нити, оператор =


Как было сказано, Oz — это мультипарадигменый язык. В Википедии он описан как функциональный, логический, императивный, объектно-ориентированный, распределённый, параллельный (в смысле concurrent) и как язык для программирования с ограничениями. Но в основе его лежит другая парадигма — программирование с потоками данных (dataflow programming). Oz реализует эту парадигму достаточно близко к исходной (применяющейся в архитектурах CPU) концепции вычислений с потоками данных, и делает это следующим образом.

1.1. Все вычисления в Oz происходят с данными, которые содержатся в хранилище (store). Хранилище при этом является штукой довольно абстрактной (внутренняя механика скрыта от программиста, и информацией в store можно управлять только через определённый интерфейс) и распределённой (со store могут работать все участники Oz-вычисления, в том числе и разбросанные по сети). Store может хранить информацию в трёх разных видах. (1) В виде свободных или связанных логических переменных. (2) В виде ячеек (cells), которые представляют из себя скорее не ячейки памяти, а именованные изменяемые указатели на переменные. (3) В виде процедур, являющихся в Oz именноваными замыканиями (естественно, они являются first-order объектами, то есть, ими можно явно манипулировать в программе).

1.2. Вычисление происходит при выполнении нити (thread). При этом нить в Oz — это и есть dataflow. То есть, некая последовательность операторов (statements), исполняемых друг за дружкой (всё как в императивных языках), но с одной оговоркой: оператор исполняется только в том случае, когда все используемые в нём переменные оказываются связанными. В противном случае нить приостанавливается и ожидает момента связывания нужных переменных. Собственно, выполнение некого оператора по факту готовности данных, используемых в нём и есть основной метод dateflow вычислений.

Создаются нити в Oz очень просто, при помощи конструкции thread ... end. Создание это происходит в fork()-стиле. То есть, порождённая нить имеет доступ ко всем переменным (именно переменным), которые были доступны родительской нити на момент выполнения операции thread ... end.

1.3. Переменные в Oz являются логическими.

Логическая переменная в стиле Oz — это единожды связываемая переменная, которая может быть уравнена (equated) с другой переменной.

Вообще, это определение (перевод из руководства) мало что говорит о переменных в Oz. Они позволяют делать с собой чуть больше. И они существенно отличаются от привычных по c/javascript/haskell переменных, представляющих из себя либо именованные ячейки памяти (c/js), либо именованные значения (haskell). Вероятно, лучше всего представление о переменных в Oz даст описание некоторых технических деталей.

1.3.1. Жизненный путь переменной в Oz начинается с того, что она объявляется (introduce) при помощи конструкций local ... in ... end или declare ... in .... Отличия этих конструкций в том, что local ... позволяет объявить переменные только для использования в операциях, стоящих внутри in ... end, а объявленные при помощи declare переменные будут доступны всюду после соответствующего in. Естественно, declare можно использовать для определения переменных только в глобальной области видимости модуля.

Объявление переменной заключается в том, что в store создаётся новый узел (node), на который ссылается объявленная переменная. И в узел записывается значение unknown, указывающее на то, что переменная никак не связана с данными. Если к такой переменной за данными обратится нить, то она будет приостановлена до того момента, как соответствующий node будет связан с данными. Например (код-1):

 
	local X in 
		thread {Browse 2 * X} end 
		X = 5 
	end 


Browse — это одна из стандартных процедур Oz, распечатывающая свой аргумент. При объявлении X в этом примере будет создана переменная и соответсвующий ей новый узел в store. Когда Browse обратится через X к этому узлу, выполнение соответствующей нити будет приостановлено до тех пор, пока в соответсвующем X node не появится значение (тут может измениться как само значение в node, так и привязка переменной к узлу).

1.3.2. Значения в узлы помещаются в процессе унификации (unification) или (другое название) инкрементального пересчёта (incremental tell). Унификацию для пары выражений выполняет оператор = (equality operator). В этом же процессе может измениться и привязка переменных к узлам. Алгоритм унификации для пары выражений (именно для выражений) в коде E1 = E2 работает так (рекурсивная процедура с разбором вариантов того, чем могут быть E1 и E2).

U.1. Результатами вычислений E1 и E2 являются значения атомарных типов Oz (атомарные — это неделимые типа: целые числа, числа с плавающей точкой, литералы — строки, например). В этом случае при равенстве этих значений оператор выполняется, а при неравенстве генерируется исключение (исключения в Oz достаточно стандартны и их обработка задаётся при помощи конструкции try ... catch ... finally ... end; finally — не обязательно).

U.2. Результатом вычисления E1 является некоторая переменная, а результатом вычисления E2 — значение некоторого типа (симметричная к этой ситуации та, в которой вычисление E1 приводит к значению некоторого типа, а E2 — к переменной). Выгядеть это может так:

 
	X = (Y + Z) * 5 


В этом случае работает всё следующим образом.

U.2.1. Задаваемая E1 переменная ссылается на unknown-узел. Если так, то в этот узел записывается значение, полученное вычислением E2, в результате чего E1-переменная оказывается связанной.

U.2.2. Если E1-переменная уже является связанной с атомарным значением, то значение, которое находится в соответствующем узле сравнивается с E2-значением, и при несовпадении типов или самих этих значений генерируется исключение. В противном случае — оператор завершается.

Именно по этому правилу связывается переменная X из кода-1. После того, как она связана со значением (5), выполняется, ожидающий этого связывания, оператор внутри вызыванной процедуры Browse. И тут должно быть понятно, что результат был бы точно таким же, если бы код-1 выглядел так:

 
	local X in 
		thread {Browse 2 * X} end 
		5 = X 
	end 


В U.2. возможен ещё один вариант, когда E1-переменная связана со значением составного типа, но он будет рассмотрен позже, в пункте U.4…

U.3. Как E1 так и E2 задают некоторую переменную. Тут тоже возможны варианты.

U.3.1. Одна из переменных является несвязанной, или обе переменные оказываются несвязанными (то есть, они ссылаются на узлы со значением unknown). В этом случае узел одной из несвязанных переменных или unknown-узел той единственной, что не связана отбрасывается. Затем, ранее ссылавшаяся на этот узел переменная изменяется так, что начинает ссылаться на тот же узел, что и другая переменная.

Здесь должно уже быть понятно, почему, выполняя код-2:

 
	local X Y Z in 
		thread {Browse X + Y + Z} end 
		X = Y 
	%	Z = X + Y 
		X = 10 
		X + Y = Z 
	end	 


Oz выдаст значение 40. И почему Oz 'подвиснет', если убрать комментарий (начинается с %).

U.3.2. Обе переменных ссылаются на узлы, в которых записаны значения. В этом случае поведение следующее. Если это значения разных типов, или если это различные атомарные значения одного типа, то будет сгенерировано исключение. Если это равные значения некоторого атомарного типа, то оператор завершит своё выполнение. Но узлах могут хранится и значения составных типов.

Основным составным типом в Oz является запись. Синтаксически записи выглядят примерно так:

 
	label (feature0: field0 feature2: field2 ... featureN: fieldN) 


Примерно, потому что выше приведена закрытая запись, с фиксированным числом полей (field), а записи бывают ещё и открытые. К полям записи можно обращаться через точку, по названию соответствующего свойства (feature). Количество полей в структуре называется её арностью (arity). Более конкретный пример:

 
	U = habrauser(nick: 'mikhanoid' karma: 10 strength: 10) 
	K = U.karma 


Тонкость: поля в значении типа запись сами являются переменными Oz. В примере выше при создании значения с типом запись и меткой habrauser создаются три переменные, которые были унифицированы согласно описываемому алгоритму со значениями атомарных типов: 'mikhanoid', 10 и 10. Но вместо таких значений унификацию полей-переменных можно проводить с любыми выражениями. К этому моменту должно быть понятно, как работает такой код:

 
	local U K S in 
(*)		U = habrauser(nick: 'mikhanoid' karma: K strength: S) 
		K = S 
		10 = K 
	end 


В строке (*) несвязанная переменная U унифицируется со значением типа запись по правилу U.2.1.

У записей Oz важная роль — они превращают store в орграфовую структуру: если в узле хранится значение типа запись, то своими полями-переменными она указывает на другие узлы, то есть, поля — это нечто вроде дуг, помеченных названиями свойств. А описываемый алгоритм, благодаря пункту U.4., можно рассматривать как алгоритм слияния графов (graph merge).

U.4. Здесь можно объединить такие случаи: (1) оба выражения E1 и E2 являются значениями типа записи, (2) оба выражения задают переменные, ссылающиеся на узлы, в которых хранятся записи, (3) одно из E1, E2 задаёт значение типа запись, а другое — задаёт переменную, связанную с узлом, в котором запись хранится. Во всех этих случаях если записи имеют (a) разные метки, или (b) разную арность, или © разные наборы свойств, то генерируется исключение. Если же записи совпадают по пунктам (a), (b) и ©, то происходит унификация пар переменных из разных записей с одинаковыми именами свойств.

На этот момент уже должно быть понятно, почему в результате работы такого кода:

 
	local W X Y Z in 
		W = X 
		Z = Y 
		f(a: 10 b: X) = f(a: Y b: 20) 
		{Browse W + Z} 
	end 


будет выведено значение 30.

Ещё одним интересным примером является такой код:

 
	local Z in 
		Z = f(Z 20) 
		{Browse Z} 
	end 


Здесь f — это вариант записи, в которой свойства полей автоматически получают целочисленные имена от 1 до её арности. В Oz такие записи называются кортежами и являются аналогами составных выражений (compound term) в логическом программировании. В коде выше происходит следующее. (1) создаётся значение типа запись с двумя полями, то есть создаётся две переменных. (2) поле со свойством 1 унифицируется с несвязанной переменной Z, второе поле с атомарным значением 20. В итоге, значение f указывает на unknown-узел, на который указывает и переменная Z и на узел, хранящий значение 20. (3) в unknown-узел, на который указывает Z записывается значение типа запись, второй элемент которой (как и переменная Z) указывает на этот узел. Ничего особенного, просто получился цикл в графе store. Browse распечатает его примерно так: R10 = f(R10 20).

Списки в Oz являются разновидностью кортежей. Организованы они стандартно для функционального и логического программирования: список — это кортеж, первый элемент — голова — которого является переменной, а второй — списком, составляющим хвост. Обозначать голову у хвост списка можно при помощи символа |: Head | Tail.

2. Процедуры


Oz позволяет абстрагировать последовательности операторов при помощи процедур. Процедуры в Oz являются значениями (first-class objects), поэтому их можно свободно 'присваивать' переменным. Более детально. Oz хранит замыкания, реализующие процедуры, в store в виде некоторого кода. Каждое такое замыкание-процедура получает уникальное для всего store имя. Имена в Oz являются литералами — значениями атомарного типа. После создания кода процедуры и присвоения ему уникального имени имя записывается в узел, на который ссылается некоторая переменная. После чего процедуру можно вызывать через эту переменную.

Именно поэтому в исходном коде NewPort, IsPort, Send,… объявляются как переменные при помощи local или declare.

Базовой конструкцией, определяющей процедуру является proc:

 
	local ... P ... in 
		... 
		proc {P X1 ... XN} S1 ... SM end 
		... 
	end 


X1, ..., XN — формальные аргументы. S1, ..., SM — последовательность операторов, реализующих процедуру. Вызываются процедуры при помощи фигурных скобок:

 
	{P A1 ... AN} 


A1, ..., AN — действительные параметры: выражения, переменные и так далее. Вызов происходит с объявлением новых соответствующих формальным параметрам переменных, которые унифицируются с аргументами, среди которых могут быть и несвязанные переменные. Следовательно, процедура может как получать, так и возвращать данные через любой параметр. Поэтому, для повышения читаемости кода, переменные, которые программист предполагает использовать только для возврата значений можно помечать приставкой ?, которая является просто комментарием.

Переменная, связанная с узлом, хранящим имя процедуры, так же может быть результатом вычисления выражения, как здесь, например:

 
	{Q.put 1} 


В Oz существует возможность работать с анонимными процедурами, но реализована она необычным способом — через механизм вложения (nesting). В последовательности операторов, заключённых в {...} одну из позиций может занимать символ $ — метка вложения. Когда Oz встречает такие метки внутри фигурных скобок, выполняя некоторый оператор, то он (1) автоматически создаёт новые переменные, по количеству {... $ ...}, в новой локальной области видимости, затем, в этой области видимости (2) делает вызовы {... $ ...}, вместо маркера подставляя новую переменную, созданную для этого вызова, и (3) вставляет эти переменные на места {... $ ...} в исполняемом операторе. Например

 
	local P in 
		proc {P X ?Y} Y = X + 10 end 
		{Browse {P 20 $} + {P 30 $} + 40} 
	end 


будет выполнено как

 
	local P in 
		proc {P X Y} Y = X + 10 end 
		local X1 X2 in 
			{P 20 X1} 
			{P 30 X2} 
			{Browse X1 + X2 + 40} 
		end 
	end 


Метку вложенности можно использовать и на первом месте внутри {...}, но только во время определения процедуры. Но механизм вложения будет работать точно так же. Коды:

 
	local P in 
		local P1 in 
			proc {P1 X1 ... XN} S1 ... SM end 
			P = P1 
		end 
	end 


и

 
	local P in 
		P = proc {$ X1 ... XN} S1 ... SM end 
	end	 


эквивалентны.

Функции Oz так же являются процедурами, и являются некоторым упрощением синтаксиса для тех случаев, когда известно, что процедура точно должна возвращать некоторое значение. Тогда можно сэкономить на описании одного формального параметра, а именно:fun {F X1 ... XN} S1 ... SM end то же самое, что и proc {F X1 ... XN Y} S1 ... Y = SM end. Вызов же {F A1 ... AN}, как функции, Oz автоматически превращает в вызов процедуры {F A1 ... AN $}

Например.

 
	fun {F X} 
		local K in 
			K = X / 20 
			{Browse K} 
		end 
		local L in 
			L = 30 
			X == L 
		end 
	end 


соответствует определению

 
	proc {F X Y} 
		local K in 
			K = X / 20 
			{Browse K} 
		end 
		 
		Y = local L in 
			L = 30 
			X == L 
		end 
	end 


При этом значение блока local ... in ... end — это значение последнего в нём оператора. То есть, в этом примере F будет результатом своего вызова иметь ответ на вопрос: равен ли первый и единственный аргумент функции 30.

3. Ячейки (cell), области памяти (chunk) и будущие (future)


Для работы с данными в store Oz поддерживает ещё несколько абстракций.

3.1. Области памяти — это некоторые записи в store. Но в отличии от обычных записей они идентифицируются при помощи уникальных имён (точно так же, как и процедуры) и не позволяют определять у себя арность. То есть, к их компонентам можно обращаться только по именам свойств (feature). Если эти имена будут скрыты от каких-то участков кода, то на этих участках обращение к элементам соответствующей области памяти будет невозможным. Области создаются при помощи вызова функции {NewChunk Record}. Вызов создаёт область памяти с уникальным именем, и возвращает это имя. Имя можно записать в узел, на который ссылается некоторая переменная. После чего через эту переменную и оператор . можно обращаться к полям области памяти:

 
	local X R in 
		R = f(a: 1 b: 2 c: 3) 
		X = {NewChunk R} 
		{Browse X.c} 
	end 


3.2. Ячейки в Oz предназначены для работы с состояниями. Ячейка, как процедуры и области памяти, в store определяется неким уникальным именем. Как и переменная, она является указателем на некоторый узел, но в отличии от переменной, узлы, на который указывает ячейка можно явно и многократно задавать. Определяет ячейку следующий интерфейс.

C = {NewCell E} — создаёт ячейку, указывающую на узел, который получиться в результате унификации значения выражения E и переменной — формального аргумента процедуры NewCell. Новое уникальное имя вновь созданной ячейки возвращается и присваивается переменной C. Ссылка на узел из ячейки, естественно, учитывается при сборке мусора. {IsCell C} отвечает на вопрос: связана ли переменная C с именем ячейки.

@C — это выражение результатом своим имеет переменную, указывающую на узел, на которой ссылается ячейка с именем, хранящимся в переменной C. C := E меняет указатель в ячейке, чьё имя хранится в C, так, чтобы он указывал на узел, полученный в результате унификаций некой несвязанной безымянной переменной и результата выражения E.

{Exchange C E1 E2} — за одну неразрывную, атомарную операцию унифицирует @C с E1 и, затем, выполняет C := E2.

Ячейку, например, можно использовать в качестве счётчика:

 
	local C in 
		C = {NewCell 0} 
		{Exchange C X thread X + 1 end} 
	end 


Здесь нельзя исключить thread ... end. А работает это по той причине, что для каждой запущенной нити Oz автоматически создаёт несвязанную переменную, которая унифицируется с последним из операторов тела нити. И эта переменная в данном примере является последним аргументом Exchange.

3.3. Будущие были впервые предложены в Smalltalk-72, а потом идея их применения была развита в MultiLISP (1985 год). Будущие достаточно распространённый инструмент: они есть в Java, доступные через java.util.cuncurrent.Future; их поддержка запланирована в C++0x. Поддерживаются они и в Oz, но в необычном виде.

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

В Oz похожим свойством обладают обычные переменные: часть действий с ними можно совершать не дожидаясь, пока в узлах, на которые они ссылаются, появятся значения. Многие из уже приведённых примеров это иллюстрируют. Однако, переменные в Oz — это двунаправленные каналы обмена информацией: стоящие слева и справа от = выражения симметричны для алгоритма унификации. Поэтому, в коде:

 
	local X in 
		thread X = 5 end 
		thread X = 7 end 
		X = 3 
		{Browse X} 
	end 


ни одна из нитей не выделяется, как производящая данные. Исключение в процессе унификации может вызвать любая нить. Будущее же позволяет установит в этой ситуации некоторый порядок. В Oz будущее — это доступная только для чтения ссылка на узел, то есть, несколько ограниченная версия переменной. Если будущее участвует в процессе унификации, то этот процесс приостанавливается до тех пор, пока соответствующая будущему переменная не оказывается связанной. Будущее для переменной X в Oz формируется оператором !!X. Порядок в пример выше можно внести, например, так:

 
	local X Y in 
		Y = !!X 
		thread Y = 5 end 
		thread X = 7 end 
		Y = 3 
		{Browse Y} 
	end 


здесь значение всегда будет формироваться только во второй нити.

4. Всё вместе


Удобно снова привести код из введения, чтобы не нужно было пролистывать этот текст на начало:

 
00	declare Port in 
01	 
02	local PortTag NewPort IsPort Send in 
03		PortTag = {NewName} 
04	 
05		fun {NewPort FS} 
06			local S C in 
07				C = {NewCell S} 
08				FS = !!S 
09				{NewChunk port(PortTag: C)} 
10			end 
11		end 
12	 
13		fun {IsPort ?P} 
14			{Chunk.hasFeature P PortTag} 
15		end 
16	 
17		proc {Send P M} 
18			local Ms Mr in 
19				{Exchange P.PortTag Ms Mr} 
20				Ms = M | !!Mr 
21			end 
22		end 
23	 
24		Port = port 
25		( 
26			new: NewPort 
27			is: IsPort 
28			send: Send 
29		) 
30	end 
31	 
32	declare NewQueueServer in 
33	 
34	fun {NewQueueServer} 
35		local Given GivePort Taken TakePort Join in 
36			GivePort = {Port.new Given} 
37			TakePort = {Port.new Taken} 
38	 
39			proc {Join Xs Ys} 
40				local Xr Yr X Y in 
41					Xs = X | Xr 
42					Ys = Y | Yr 
43					X = Y 
44					{Join Xr Yr} 
45				end 
46			end 
47	 
48			thread {Join Given Taken} end 
49		 
50			queue 
51			( 
52				put: proc {$ X} {Port.send GivePort X} end 
53				get: proc {$ X} {Port.send TakePort X} end 
54			) 
55		end 
56	end 
57	 
58	declare Q in 
59	 
60	thread Q = {NewQueueServer} end 
61	 
62	{Q.put 1} 
63	{Browse {Q.get $}} 
64	{Browse {Q.get $}} 
65	{Q.put 2} 


Нет смысла описывать всю программу строчка за строчкой (что редко даёт представление о работе даже непараллельных программ), а нужно пояснить несколько моментов.

4.1. Cоздание Port.

Port представляет собой запись, поля связаны с набором процедур. Это не более, чем удобная группировка нескольких процедур. Сам же порт представляет собой область памяти, содержащую ячейку под именем, чья уникальность гарантируется функцией NewName. Переменная PortName видна только в локальном блоке (02-30), поэтому доступ к составляющим область памяти можно получить только из процедур IsPort, NewPort, Send. Снаружи этого блока о переменной PortName ничего не известно, поэтому и доступ к полю области памяти, хранящей состояние порта, маловероятен (конечно, имя можно и угадать, но для инкапсуляции такого механизма вполне достаточно).

Внимание нужно обратить и на функцию NewPort (05-11). Кроме возврата самой области памяти порта аргумент она возвращает через свой первый аргумент ещё и будущее переменной, инициализирующей ячейку. Это используется для инициализации Given и Taken (36-37).

4.2. Процедура Send (17-22).

Нужно отметить, что ячейка области памяти порта всегда указывает на unknown-узел. И Send при очередном вызове первым делом гарантирует это, ссылку на предыдущий узел занося в переменную Ms, а на её место помещая ссылку на узел несвязанной переменной Mr. Далее, в Send формируется значение переменной Ms, которое получается в ходе унификации с кортежем-списком, у которого на первом месте стоит отправляемое сообщение (которое может быть и несвязанным), а на втором — будущее Mr (ещё раз: список это просто кортеж, на втором месте которого стоять может всё, что угодно). Последующий вызов Send проделает то же самое с переменной Mr, и этот процесс будет постепенно превращать переменную, которая использовалась для инициализации ячейки в NewPort, и будущее которой было возвращено наружу, в список. Иными словами, Send действительно посылает сообщения в некоторую очередь.

4.3. Процедура Join (39-46).

Самое главное здесь то, к чему применяется эта процедура, работающая в отдельной нити (48). Она применяется к Given и Taken, которые в результате вызовов Send постепенно превращаются в списки. Действительные параметры Join всегда являются будущими, поэтому операторы унификации (41-42) выполняются только после связывания Xs и Ys, которое происходит в Send, превращая эти переменные в ссылки на кортежи вида Message | SomeFuture . После чего Message-составляющие входящего (Given) и исходящего (Taken) потоков унифицируются, а Join вызывается для будущих хвостов списков (Немного похоже на квантовую механику, не так ли? Возможно, Эйнштейну не хватило знаний о параллельном программировании, чтобы построить модель Вселенной со скрытыми переменными).

4.4. В итоге.

Любая выставленная в поток Given при помощи Send связанная переменная, будет в нити (48) унифицирована с соответствующей несвязанной переменной, выставленной в поток Taken. Естественно, ситуация полностью симметричная, и имена put и get используются для удобства. Но queueserver всё-равно получается асинхронным, что полезно.

5. Другие интересные особенности Oz


Oz предлагает среди прочих своих возможностей (а множество их обширно), предлагает ещё и интересную модель параллельного программирования. Которая отличается от классической, принятой в Prolog, например, тем, что позволяет программисту определять собственные процедуры поиска и перебора вариантов. И, конечно, модель логического программирования, принятая в Oz, является параллельной.
Tags:
Hubs:
+36
Comments 14
Comments Comments 14

Articles