Параллельное и распределенное программирование на С++

Хьюз Камерон

Хьюз Трейси

Реализация агентно-ориентированных архитектур

 

 

Если бы последовательное (процедурное) программирование позволяло находить решения в любых ситуациях, то не было бы необходимости для развития технологий параллельного и распределенного программирования. Во многих случалх методы последовательного программирования просто не отвечают требованиям и опыту современных пользователей компьютеров. В процессе поиска разработчиками новых подходов к решению все возрастающих проблем и создаются альтернативные модели программного обеспечения. Программисты находят более эффективные способы организации ПО. Структурное программирование было шагом вперед по сравнению с процедурным (изобиловавшим безусловными переходами), объектно-ориентированное программирование сменило структурное. Во многих отношениях агенты и агентно-ориентированное программирование можно рассматривать как очередную (более высокую) ступень развития программирования. Агенты представляют иной (более сложный) метод организации и представления распределенных/параллельных программ.

 

Что такое агенты

 

Когда объектное программирование впервые залвило о себе, сама трактовка понятия объекта вызвала большие споры. Подобные разногласия вызывает и трактовка понятия агента. Одни определяют агенты как автономные постоянно выполняющиеся про-траммы, которые действуют от имени пользователя. Однако это определение можно применить и к UNIX-демонам или даже некоторым драйверам устройств. Другие дополняют это определение тем, что агент должен обладать специальными знаниями пользователя, должен выполняться в среде, «населенной» другими агентами, и обязан действовать только в рамках заданной среды. Эти требования должны исключать другие программы, которые можно было бы до некоторой степени считать агентами. Например, многие агенты электронной почты действуют автономно и могут работать по многих средах. Кроме того, в различных кругах программистов для описания агентов появились такие термины, как софтбот, т.е. программный робот (softbot), база знаний (knowbot), программный брокер (software broker) и интеллектуальный объект (smart object). В этой главе мы многократно будем определять термин агент. Начнем с простых согласованных частичных определений и построим определение, которое бы устраивало С++-программистов.

Существует определение, согласно которому агент определяется как некоторая сущность, функционирующая постоянно и автономно в среде, в которой выполняются другие агенты и процессы. Хотя весьма заманчиво принять это определение и развить его, мы не будем этого делать, поскольку оно «с таким же успехом» описывает и другие виды программных конструкций. Многие объектно-ориентированные компоненты функционируют постоянно и автономно в среде, в которой выполняются другие процессы и существуют другие агенты. И в самом деле, многие CORBA-ориентированные системы типа «клиент-сервер» вполне соответствуют этому описанию! Поэтому, если мы заменим в этом определении слово агент словом объект, оно в точности опишет многие объектно-ориентированные системы. Если заглянуть в более официальный источник, Foundation for Intelligent Physical Agents (FIPA), то в соответствии с ним термин агент определяется следующим образом:

Агент — это главный исполнитель в домене. Он обладает одной или несколькими сервисными возможностями, образующими единую и комплексную модель выполнения, которая может включать доступ к внешнему ПО, пользователям (людям) и средствам связи.

Несмотря на то что это определение имеет более структурированную форму, оно также нуждается в дальнейшем уточнении, поскольку под это определение попадают многие серверы (объектно-ориентированные и нет). Это определение в таком виде включило бы слишком много типов программ и программных конструкций. И хотя мы опираемся на FIPA-спецификацию, это базовое определение требует дальнейшей проработки.

 

Агенты: исходное определение

Одной из причин, по которой слово объект может заменить с л ово агент во многих определениях и описаниях агента, состоит в том, что агенты по сути основаны на объектах. И в самом деле, наше первое требование к определению агента заключается в том, что оно в первую очередь должно удовлетворять определению объекта , т.е. мы имеем в виду, что агент — это объект определенного вида. В этой главе особый акцент делается на том, что отличает агента от других категорий объектов. Исходя из того, что С++ поддерживает интерфейсные, контейнерные и каркасные классы, мы можем с таким же успехом ввести и агентные классы. Это приводит нас ко второму требованию, выдвигаемому к определению агентов в С++-среде. В С++ агент реализуется с использованием понятия класса. Типы классов отличаются друг от друга тем, как они функционируют, или тем, как они структурированы. Например, контейнерный класс описывает объект, используемый для хранения других объектов. Интерфейсный класс применяется для описания объекта, который преобразует или адаптирует интерфейс другого объекта. Каркасный класс описывает объект, который содержит шаблон, или образец поведения, являющегося общим для целого семейства других объектов. Агентные классы предназначены для определения объектов, которые обладают тем, что Иогав Шохам (Yohav Shoham) описывает как психическое (интеллектуальное) состояние: «Психическое состояние должно включать такие компоненты, как представления, возможности, варианты выбора и обязательства». Это психическое состояние зачастую описывается моделью убеждений, желаний и намерений (Belief, Desires and Intentions — BDI). Мы расширяем модель BDI, чтобы включить в нее действия. Теперь в нашем первом определении агент описывается как часть ПО, отвечающая следующим требованиям.

1. Это определенный тип объекта (т.е. не все объекты являются агентами).

2. Его реализация использует понятие класса (для агентов весьма существенны инкапсуляция, наследование и полиморфизм).

3. Он содержит набор поведенческих вариантов и атрибутов, которые должны включатьубеждения, желания, намерения идействия.

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

 

Типы агентов

Существует несколько категорий агентов. Несмотря на то что не все агенты можно отнести к одной из них, с их помощью все же можно описать большинство агентов, которые уже нашли практическое применение. В табл. 12.1 перечислено пять основных категорий агентов. Очевидно, существуют и агенты смешанного типа, которые можно отнести к нескольким категориям одновременно, поскольку для распределения агентов по категориям нет никаких жестких правил. Эти категории представлены для удобства и используются в качестве отправной точки в попытке классифицировать агенты, которые, возможно, вам придется разрабатывать или использовать в своей работе.

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

Табл и ца 12.1. Пять ос н ов н ых категор и й аге н тов

Интерфейсные агенты Представляют следующее поколение взаимодействия между человеком и компьютером. Эти агенты обеспечивают новый пользовательский интерфейс с компьютером

Агенты поиска Выполняют различные виды поиска информации

Агенты мониторинга /управления Патрулируют, наблюдают, отслеживают (выполняемые действия), управляют и контролируют устройства и условия, данные и процессы

Агенты сбора данных Уполномочены запросить некоторые данные или услуги от имени пользователя

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

 

В чем состоит разница между объектами и агентами

Агент прежде всего должен отвечать условиям объектной ориентации. Это означает, что агенты и объекты имеют больше общего, чем многие специалисты хотели бы это признать. Именно функциональнал и конструктивная составляющие объектов сближают их с агентами. Объекты по определению самодостаточны и проявляют определенную автономность. Если степень автономности пересекает определенный порог, и объекту предоставляются такие когнитивные (познавательные) структуры данных, как те, что характерны для модели BDI, то такой объект является агентом. Автономный рациональный объект является агенто м. Объект считается рациональны м в случае, если он обладает:

•  м етода м и, которые реализуют некоторую фор м у дедукции, индукции или абдукции;

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

Слелует иметь в виду, что в объектно-ориентированном программировании подпрограммы, определенные для класса, называются методами, а в С++— функциями-членами. Пере м енные или ко м поненты данных, определенные для класса, называются атрибутами, а в С++ — членами данных. Если некоторые функции-члены используются для реализации дедукции, индукции или абдукции с использование м членов данных, которые представляют собой реализации когнитивных структур данных, то такой объект является рациональным. Если рациональный объект при этом пересекает определенный порог автономности, то это и есть агент.

Когнитивные структуры данных — это структуры, используемые для представления таких интеллектуальных компонентов, как убеждения, намерения, обязательства, решения, настроения и знания. Например, мы могли бы обозначить структуру убеждений, используя С++-множество (set).

set Beliefs;

struct statement{

//. . .

float ArrivalTime;

float DepartureTime;

string Destination;

//.. .

};

Здесь инструкции связаны с составлением расписания для некоторого вида общественного, транспорта. Коллекция этих инструкций хранится в С++-множестве set и представляет «убеждения» агента. Это именно то, что мы подразу м евае м под члена м и данных, которые являются реализация м и когнитивных структур данных. Агент должен объявить член данных соответствующим образом.

class agent{

//.. .

set Beliefs;

//. . .

};

В классе agent для обработки множества Beliefs, чтобы сфор м ировать на м ерения, обязательства или планы, используется дедукция, индукция или абдукция. Из нашего определения агентов следует, что, если мы имеем дело с рациональным автономным объектом, то мы имеем дело с агентом. Если он не рациональный, то он не агент, он — просто объект. А о степени автономности мы поговорим подробнее ниже в этой главе.

 

Понятие об агентно-ориентированном программировании

 

Агентно-ориентированное программирование — это процесс назначения работы, порученной программе, одному или нескольким агентам. В декомпозиции работ (Work Breakdown Structure — WBS) в этом случае участвуют только агенты. Если всю работу, которую должна выполнить программа, можно назначить одному или нескольким агентам, мы имеем дело с чистой агентно-ориентированной программой, в которой весь необходимый объем проектирования и разработки требует только агентно-ориентированного программирования. Во многих ситуациях нарялу с агентами в приложении будут задействованы и другие виды объектов и систем, которые не являются агентно-ориентированными, и, следовательно, такое программирование нельзя назвать агентно-ориентированным. Подобное сотрудничество часто имеет место, когда агенты участвуют в работе серверов баз данных, серверов приложений и других типов объектно-ориентированных систем. При создании систем ПО — либо полностью агентно-ориентированных, либо только частично — создаются рациональные объектно-ориентированные программные компоненты.

§ 12:1 Дедукция, индукция и абдукция

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

Все фигуры с тре м я сторона м и являются треугольника м и.

Даннал фигура и м еет три стороны.

Эта фигура — треугольник. <— Вывод получен по делукции.

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

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

Вчера шел дождь.

Позавчера шел дождь.

Дождь шел всю прошлую неделю.

Завтра будет идти дождь. <— Вывод получен по индукции.

Тогда как следствия, полученные в процессе дедукции объявляются непременно истинными (если правила генерирования вывода были применены корректно), то заключения, к которым приходят в процессе индукции, имеют лишь некоторую вероятность быть истинными. Насколько близко эта вероятность приближается к 100%, зависит от характера и контекста утверждений, а также данных, на которые они опираются.

Процесс абдукции позволяет механизму рассуждений сделать наиболее правдоподобный вывод на основе набора утверждений или данных, например, так. Предметы одежды обвиняемого были обнаружены на месте преступления. Межлу обвиняемым и покойником недавно произошел бурный конфликт. ДНК обвиняемого была обнаружена на месте преступления.

Обвиняемый виновен в свершении преступления. <— Вывод получен по абдукции.

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

Основные правила генерирования вывода

 

Роль агентов в распределенном программировании

Возникновение распределенных программ было вызвано практической необходимостью. Нетрудно представить, что существует некоторый ресурс, который нужен программе, но этот ресурс размещен на другом компьютере или в сети. Под такими ресурсами часто понимают базы данных, Web-серверы, серверы электронной почты, серверы приложений, принтеры и крупные запоминающие устройства. Подобными ресурсами обычно управляет часть ПО, именуемал сервером. Другал часть ПО, которой необходимо получить доступ к ресурсам, называется клиентом. Тот факт, что ресурсы и клиент расположены на рааличных компьютерах, приводит к необходимости использования распределенных архитектур. В большинстве случаев не имеет смысла объединять эти программы в одну большую и выполнять ее на одном компьютере и в едином адресном пространстве. Более того, существует множество программ, разработанных в различное время, разными разработчиками и для разных целей, но которые могут успешно использовать преимущества друг друга. Приложение, которое использовало эти программы, эволюционировало определенным образом и в итоге «заслужило звание» распределенного приложения. Поскольку эти программы отделены друг от друга, каждая из них должна иметь собственное адресное пространство и «свои» ресурсы. Когда эти программы используются для совместного решения задачи, они образуют распределенное приложение. Оказывается, что архитектура распределенной программы обнаружила высокую степень гибкости, что позволило применить ее к крупномасштабным приложениям. Во многих приложениях необходимость в распределенной архитектуре обнаруживается довольно поздно, «когда поезд уже ушел». Но если заранее идентифицировать такую необходимость, можно с успехом использовать соответствующие методы проектирования программного обеспечения. Если вы уже точно знаете, что вам нужно разрабатывать распределенное приложение, то следующий вопрос должен прозвучать так: «как именно оно должно быть распределено?». От ответа на этот вопрос будет зависеть, какую модель следует использовать в этом случае. Несмотря на существование множества различных моделей (равноправных узлов и типа «клиент/сервер»), в этой книге мы остановимся только надвух: мультиагентной архитектуре и архитектуре «классной доски».

Оба эти вида архитектуры м огут использовать преи м у щ ества агентов, поскольку агенты представ л яют собой са м одостаточные, автоно м ные и рациональные программные структуры. Рациональность агентов зак л ючается в том, что им известно их назначение. И обычные объекты имеют це л ь, но агенты «знают», какова эта цель. Идентификация наз н аче н ия каждого аспекта ПО — вполне естественный процесс. На этане проектирования нетрудно продумать цель отдельной части ПО, и поэтому простейшая форма декомпозиции ПО состоит в том, чтобы назначить агенту его цель. Затем приходит черед понять, агентов какого класса лучше всего уполномочивать на выполнение той или иной работы. Поскольку агент— это единица модульности в агентно-ориентированной программе (agent-oriented program — АОР), то проблема распределения сводится к поиску средств взаимодействия множества агентов. Процесс проектирования исходного класса агента вбирает в себя все то, что необходимо для идентификации отдельных составных частей распределенной программы. Справившись с созданием агентов как действительно рациональных объектов, мы сможем воспользоваться преимуществами CORBA-спецификации для разработки действительно распределенных мультиагентных систем. CORBA скрадывает сложность распределенного программирования и взаимодействия посредством сетей (intranet Hlnternet). Обзор средств распределенного программирования с использованием CORBA-сиецификации приведен в главе 8. Поскольку агенты являются объектами, этот обзор CORBA-средств имеет силу и для агентов. В главе 6 рассмотрена система PVM (Parallel Virtual Machine — параллельнал виртуальная машина). Систему PVM также можно использовать для значительного упрощения взаимодействия между агентами, существующими в различных процессах или на разных компьютерах. Агенты можно реализовать как CORBA-объекты, либо их можно назначить отдельным PVM-процессам. В обоих случалх взаимодействие агентов упрощается в значительной степени. Если в одном приложении задействовано несколько агентов, то такое приложение представляет собой мультиагентную систему. Если агенты расположены на одном компьютере, то для взаимодействия между собой они могут использовать CORBA-, PVM- или MPI-средства (Message Passing Interface). Агенты в различных процессах также могут использовать такие традиционные методы межпроцессного взаимодействия (IPC), как FIFO-структуры, разделяемую память и каналы. В распределенном программировании есть три основные проблемы.

1. Идентификация декомпозиции ПО распределенного решения.

2. Реализация эффективного и рационального взаимодействия между распределенными компонентами.

3. Обработка исключительных ситуаций, ошибок и частичных отказов.

Несмотря на то что для реализации п. 2 в понятии класса агента нет ничего такого, что было бы свойственно только агентам, смысл п. 1 и 3 почти подразумевается в самой сути агента. Рациональность каждого агента определяет его назначение, а следовательно, и роль, которую он будет играть в решении ПО. Поскольку агенты самодостаточны и автономны, то хорошо продуманный класс агента должен включать необходимые меры по обеспечению их отказоустойчивости.

 

Агенты и параллельное программирование

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

1. Эффективное и рациональное разделение работы между несколькими компонентами.

2. Координация параллельно выполняющихся программных компонентов.

3. Разработка соответствующего взаимодействия (когда это необходимо) между компонентами.

4. Обработка исключительных ситуаций, ошибок и частичных отказов (если агенты функционируют на отдельных компьютерах).

Мультиагентные параллельные архитектуры часто характеризуются как слабосвязанные, т.е. им присущ минимум взаимодействия и взаимозависимости. Каждый агент знает свою цель и обладает методами для ее достижения. В то время как п. 3 не подвластен классу агента, п. 1, 2 и 4 можно легко управлять с помощью классов агентов. Например, при использовании агентов влияние п. 2 уменьшается, поскольку каждый агент рационален, имеет цель, а также способы и средства ее достижения. Поэтому вся ответственность смещается с алгоритма координации и управления на действия каждого агента. Влияние п. 4 также уменьшается, поскольку агенты самодостаточны, рациональны и автономны, а кроме того, хорошо продуманный класс агента должен включать необходимые меры по обеспечению отказоустойчивости агентов. Поскольку состояние агента инкапсулировано, ответственность за защиту критических разделов в объекте агента целиком воалагается на класс агента. Агент должен приводить в исполнение собственные стратегии доступа к данным. Возможные стратегии доступа, из которых могут выбирать агенты, перечислены в табл. 12.2.

Таблица 12.2. Стратегии доступа

EREW  Монопольное чтение, монопольная запись

(Exclusive Read Exclusive Write)

CREW  Параллельное чтение, монопольная запись

(Concurrent Read Exclusive Write)

ERCW  Монопольное чтение, параллельная запись

(Exclusive Read Concurrent Write)

CRCW  Параллельное чтение, параллельная запись

(Concurrent Read Concurrent Write)

Класс каждого агента должен определить, какал именно стратегия доступа приемлема в мультиагентной среде. В ряде случаев реализуются не просто отдельные стратегии доступа, перечисленные в табл. 12.2, а их комбинации. Это позволяетупростить параллельное программирование, поскольку разработчик может работать на более высоком уровне и не беспокоиться о построении мьютексов, семафоров и пр. Мультиагентные решения позволяют разработчику не погружаться в детали координации вызова каждой функции и организации доступа к данным. Каждый агент имеет цель. Каждый агент рационален, а следовательно, обладает определенной логикой для достижения своей цели. Процесс программирования в этом случае больше напоминает делегирование задач, а не координацию задач, которая характерна для традиционного параллельного программирования. Поскольку агентно-ориентированное программирование — это объектно-ориентированное программирование специального вида, применительно к агентам используется более декларативный вид параллельного программирования по сравнению с традиционным процедурно-ориентированным программированием, которое часто реализуется такими языками, как Fortran или С. Разработчик лишь определяет, что нужно сделать и какие агенты должны это сделать, т.е. выходит, что параллелизм практически сам заботится о себе. При этом всегда существует некоторый объем программирования, связанного с координацией и организацией взаимодействия, но агентно-ориентированное программирование сводит этот необходимый объем к минимуму. Однако обо всех этих «плюсах» можно говорить лишь при условии существования классов агентов. Очевидно, кто-то должен спроектировать классы агентов и написать их код. Теперь самое время разобраться в том, что должен содержать класс агента.

 

Базовые компоненты агентов

 

Агент объявляется с использованием ключевого слова class. Компоненты агента должны состоять из С++-членов данных и функций-членов. Логическая структура класса агента показана на рис. 12.1.

Класс агента (см. рис. 12.1) определяет типичные методы инициализации, чтения и записи, которые должен иметь практически любой объект. В «джентльменский набор» входят конструкторы, деструкторы, операторы присваивания, обработчики исключений и т.д. Атрибуты этого класса включают переменные состояния, определяющие объект. Если же ограничиться перечнем этих атрибутов и методов, мы получим только традиционный объект. Рациональный компонент создают когнитивные структуры данных и методы рассуждений (логического вывода). А ведь именно рациональный компонент трансформирует «обычный» объект в агент.

#img_81.png Рис. 12.1. Логическая структура класса агента

 

Когнитивные структуры данных

 

Под структурой данных пони м ается набор правил, при м еняе м ых д ля логической организации данных, а также правила доступа к этой логической организации. Именно метод организации определяет, как данные должны быть концептуально структурированы и какие операции доступа могут быть применены к этой структуре. Если для типов данных вообще и абстрактных типов данных (abstract datatypes — ADT) в частности важно, что хранить, то для структур данных важно, как хранить. Напри м ер, целочисленный тип данных определяет некоторую «сущность», которая характеризуется наличием компонента данных и некоторого количества арифметических операций (например, сложение, вычитание, умножение, деление и т.д.). Этот компонент данных не имеет дробной части и состоит из отрицательных и положительных чисел. Спецификация типа данных ничего не «говорит» о том, как целые числа нужно использовать или как к ним получить доступ. Однако спецификация структуры данных (например, стека) определяет список элементов, сохраняемых по принципу «последним прибыл — первым обслужен» (last-in-first-out— LIFO). Структура данных стека также определяет, что элементы из нее можно извлекать только по одному за раз и причем только из вершины стека. Другими словами, элемент, помещенный в стек последним, должен быть извлечен из него раньше остальных элементов. Это означает, что структура данных стека определяет не только характер организации элементов, но и характер доступа к ним (т.е. как элементы можно помещать в структуру, опрашивать, изменять, удалять и т.п.). Когнитивные структуры данных ограничивают правила организации данных и доступа к ним такими, которые относятся к области логики и эпистемологии. Особенности когнитивных структур данных определяются правилами логического вывода, методами рассуждений (т.е. делукцией, индукцией иабдукцией), понятиями эпистемологических данных, знания, обоснования, убеждений, посылок, высказываний, ошибочных доказательств и заключений.

Тогда как для традиционных структур данных вполне обычными являются, например, алгоритмы сортировки и поиска, то для когнитивных структур данных более приемлемы методы рассуждений. Абстрактные типы данных, используемые вместе с когнитивными структурами данных, часто включают следующие:

вопросы  события

факты  вре м я

предположения  заблуждения

убеждения  цель

утверждени я   обоснование

заключения

Безусловно, с когнитивными структурами данных можно сочетать и другие типы данных, но приведенные выше являются характеристиками программ, которые используют такие рациональные программные компоненты, как агенты. Эти абстрактные типы обычно реализуются как типы данных, объявленные с помощью ключевых слов struct или class. Напри м ер, так.

struct question{

class justification{

//...

//...

string RequiredInformation;

time EventTime;

target_object QuestionDomain; 

bool Observed;

string Tense; 

bool Present;

string Mood; 

//...

//... };

};

Шаблонные и контейнерные С++-классы можно использовать для организации таких когнитивных структур данных, как знания, например, так.

class preliminary_knowledge{ //.. .

map Opinion;

map SimpleKnowledge;

set Argument; //.- .

};

 

Методы рассуждений

Под методами рассуждений (см. рис. 12.1) пони м ают дедукцию, индукцию и абдукцию. (Краткое описание этих методов приведено в параграфе 12.1.) Несмотря на то что в агентно-ориентированной архитектуре требуется их использование, не существует конкретных ссылок на то, как они реализуются. Делукция, индукция и абдукция относятся к процессам высокого уровня. Подробности реализации этих процессов — личное дело разработчика ПО. Рассуждение — это процесс выведения логического заключения на основании посылок, истинность которых предполагается или точно установлена. Не существует единственно правильного способа реализации процесса рассуждений, ино г да называе м о г о машиной (и л и м еха н из м о м) логического вывода. При этом на практике приме н яется н еско л ько распростра н е н ных способов реализации это г о процесса. Напри м ер, можно испо л ьзовать методы прямого построения цепочки (рассуждений от исходных посылок к целевой гипотезе) или обратного построения цепочки (рассуждений от целевой гипотезы к исходным посылкам). Нашли здесь применение методы анализа целей и средств, а также такие алгоритмы обхода графов, как «поиск вглубь» (Depth First Search — DFS) и «поиск в ширину» (Breadth First Search — BFS). Существует также целал совокупность методов доказательства теорем, которые можно использовать для реализации методов рассуждений и механизмов логического вывода. Здесь важно отметить, что класс агента может иметь один или несколько методов рассуждений. Описание самых основных способов их реализации приведено в табл. 12.3.

Таблица 12.3. Основные способы реализации методов рассуждений

Обратное построение цепочки Управляемый целями метод, в котором процесс начинается с предположения, утверждения или гипотезы и стремится найти подтверждающие доказательства

Прямое построение цепочки Управляемый данными метод, который начинается с анализа имею щ ихся данных или фактов и приходит к определенным выводам

Анализ целей и средств   Использует множество операторов для последовательного решения подзадач до тех пор, пока не будет решена вся задача в целом

Эти методы достаточно понятны и широко доступны во многих библиотеках, оболочках и языках программирования. Эти методы являются «строительными блоками» для базовых методов рассуждений. Чтобы понять, как происходит процесс рассуждения, используем одно из правил генерирования вывода, а именно молус поненс (правило отделения), и построим простой метод рассуждения. Возьмем следующее утверждение. Если существует автобусный маршрут из Детройта в Нью-Йорк, то Джон поедет в отпуск. Если мы выясним, что автобусный маршрут из Детройта в Нью-Йорк действительно существует, то будем знать, что Джон поедет в отпуск. Правило молус поненс имеет следующий формат.

P Q P

Q

Здесь:

P = Если су щ ествует автобусный маршрут из Детройта в Нью-Йорк, Q = Джон поедет в отпуск.

Мы могли бы спроектировать простой агент обеспечения решения, который позволит нам узнать, поедет Джон в отлуск или нет. Этому агенту нужно узнать все возможное об автобусных маршрутах. Предположим, у нас есть список автобусных маршрутов:

Толедо-Кливленд Детройт-Чикаго Янгстаун-Нью-Йорк

Кливленд-Колумбус Цинциннати-Детройт Детройт-Толедо

Колумбус-Нью-Йорк Цинциннати-Янгстаун

Каждый из этих маршрутов представляет обязательство, взятое на себя компанией ABC Bus Company. Если наш агент получит доступ к расписанию автобусных маршрутов этой компании, то приведенный выше список маршрутов можно будет использовать для представления некоторой части убеждений нашего агента. Возникает вопрос: как перейти от списка маршрутов к убеждениям? Для начала попробуем разработать простую структуру утверждений.

struct existing_trip{

//. . .

string From;

time Departure;

string То;

time Arrival;

//.. .

};

Затем попытаемся использовать контейнерный класс для представления убеждений нашего агента в отношении автобусных маршрутов.

set BusTripKnowledge ;

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

//...

existing__trip Trip;

Trip. From. append (" Toledo " ) ;

Trip.To.append( «Cleveland») ;

Trip.Departure(«4:3 О») ;

Trip.Arrival(«5:45») ;

BusTripKnowledge. insert(Trip) ;

//...

Если мы поместим каждый маршрут в множество BusTripKnowledge, то убеждения нашего агента об автобусных перевозках компании ABC Bus Company будут полностью описаны. Обратите внимание на то, что прямого маршрута из Детройта в Нью-Йорк не существует. Но Джон может добраться в Нью-Йорк из Детройта более сложным путем, осуществив следующие переезды автобусом:

из Детройта в Толедо; из Толедо в Кливленд; из Кливленда в Кол^мбус; из Колумбуса Нью-Йорк.

Поэтому, несмотря на то, что компания ABC Bus Company не предоставляет прямого маршрута (из пункта А в пункт Б), она позволяет совершить переезд с большим количеством промежуточных остановок. Задача состоит в следую щ ем: как об этом может узнать наш агент? Агент на основе своих знаний об автобусных маршрутах должен обладать некоторым алгоритмом генерирования вывода о том, су щ ествует ли маршрут из Детройта в Нью-Йорк. Мы используем простой цепной метод. Просматриваем элементы множества BusTripKnowledge и находим первый маршрут из Детройта— из Детройта в Чикаго. Опрашиваем атрибут То этого элемента. Если бы он был равен значению «Нью-Йорк», процесс поиска был бы прекращен, поскольку мы нашли нужный маршрут. В противном случае сохраняем найденный (промежуточный) маршрут в стеке. Затем ищем маршрут с атрибутом From, равным «Чикаго». При этом может оказаться, что таких маршрутов не прелусмотрено вообще. Поскольку далее хранить элемент множества, соответствующий маршруту «Детройт-Чикаго», нет никакого смысла, мы удаляем его из стека, сделав пометку, что этот маршрут уже был рассмотрен. Затем повторяем поиск маршрута с отправлением из Детройта. Находим такой маршрут: «Детройт-Толедо». Проверяем, не равен ли его атрибут То значению «Нью-Йорк», и поскольку наши надежды не оправдались, сохраняем этот элемент в стеке. Затем ищем маршрут с атрибутом From, равным «Толедо». Находим маршрут «Толедо-Кливленд» и также помещаем его на хранение в стек. После это г о просматриваем маршруты в надежде найти элемент, у которого атрибут From был бы равен значению «Кливленд». Для каждо го найденного маршрута проверяем значение атрибута То. Если он равен значению " Нью-Йорк» , то промежуточные маршруты, помещенные в стек, представляют в целом маршрут из Детройта в Нью-Йорк, начало которого находится на «дне» стека, а его конечный пункт — в вершине. Если мы пройдем по всему списку маршрутов и не найдем ни одного с атрибутом То, равным значению «Нью-Йорк», или иссякнут возможные варианты проверки атрибута То для верхнего элемента стека, то мы, извлекал верхний элемент из стека, будем искать следующий элемент, значение атрибута From которого совпадает со значением атрибута То элемента, расположенного в вершине стека. Этот процесс повторяется до тех пор, пока стек не опустеет или мы все-таки не най дем нужный маршрут. Для определения, существует ли маршрут из пункта А в пункт Б, используется в данном случае упрощ ен ный метод DFS (Depth First Search — «поиск вглубь»).

Наш простой агент будет использовать этот DFS-метод для выяснения, существует ли маршрут из Детройта в Нью-Йорк. Выяснив этот факт, агент может обновить свои убеждения насчет Джона. Теперь агент убежден, что Джон поедет в отпуск. Предположим, мы внесли дополнительное прелусловие относительно отпуска Джона.

Если Джон обслужит 15 или больше новых клиентов, его доходы превысят (>) 150000.

Если доходы Джона превысят 150000 и существует маршрут из Детройта в Нью-Йорк, то Джон отправится в отпуск.

Теперь агент должен выяснить, превышают ли доходы Джона лумму 150000 и существует ли маршрут из Детройта в Нью-Йорк. Чтобы выяснить положение дел насчет доходов Джона, агент должен сначала узнать, обслужил ли Джон хотя бы 15 новых клиентов. Предположим, мы уверяем программного агента в том, что Джон обслужил 23 новых клиента. Затем агент должен убедиться в том, что его доходы превышают 150000. На основе содержимо г о множества BusTripKnowledge агент сумел прийти к выволу о существовании маршрута из Детройта в Нью-Йорк. На основании убеждений об автобусных маршрутах и 23 новых клиентах агент использует процесс прямого построения цепочки (т.е. рассуждений от исходных посылок к целевой гипотезе) и приходит к заключению, что Джон таки поедет в отпуск. Формат рассуж-дений этого процесса имеет такой вид.

А -> В (В и С) -> D А С

D

Здесь:

А=ЕслиДжон обслужит не менее 15 новых клиентов, В = Доходы>150000,

С = Су щ ествует автобусный маршрут из Детройта в Нью-Йорк, D = Джон поедет в отпуск.

В этом примере агент убеждается, что эле м енты А и С истинны. С использование м правил ведения рассуждений агент заключает, что эле м енты В и D равны значению ИСТИНА. Следовательно, агент делает вывод о том, что Джон поедет в отпуск. Подобный вид обработки имеющихся данных можно было бы применить к агенту в ситуации, когда у директора фирмы в подчинении находятся сотни или даже тысячи служащих, и он хотел бы, чтобы агент регулярно составлял почасовой график работы для своих служащих. Директор намерен затем получать от агента справку о том, кто работал, кто находился в отпуске по болезни, а кто — в очередном отпуске и т.д. Агент должен обладать знаниями и полномочиями устанавливать график работы. Каждую неделю агент должен представлять ряд приемлемых графиков работы, очередных отпусков и сведений о пропусках по болезни. Агент в этом случае для получения результата использует простой метод прямого построения цепочки и метод DFS. Чтобы реализовать этот вид рассуждений, мы использовали такие типы данных, как struct и классы стеков и множеств. Эти классы используются для хранения знаний, предположений иметодов рассуждений. Они позволяют реализовать когнитивные структуры данных (Cognitive Data Structures — CDS). Для поддержки процесса рассуждений, а именно при опросе наших структур данных (стека и множества) мы использовали DFS-методы.

При сочетании метода прямого построения цепочки и метода DFS создается процесс, в соответствии с которым одно предположение может быть подтверждено на основе уже принятых предыдущих. Это очень важный момент, поскольку наш агент при достижении цели должен знать, что в действительности следует считать корректным. Такой подход также влияет на отношение к вопросам параллельного программирования. Тот факт, что агент рационален и действует в соответствии с правилами построения рассуждений, позволяет разработчику сосредоточиться на корректном моделировании задачи, выполняемой агентом, а не на стремлении явно управлять параллелизмом в программе. Минимальные требования параллелизма, выражаемые тремя «китами» — декомпозицией, взаимодействием и синхронизацией (decomposition, communication, synchronization — DCS), — по большей части относятся к архитектуре агента. Каждый агент для своего поведения имеет логическое обоснование. Это обоснование должно опираться на хорошо определенные и хорошо понимаемые правила ведения рассуждений. Декомпозиция зачастую выражается в простом назначении агенту одного или нескольких основных указаний (директив). Декомпозиция работ в этом случае должна иметь естественный характер и в конце концов выразиться в параллельных или распределенных программах, которые нетрудно поддерживать и развивать. Взаимодействие агентов проще представить, чем взаимодействие анонимных модулей , поскольку границы между агентами более четки и очевидны. Каждый агент имеет цель, которая лежит на поверхности. Знания, или информация, необходимые каждому агенту для достижения его цели, в этом случае легко определяются. Чтобы позволить агентам взаимодействовать, разработчик может использовать простые MPI-функции или средства взаимодействия объектов, которые являются частью любой CORBA-реализации. При обеспечении взаимодействия агентов самыми сложными являются следующие моменты:

• посредством чего должно происходить взаимодействие;

• кому нужно взаимодействовать;

• когда должно происходить взаимодействие;

• какой формат должно иметь взаимодействие.

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

 

Реализация агентов в С++

 

Рассмотрим упрощенный вариант предыдущего примера агента и продемонстрируем, как его можно реализовать в С++. Цель этого агента — составлять график отпусков и выполнять подготовку к поездкам владельца компании ABC Auto Repair Company. В компании работают десятки служащих, и поэтому у хозяина нет времени заботиться о проведении своего очередного отпуска. Кроме того, если хозяин не получит определенного объема прибыли, об отпуске не может быть и речи. Поэтому владельцу компании хотелось бы, чтобы агент распланировал его отпуска равномерно по всему голу при условии процветания фирмы. По мнению владельца компании, главное, чтобы агент работал автоматически, т.е. после инсталляции на компьютере о нем можно было не беспокоиться. Когда агент определит, что подошло время для отпуска, он должен предъявить план проведения отпуска, забронировать места в отеле и проездные билеты, а затем по электронной почте представить хозяину маршрут. Владелец должен побеспокоиться только о формировании задания для агента. Он должен указать, куда желает отправиться и какой объем прибыли необходимо получить, чтобы запланированная поездка состоялась. Теперь рассмотрим, как можно спроектировать такой агент. Вспомним, что рациональный компонент (см. рис. 12.1) класса агента состоит из когнитивных структур данных и методов рассуждений (стратегий логического вывода). Когнитивные структуры данных (CDS) позволяют хранить убеждения, предположения, знания, заблуждения, факты и пр. Для доступа к этим когнитивным структурам данных в процессе решения проблемы и выполнения задач класс агента использует стратегии логического вывода. Для реализации CDS-структур данных и методов построения рассуждений можно использовать ряд контейнерных классов и алгоритмов, которые содержатся в стандартной библиотеке С++.

 

Типы данных предположений и структуры убеждений

Этот агент обладает убеждениями о показателях авторемонтной мастерской. Убеждения составляют информацию о том, сколько клиентов обслуживается в час, какова загрузка ремонтных секций в день и общий объем продаж (запчастей и услуг) за некоторый период времени. Кроме того, агент знает, что владелец фирмы любит путешествовать только автобусами. Поэтому агент хранит информацию об автобусных маршрутах, которые могут для отпускника оказаться привлекательными. В программе, насыщенной математическими вычислениями, используются в основном целочисленные значения и числа с плавающей точкой. В графических программах участвуют пиксели, линии, цвета, геометрические фигуры и пр. В агентно-ориентированной программе основными типами данных являются предположения, правила, утверждения, литералы и строки. Для построения типов данных, свойственных агентно-ориентированному программированию, мы будем опираться на объектно-ориентированную поддержку, прелусмотренную в С++. Итак, рассмотрим объявление класса предположения (листинг 12.1).

// Листинг 12.1. Объявление класса предположения

template class proposition {

//...

protected:

list UniverseOfDiscourse;

bool TruthValue; public-virtual bool operator()(void) = О;

bool operator&&(proposition &X);

bool operator||(proposition &X);

bool operator||(bool X);

bool operator&&(bool X);

operator void*();

bool operator!(void);

bool possible(proposition &X);

bool necessary(proposition &X);

void universe(list &X);

//.. .

};

Предположение представляет собой утверждение, тема (предмет) которого подтверждается или отрицается предикатом. Предположение может принять значение ИСТИНА или ЛОЖЬ. Предположение можно использовать для фиксации одного убеждения, которое есть у агента. Кроме того, в качестве предположения может быть представлена некоторая другая информация, которая предлагается агенту и которую агент необязательно воспринимает как убеждение. Для представления предположений используется когнитивный тип данных, который должен быть таким же функциональным в агентно-ориентированной программе, как целочисленные и вещественные типы данных в математических программах. Поэтому, чтобы обеспечить некоторые основные операторы, применимые к предположениям, мы используем C++-средства перегрузки операторов. В табл. 12.4 показано, как такие операторы преобразуются в логические.

Класс proposition (см. листинг 12.1) представляет собой упрощенную версию (с сокращённым набором функциональных возможностей). Назначение этого класса— сделать использование типа данных proposition таким же простым и естественным, как использование любого другого С++-типа данных. Обратите внимание на слелующее объявление в классе proposition: virtual bool operator()(void) = 0;

Таблица 12.4. Преобразование операторов влогические

Пользовательские C++onepamopы   Распространенныелогические операторы

&& ^

|| v

! ~

possible  ♦

necessary  □

Это объявление чисто виртуального метода. Если в классе объявлен чисто виртуальный метод, это означает, что данный класс — абстрактный, и из него нельзя создавать объекты, поскольку в нем отсутствует определение этого метода. Метод лишь объявлен, но не определен. Абстрактные классы используются для определения стратегий и являются своего рода проектами производных классов. Производный класс должен определить все виртуальные функции, которые он наслелует от абстрактного класса. В данном случае класс proposition используется для определения минимального набора возможностей, которыми может обладать класс-потомок. Необходимо также отметить еще одну важную особенность класса proposition (см. листинг 12.1): это шаблонный класс. Он содержит такой член данных: list UniverseOfDiscourse;

Этот член данных предполагается использовать для хранения значения предметной области, к которой относится предположение. В логике область рассуждения содержит все легальные сущности, которые могут рассматриваться при обсуждении. Здесь мы используем контейнер list. Поскольку в общем случае темы обсуждения могут быть самыми разными, мы используем контейнерный класс. Список UniverseOfDiscourse мы объявляем защищенным (protected), а не закрытым (private), чтобы к нему могли получить доступ все потомки класса proposition. Классу proposition также «знакомы» такие понятия модальной логики, как логическая необходимость и вероятность, которые весьма полезны в агентно-ориентированном программировании. Модальнал логика позволяет агенгу различать такие определения, как «вероятно, ИСТИНА» и «несомненно, ИСТИНА». Основные операторы, используемые для выражения логической необходимости и вероятности, перечислены в табл. 12.4. Мы определяем эти методы только в описательных целях; их реализация выходит за рамки рассмотрения в этой книге. Но они являются частью классов предположений, которые мы успешно применяем на практике. Чтобы сделать класс proposition «годным к употреблению», выведем из него новый класс и назовем его trip_announcement. Класс trip_announcement представляет собой утверждение о существовании автобусного маршрута из некоторого исходного пункта (отправления) в пункт назначения. Например, предположим, что существует автобусный маршрут из Детройта в Толедо. Эта информация позволяет сформулировать высказывание, которое может быть либо истинным, либо ложным. Если бы нас интересовало, когда это высказывание истинно или ложно, мы бы воспользовались понятиями временной логики. Временняя логика— это логика времени. Агенты также применяют обоснования, зависящие от времени. Но в данном случае все предположения относятся к текущему времени. Это утверждение декларирует, что в данное вре м я существует автобусный м аршрут из Детройта в Толедо. Агент должен «у м еть» удостовериться в этом и либо «довериться» это м у факту, либо отвергнуть его как ложное высказывание. Теперь м ожно расс м отреть объявление класса trip_armouncement, представленное в листинге 12.2.

// Листинг 12.2. Объявление класса trip_announcement

class trip_announcement :

publiс proposition{

//.. .

protected:

string Origin; string Destination;

stack Candidates; public:

bool operator()(void);

bool operator==(const trip_announcement &X) const;

void origin(string X);

string origin(void);

void destination(string X);

string destination(void);

bool directTrip(void);

bool validTrip(list::iterator I,

string TempOrigin);

stack candidates(void);

friend bool operator||(bool X,trip_announcement &Y);

friend bool operator&&(bool X,trip_announcement &Y);

//. . .

};

Обратите вни м ание на то, что класс trip_armouncement наследует класс proposition. Вспо м ните, что класс proposition является шаблонным и требует задания параметра, определяющего тип. Объявление

class trip_announcement :

public proposition

{... } ;

обеспечивает класс proposition требуе м ы м типо м. Кро м е того, важно от м етить, что класс trip_announcement определяет операторный м етод operator (). Следовательно, наш класс trip_armouncement — конкретный, а не абстрактный. Теперь мы можем объявить и использовать предположение типа trip_announcement непосредственно в программе агента. В классе trip_announcement определены слелую-щие дополнительные члены данных: Origin Destination Candidates

Эти члены данных используются для указания пунктов отправления и назначения автобусного маршрута. Если автобусный маршрут требует пересадки с одного автобуса надругой и несколько остановок в пути, то член данных Candidates будет содержать полный путь следования. Следовательно, объект класса trip_armouncement представляет собой утверждение об автобусном маршруте и пути следования. В классе trip_armouncement также определены некоторые дополнительные операторы. Эти операторы позволяют уравнять класс trip_announcement «в правах» со встроенны м и типа м и данных языка С++. Помимо убеждений относительно автобусных м аршрутов, агент также обладает убеждения м и, связанны м и с показателя м и успешности функционирования расс м атривае м ой ко м пании. Эти убеждения отличаются по структуре, но в основном содержат высказывания, которые могут быть истинными либо ложными. Итак, мы снова испо л ьзуе м класс proposition в качестве базового. Влистинге 12.3 представлено объявление класса p eformance_statement.

// Листинг 12.3. Объявление класса performance_statement

class performance__statement :

public proposition{

//. . .

int Bays;

float Sales;

float PerHour;

public:

bool operator() (void);

bool operator==(const performance__statement &X) const;

void bays(int X);

int bays(void);

float sales(void);

void sales(float X);

float perHour(void);

void perHour(float X);

friend bool operator||(bool X,performance_statement &Y);

friend bool operator&&(bool X,performance_statement &Y); //. . .

};

Обратите вни м ание на то, что этот класс также обеспечивает шаблонный класс proposition параметром .

class performance_statement :

public proposition {...}

Благодаря это м у объявлению класс proposition теперь определен д ля объектов типа performance_statement. Класс performance_statement используется для представления убеждений об объе м е продаж, количестве обслуженных клиентов (в час) и загрузке ре м онтных секций в день. Для каждого из перечисленных убеждений о том, что агент имеет в соответствующей области, существует отдельное высказывание. Эта инфор м ация хранится в таких членах данных:

Bays

Sales

PerHour

Такие высказывания, как «По секции 1 объе м продаж составил 300тыс. долл., обслужено 10 клиентов в час, а коэффициент загрузки равен 4», м ожно представить с по м о щ ью объекта класса performance_statement. Итак, наш класс агента и м еет две категории убеждений, реализованных в виде данных, тип которых выведен из класса proposition. На рис. 12.2 представлена UML-диагра мм а классов trip_announcement и performance__statement. Эти классы предназначены для хранения структуры убеждений агента.

#img_82.png Рис. 12.2. UML-диаграмма классов trip_announcement и performance_statement

 

Класс агента

 

Классы, представленные на рис. 12.2, образуют фундаментдля когнитивных структур данных агента, которые делают агента рациональным. Именно рационализм класса агента отличает его от других типов объектно-ориентированных классов. Рассмотрим объявление класса агента, приведенное в листинге 12.4.

// Листинг 12.4. Объявление класса agent

class agent{

//.. .

private:

performance_statement Manager1;

performance_statement Manager2;

performance_statement Manager3;

trip_announcement Trip1;

trip_announcement Trip2;

trip_announcement Trip3;

list TripBeliefs;

list PerformanceBeliefs;

public:

agent(void);

bool determineVacationAppropriate(void);

bool scheduleVacation(void);

void updateBeliefs(void);

void setGoals(void);

void displayTravelPlan(void);

//.. .

} .

Как и классы предположений, класс агента представляет собой упрощенную версию. Полный листинг объявления класса, который можно было бы использовать на практике, занял бы три или четыре страницы. Но для описательных целей, которые мы преслелуем в этой книге, приведенного вполне достаточно. Итак, класс agent содержит два контейнера-списка.

li s t TripBeliefs;

list PerformanceBeliefs;

Контейнеры типа list — это стандартные С++-списки. Каждый список используется для хранения коллекции текущих убеждений агента. «Мировоззрение» нашего простого агента ограничено знаниями об автобусных маршрутах и характеристиках успешности его владельца. Содержимое этих двух контейнеров представляет полные знания агента и набор его убеждений. Если в этих списках есть утверждения, в которые агент больше не верит, их следует удалить. Если в процессе рассуждений агент обнаруживает новые утверждения, они добавляются в список уже существующих убеждений. Агент имеет постоянный доступ к информации об автобусных маршрутах и эффективности ведения бизнеса его владельца и при необходимости может обновлять свои убеждения. Помимо убеждений, агент имеет цели, которые иногда представляются как желания в модели убеждений, желаний и намерений (Beliefs, Desires, Intentions — BDI). Цели поддерживают основные директивы, выдаваемые агенту клиентом. В нашем случае цели сохраняются в высказываниях, приведенных ниже.

performance_statement Manager1;

performance_statement Manager2;

performance_statement Manager3;

trip_announcement Trip1;

trip_announcement Trip2;

trip_announcement Trip3;

С л едует иметь в виду, что мы значительно упрощаем представление целей и директив в классе агента. Но все же этого достаточно, чтобы понять, как построены эти структуры. Три Manager -утверждения содержат цели, связанные с эффективностью бизнеса, которые должны быть удовлетворены, прежде чем владелец фирмы сможет хотя бы подумать об отпуске. Три Trip -утверждения содержат автобусные маршруты, по которым владелец фирмы хотел бы прокатиться при условии успешности его бизнеса. Убеждения вместе с директивами образуют базовые когнитивные типы данных, которыми располагает агент. Используемые агентом стратегии логического вывода вместе с этими когнитивными типами данных образуют когнитивную структуру данных агента (Cognitive Data Structure — CDS). На базе CDS

Намерения или планы должны быть обработаны аналогичным образом. Если агент может выполнить директивы, он распланирует поездку и по электронной почте подробно сообщит об этом своему владельцу. Агент приступает к своим обязанностям в момент создания объекта. Фрагмент конструктора агента представлен в листинге 12.5

// Листинг 12.5. Конструктор класса agent

agent: :agent(void) {

setGoals();

updateBeliefs () ;

if(determineVacationAppropriate()){

displayTravelPlan(); scheduleVacation();

cout « «Сообщение о возможности отпуска.» « endl;

} else {

cout « «В данное время отпуск нецелесообразен.» « endl;

}

 

Цикл активизации агента

Многие определения агентов включают требования непрерывности и автономности. Идея состоит в том, что агент должен непрерывно выполнять поставленные перед ним задачи без вмешательства оператора. Агент обладает способностью взаимодействовать со своей средой и (до некоторой степени) контролировать ее благодаря наличию цепи обратной связи. Непрерывность и автономность часто реализуются в виде событийного цикла, при выполнении которого агент постоянно получает сообщения и информацию о событиях. Эти сообщения и события агент использует для обновления своей внутренней модели мира, намерений и предпринимаемых действий. Однако непрерывность и автономность — понятия относительные. Одни агенты должны активизироваться каждую микросекунду, в то время как другие — лишь один раз в год. А в случае программного обеспечения полетов в дальний космос агент может иметь цикл даже больше одного года. Поэтому мы не будем акцентировать внимание на физических событийных циклах и постоянно активных очередях сообщений. Такая организация может подходить для одних агентов, но оказаться непригодной для других. Мы пришли к выводу, что лучше всего здесь применить понятие логического цикла. Логический цикл может (или не может) быть реализован как событийный. Логический цикл может длиться от одной наносекунды до некоторого количества лет. Общий вид простого логического цикла активизации агента показан на рис. 12.3.

Область рассуждения (см. рис. 12.3) представляет все, с чем наш агент может легитимно взаимодействовать. Эта область может состоять из файлов, информации от портов или устройств сбора данных. Получаемая информация должна быть представлена в виде предположений или утверждений (высказываний). Обратите внимание на существование цепи обратной связи от выходных данных агента к входным. Наш агент (см. листинг 12.4) активизируется только несколько раз в год. Следовательно, нет смысла помещать его в постоянно выполняющийся событийный цикл. Наш агент должен периодически активизироваться в течение года для выполнения своих задач. В листинге 12.5 представлен конструктор агента. При активизации агент устанавливает цели, обновляет убеждения, а затем определяет уместность отпуска. Если отпуск возможен, агент предпринимает некоторые действия и по электронной почте уведомляет об этом владельца фирмы. Если же отпуск в данное время нецелесообразен, владелец получает от агента сообщение другого содержания.

#img_83.png Рис. 12.3. Общий вид простого логического цикла активизации агента

12.4.2.2. Стратегии логического вывода агента

Этот агент обладает способностями рассуждать, реализованными частично классом proposition и частично м етодо м determineVacationAppropriate() . Вспомните, что в классе proposition объявлен метод operator() =0 в виде чисто виртуальной функции. Поэто м у в производно м к л ассе необходи м о реализовать м етод operator (). Мы используем этот оператор, чтобы объект предположения мог самостоятельно определить свою «суть», т. е. понять, истинно данное предположение или ложно. Это означает самодостаточность классов предположений. Именно в самодостаточности и состоит фундаментальный принцип объектно-ориентированного программирования: класс представляет собой самостоятельную конструкцию, инкапсулирующую его характеристики и поведение. Итак, одной из основных линий поведения класса предположений и его потомков является способность определять, истинно данное предположение или нет. Для реализации этого средства используется перегрузка операторов и объекты-функции. Рассмотри м фрагменты определения класса proposition и определений его потомков.

//Листинг 12.6. Фрагменты определений класса

// proposition и его потомков

template bool proposition::operator&&(

proposition &X)

{

return((*this)() &&X());

template bool proposition::operator||(

proposition &X)

{

return((*this)() || X());

template proposition::operator void*(void) {

return((void*)(TruthValue));

bool trip__announcement::operator()(void) {

list::iterator I; if(directTrip()){

return(true);

}

I = UniverseOfDiscourse.begin();

if(validTrip(I,Origin)){

return(true);

}

return(false);

}

Операторы "||" и "&&", используемые в классах предположений, позволяют определить, истинно данное предположение или ложно. В каждом из этих определений операторов в конечном счете вызывается метод operator () , определенный в классе-потомке. Обратите внимание на определение оператора "||" (см. листинг 12.6). Этот оператор определен следующим образом.

template bool proposition::operator||

(proposition &X)

{

return((*this)() || X());

}

Это определение позволяет использовать следующий код.

trip_announcement А;

performance_statement В;

if (А || В) {

// Какие-нибудь действия.

}

При вычис л ении выражений А или В будет вызван оператор operator (). Каждый класс предположений определяет поведение оператора operator () по своему. Напри м ер, в классе trip_announcement оператор operator () определяется так.

bool trip_announcement::operator()(void) {

list::iterator I;

if(directTrip()){

return(true);

}

I = UniverseOfDiscourse.begin();

if(validTrip(I, Origin)){

return(true);

}

return(false);

}

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

Детройт - Толедо

Толедо - Колу м бус

Тогда объект класса trip_announcement «доложит» о то м, что утверждение о су щ ествовании автобусного м аршрута из Детройта в Колу м бус истинно, нес м отря на то, что область рассуждений не содержит утверждения о пря м о м маршруте:

Детройт - Колу м бус

Объект класса trip_announcement действительно проверит, существует ли прямой маршрут из Детройта в Колумбус. Если он существует, объект возвратит значение ИСТИНА. В противном случае он попытается найти обходной путь. Подобное поведение реализуется так.

if(directTrip()){

return(true);

}

I = UniverseOfDiscourse.begin();

if(validTrip(I,Origin)){

return(true);

}

«Самоопределением» истинности объект обязан оператору operator () класса trip_anouncement. Метод directTrip () довольно прост, и его работа заключается в последовательном просмотре области рассуждений на предмет существования следующего утверждения:

Детройт - Колу м бус

Метод validTrip () , чтобы узнать, существует ли обходной путь, использует технологию поиска вглубь (Depth First Search— DFS). Определения методов validTrip () и directTrip () приведены в листинге 12.7.

// Листинг 12.7. Определения методов validTrip() и // directTrip()

bool trip_announcement::validTrip(list::iterator I, string TempOrigin)

{

if(I == UniverseOfDiscourse.end()){ if(Candidates.empty()){ TruthValue = false; return(false);

}

else{

trip_announcement Temp; Temp = Candidates.top(); I = find(UniverseOfDiscourse.begin(), UniverseOfDiscourse.end(),Temp); UniverseOfDiscourse.erase(I); Candidates.pop(); I = UniverseOfDiscourse.begin(); if(I != UniverseOfDiscourse.end()){ TempOrigin = Origin;

} else {

TruthValue = false; return(false);

}

}

>

if((*I).origin() == TempOrigin &&

(*I).destination() == Destination){ Candidates.push(*I); TruthValue = true; return(true);

}

if((*I).origin() == TempOrigin){ TempOrigin = (*I).destination(); Candidates.push(*I);

}

I++;

return(validTrip(I,TempOrigin));

bool trip_announcement: :directTrip(void) {

list::iterator I; I = find(UniverseOfDiscourse.begin(),

UniverseOfDiscourse.end(), *this); if(I == UniverseOfDiscourse.end()){

TruthValue = false;

return(false);

}

TruthValue = true; return(true);

В обоих методах validTrip () и directTrip () используется алгоритм find() из стандартной библиотеки С++. UniverseOfDiscourse — это контейнер, который содержит убеждения агента и подготовленные для него утверждения. Вспомните, что одним из первых действий, предпринимаемых агентом, является вызов метода updateBeliefs(), который заполняет контейнер UniverseOfDiscourse. Определение метода updateBeliefs () приведено в листинге 12.8.

// Листинг 12.8. Обновление убеждений

void agent::updateBeliefs(void) {

performance_statement TempP;

TempP.sales(203.0);

TempP.perHour(10 0.0);

TempP.bays(4);

PerformanceBeliefs.push_back(TempP);

trip_announcement Temp;

Temp.origin(«Detroit»);

Temp.destination(«LA»);

TripBeliefs.push_back(Temp);

Temp.origin(«LA»);

Temp.destination(«NJ»);

TripBeliefs.push_back(Temp);

Temp.origin(«NJ»);

Temp.destination(«Windsor»);

TripBeliefs.push_back(Temp);

}

На практике убеждения обычно поступают из среды выполнения агента ( т.е. из файлов, от датчиков, портов, устройств сбора данных и пр.). В листинге 12.8 инфор м ация, поступающая в списки TripBeliefs и PerformanceBeliefs, представляет новые высказывания, которые агент получает о приемлемых маршрутах и эффективности авторемонтной мастерской. Эти высказывания оцениваются относительно директив, выданных агенту. Установкой директив агента зани м ается м етод setGoals (). (Его опреде л ение приведено в л истинге 12.9.)

// Листинг 12.9. Метод установки целей агента

void agent::setGoals(void) {

Managerl.perHour(15.0);

Managerl.bays(8);

Managerl.sales(123.2 3);

Manager2.perHour(2 5.3 4);

Manager2.bays(4);

Manager2.sales(12.33);

Manager3.perHour(3 4.3 4);

Manager3.sales(100000.12);

Manager3.bays(10);

Trip1.origin(«Detroit»);

Tripl.destination(«Chicago»);

Trip2.origin(«Detroit»);

Trip2.destination(«NY»);

Trip3.origin(«Detroit»);

Trip3.destination(«Windsor»);

}

Эти директивы сообщают агенту о том, что его владелец хотел бы отправиться в отпуск из Детройта в Чикаго, из Детройта в Нью-Йорк или из Детройта в Виндзор. Помимо маршрутов, также устанавливаются финансовые цели. Чтобы отпуск состоялся, необходимо достижение одной или нескольких таких целей. После установки целей агент обновляет свои убеждения, и его следующая задача будет определена в зависимости от целей и убеждений при условии возможности планирования отпуска. И тогда вызывается второй компонент методов рассуждений агента: determineVacationAppropriate()

Этот метод передает контейнер UniverseOfDiscourse каждому из объектов предположен и й. После это г о он использует утверждение, выраженное в следую щ ей форме: (А v В v С) ^ (Q v R v S) --> W

Это выражение можно озвучить так: если хотя бы одно из утверждений каждой группы истинно, то элемент W примет значение ИСТИНА. Для наше г о а г ента это означает, что если дости г нута хотя бы одна из целей эффективности бизнеса и существует хотя бы один из приемлемых автобусных м аршрутов, то отпуск м ожно планировать. Определение м етода determineVacationAppropriate () представлено в листинге 12.10.

// Листинг 12.10. Второй метод рассуждений

bool agent::determineVacationAppropriate(void) {

bool TruthValue;

Managerl.universe(PerformanceBeliefs);

Manager2.universe(PerformanceBeliefs);

Manager3.universe(PerformanceBeliefs);

Tripl.universe(TripBeliefs);

Trip2.universe(TripBeliefs);

Trip3.universe(TripBeliefs);

TruthValue = ((Managerl || Manager2 || Manager3) &&

(Tripl || Trip2 || Trip3)); return(TruthValue);

}

Обратите внимание на то, что списки TripBeliefs и PerformanceBeliefs являются аргументами метода universe() объектов Trip и Manager. Именно здесь объекты предположений получают информацию из предметной области (UniverseOfDiscourse). Прежде чем объект класса proposition вызовет оператор operator(), его контейнер UniverseOfDiscourse должен заполниться имеющимися у агента данными. В листинге 12.10 при вычислении выражения

((Managerl || Manager2 || Manager3) && (Tripl || Trip2 || Trip3));

оценивается шесть предположений (посредством выполнения оператора "||"). Оператор " | |" для каждого предположения выполняет оператор operator (), который для определения истинности предположения использует список UniverseOfDiscourse. Слелует иметь в виду, что классы trip_announcement Hperformance_statement наследуют довольно много функций класса proposition. В листингах 12.6 и 12.7 было показано, как определяется оператор operator() для класса trip_announcement, а в листинге12.11 приведено определение оператора operator () для класса performance_statement.

// Листинг 12.11. Класс performance_statement

bool performance_statement::operator()(void) {

bool Satisfactory = false;

list::iterator I;

I = UniverseOfDiscourse.begin();

while(I != UniverseOfDiscourse.end() && !Satisfactory) {

if(((*I).bays() >= Bays) || ((*I).sales() >= Sales)

|| ((*I).perHour() >=PerHour)){ Satisfactory = true;

}

I++;

}

return(Satisfactory);

}

Оператор operator () для каждого класса proposition играет «свою» роль в способности класса агента делать логические выводы. В листинге 12.6 показано, как вызывается оператор operator () при каждом вычислении оператора " || " или "&&" для класса proposition или для одного из его потомков. Именно такое сочетание методов operator (), определенных в proposition-классах, и методов класса agent образует стратегии логического вывода для класса agent. В дополнение к операторам "||" и "&&", определенным в классе proposition, классы trip_announcement и performance_statement содержат свои определения.

friend bool operator||(bool X,trip_announcement &Y); friend bool operator&&(bool X,trip_announcement &Y);

Эти friend -объявления позволяют использовать предположения в более длинных выражениях. Сделаем следующие объявления.

//. . .

trip_announcement А, В, С; bool X;

X = А || В || С;

//.. .

При этом объекты А и В будут объединены с помощью операции ИЛИ, а результат этой операции будет иметь тип bool. Затем мы попробуем с помощью той же операции ИЛИ получить значение типа bool и объект типа trip_announcement: bool || trip_announcement

Без приведенных выше friend -объявлений такая операция была бы недопустимой. Определение этих функций-«друзей» показано влистинге 12.12.

// Листинг 12.12. Перегрузка операторов "||" и "&&"

bool operator||(bool X,trip_announcement &Y) {

return(X | | YO) ;

}

bool operator&&(bool X,trip_announcement &Y) {

return(X && Y());

}

Обратите внимание на то, что в определении этих функций-«друзей» (благодаря ссылке на элемент Y ()) также используется вызов функции operator (). Эти функции определяются и в классе performance_statement. Наша задача — сделать использование proposition-классов таким же простым, как использование встроенных типов данных. В классе proposition также определен другой оператор, который позволяет использовать предположение естественным образом. Рассмотрим следующий код.

//.. .

trip_announcement А; if(A) {

//... Некоторые действия.

}

//.. .

Как в этом случае компилятор тестирует объект А? При выполнении инструкции if () компилятор стремится найти в скобках значение целочисленного типа данных или типа bool. Но тип объекта А совсем другой. Мы хотим, чтобы ко м пилятор восприни м ал объект А как высказывание, которое м ожет быть либо истинны м, либо ложны м. При таких обстоятельствах функция operator () не вызывается. Поэто м у для получения нужного эффекта м ы определяем оператор void*. Эту функцию-оператор можно определить следующим образом.

template proposition::operator void*(void) {

return((void*)(TruthValue));

}

Это определение позволяет предположение любого типа, представленное «в единственном числе», протестировать как значение истинности. Например, когда наш класс agent собирается отправить по электронной почте владельцу фирмы сообщение, содержащее путь следования, агенту нужно определить, какой маршрут отвечает заданным требованиям. В листинге 12.13 представлен еще один фрагмент из методов обработки автобусных маршрутов.

// Листинг 12.13. Метод displayTravelPlan()

void agent::displayTravelPlan(void) {

stack Route;

if(Tripl){

Route = Tripl.candidates();

}

if(Trip2){

Route = Trip2.candidates();

}

if(Trip3){

Route = Trip3.candidates();

}

while(!Route.empty()) {

cout << Route.top().origin() << " TO "« Route.top().destination() « endl; Route.pop();

}

Обратите внимание на то, что объекты Tripl, Trip2 и Trip3 тестируются так, как будто они имеют тип bool. Метод candidates () просто возвращает путь следования, соответствующий заданному маршруту. Таким образом, разработка стратегий логического вывода и когнитивных структур данных становится проще благодаря использованию перегрузки операторов и С++-шаблонов. Именно стратегии логического вывода и когнитивные структуры данных делают объект рациональным. C++-программист для разработки агентов использует конструкцию класса, а для реализации когнитивных сгруктур данных (CDS) — контейнерные объекты в сочетании со встроенными алгоритмами. Класс, который содержит CDS-структуры, становится рациональным, а рациональный класс — агентом.

 

Простая автономность

Поскольку наш простой класс агента не требует выполнения традиционного «цикла активизации», нам нужны другие средства, которые бы периодически активизировали агент без вмешательства человека. Возможны ситуации, когда агент нужно запускать на выполнение лишь иногда или только при определенных условиях. Среды UNIX/Linux оснащены утилитой crontab, которая представляет собой пользовательский интерфейс «хрон-систе м ы» (xpon— это де м он ОС UNIX, исполняющий предписанные команды в соответствии со строго определенными значениями даты и времени, указанными в спе циально м файле с именем crontab). Утилита crontab позволяет организовать периодическое выполнение одной или нескольких программ. Задания для утилиты crontab можно назначать с указанием месяца, дня недели, дня (месяца), часов и минут. Для использования утилиты crontab в нашем случае необходимо создать текстовый файл, который будет содержать график активизации агента. Записи этого файла должны иметь следующий формат:

м инуты часы день м есяц день недели ко м анда

Каждый эле м ент записи м ожет прини м ать следую щ ие значения:

минуты 0-59

часы 0-23

день 1-31

месяц 1-12

день недели 1-7 (1 — понедельник, 7 — воскресенье)

команда может быть любой UNIX/Linux-командой, а также именем файла,

который содержит агенты

Созданный в таком формате текстовый файл передается «хрон-системе» с помощью слелующей команды:

$crontab  NameOfCronFile

Например, предположи м, у нас есть файл activate.agent, содержи м ое которо г о и м еет такой вид.

15   8   *   *   *   agentl

0   21   *   *   6   agent2

*   *   1   12   *   agent3

После выпол н ения crontab-ко м анды $crontab activate.agent

агент agentl будет активизироваться каждый день в 8:15, агент agent2 — каждое воскресенье в 21:00, а агент agent3— каждый раз при наступлении первого декабря. Хрон-файлы можно при необходимости добавлять или удалять. Хрон-файлы могут содержать ссылки на другие хрон-задания, позволяя таким образом агенту «самому» перепланировать свою работу. Так, для обеспечения чрезвычайно гибкой, динамичной и надежной процедуры активизации агентов можно использовать сценарии оболочки в сочетании с утилитой crontab. Чтобы получить полное описание утилиты crontab, обратитесь к оперативным страницам руководства (manpages— г ипертекстовые страницы консультативной инфор м ации, поясняю щ ие действие конкретных ко м анд): $man crontab или $man at

Средства crontab и at представляют собой простейший способ автоматизации или регулярного запуска агентов, который не требует постоянного выполнения циклов активизации. Эти утилиты надежны и гибки. Однако для реализации автоматической активизации агента также можно использовать хранилище, или репозиторий, реализаций и брокер объектных запросов (object request brokers — ORB), который мы рассматривали в главе 8. Стандартные CORBA-реализации также предоставляют средства организации событийных циклов.

12.5. Мультиагентные системы

Мультиагентные системы— это системы, в которых задействовано несколько агентов, обладающих способностью в процессе решения некоторой задачи взаимодействовать, сотрудничать, «договариваться» или соперничать. У С++-разработчика программного обеспечения есть несколько вариантов для реализации мультиагентных систем. Агенты можно реализовать в отдельных потоках выполнения с помощью API-интерфейса POSIX thread. В этом случае одна программа разбивается на несколько потоков, каждый из которых содержит один или несколько агентов. Следовательно, агенты одного потока будут разделять одно и то же адресное пространство. Это позволяет агентам легко взаимодействовать путем использования глобальных переменных и простой передачи параметров. Если компьютер, на котором выполняется программа, содержит несколько процессоров, то агенты могут выполняться параллельно. В этом случае каждый агент должен быть оснащен объектами синхронизации (см. главы 5 и 11) и компонентами обработки исключительных ситуаций (см. главу7). Мультиагентные системы, реализованные посредством многопоточности, представляют самое простое решение, но тем не менее ограничивающее агентов рамками одного компьютера. Более гибкий подход к созданию мультиагентных систем предоставляет CORBA-реализация. Стандарт CORBA (помимо ядра спецификации CORBA) содержит спецификацию мультиагентного средства (multi-agent facility— MAF). MICO-реализацию, которую мы используем в CORBA-примерах этой книги, можно применять для реализации агентов, которые способны взаимодействовать через сети Internet, intranet и локальные сети. С++-привязка CORBA-стандарта имеет полную поддержку объектно-ориентированного представления и, следовательно, поддержку агентно-ориентированного программирования. В главе 13 мы рассмотрим, как можно использовать библиотеки PVM и MPI для поддержки агентов в контексте параллельного и распределенного программирования.

 

12.6. Резюме

Агенты — это рациональные объекты. Агентно-ориентированное программирование — это свежий взгляд на старые проблемы декомпозиции, взаимодействия и синхронизации, которые являются обязательной частью каждого проекта параллельного или распределенного программирования. С++-поддержка перегрузки операторов контейнеров и шаблонов обеспечивает эффективные средства реализации широкого диапазона классов агентов. Будущие системы с массовым параллелизмом и большие распределенные системы будут опираться на агентно-ориентированные реализации поскольку практически не существует других путей построения таких систем. Несмотря на «вводный» характер примеров создания агентов, представленных в этой главе, они вполне обеспечивают основу для понимания практических принципов построения агентных систем. Для развертывания мультиагентных систем можно использовать об щ е д оступные и популярные библиотеки POSIX thread API, MICO, PVM и MPI. Мультиагентные системы можно использовать д ля реализации решений, которые требуют параллельного или распределенного программирования. В этой книге представлены два основных варианта архитектуры для параллельного и распределенного программирования: первый представляют агенты, а второй — «классные доски» (которые предполагают использование агентов). О том, как использовать «классные доски» для реализации решений параллельного и распределенного программирования, мы поговорим в следующей главе.