Информационно-образовательный портал СОДРУЖЕСТВА НЕЗАВИСИМЫХ ГОСУДАРСТВ
ИНФОРМАТИЗАЦИЯ ОБРАЗОВАНИЯ
И ДИСТАНЦИОННОЕ ОБУЧЕНИЕ В СНГ
Информационно-образовательный портал СОДРУЖЕСТВА НЕЗАВИСИМЫХ ГОСУДАРСТВ  

Страны
Азербайджанская Республика
Республика Армения
Республика Беларусь
Республика Казахстан
Кыргызская Республика
Республика Молдова
Российская Федерация
Республика Таджикистан
Туркменистан
Республика Узбекистан
Украина

Типы материала
Информационно-коммуникационные технологии
Дополнительные информационные материалы
Нормативно-правовое обеспечение
Организация и методики обучения
Экономика образования
Межгосударственное сотрудничество
Образовательные центры
Методики обучения
Межвузовское сотрудничество
Повышение квалификации
Международные проекты и гранты, конкурсы
Конференции, симпозиумы, семинары и др.
Библиотека
 
Журнал «Вестник РУДН» серия «Информатизация образования»
 
2014, №4
2014, №3
2014, №2
2014, №1
2013, №4
2013, №3
2013, №2
2013, №1
2012, №4
2012, №3
2012, №2
2012, №1
2011, №4
2011, №3
2011, №2
2011, №1
2010, №4
2010, №3
2010, №2
2010, №1
2009, №4
2009, №3
2009, №2
2009, №1
2008, №4
2008, №3
2008, №2
2008, №1
2007, №4
2007, №3
2007, №2-3
2007, №1
2006, №1(3)
2005, №1(2)
2004, №1
Научные и специальные электронные ресурсы
Учебная, научная и специальная литература
Комиссия по дистанционному обучению совета по сотрудничеству в области образования государств-участников СНГ
Новости

Раннее обучение параллельному программированию


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

Текст документа

И.Н. Скопин

 

Кафедра программирования

Новосибирский государственный университет

 

 

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

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

Ограничительность  складывающегося у программистов образа мышления проявляет себя наглядно, когда решаются задачи, предполагающие параллелизм выполнения. Это утверждение подтверждает подход к разработке параллельной программы, когда сначала строится ее последовательная версия, а потом она распараллеливается. Для поддержки такого подхода рынок предлагает специализированные системы поддержки OpenMP [1], MPI [2] и др., средства которых являются надстройками над последовательными языками.

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

Вывод из сказанного выше парадоксальный: нацеленность изучения программирования на развитие мышления на деле приводит к его сужению, и, возможно, единственное полезное, что получают учащиеся, это тренировка способностей приспосабливать себя к объективно существующим ограничениям. Максимально высокий уровень, который обычно в состоянии достичь программист, — комбинаторное мышление [4]. Такое мышление не способствует разработке новых методов. Они появляются не благодаря, а вопреки деятельности программистов, объективно комбинаторной по своей сути. Иными словами, методы разрабатываются за счет остатков естественных для некоторых выдающихся личностей способностей, не вытравленных рутиной программистского труда.

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

Приведем наглядный пример, который автор неоднократно использовал в преподавании для демонстрации параллельного решения классической задачи поиска наилучшего пути[2] между двумя городами А и Б, связанными системой дорог. Выскажем гипотезу, что индивидуум, не обладающий навыками последовательного программирования, скорее всего, предложит алгоритм (пусть даже с вполне типичными ошибками), который можно охарактеризовать как мультиагентное решение [5]. Последовательное, а точнее, квазипараллельное решение было предложено У.-И. Далом и Ч. Хоаром для демонстрации возможностей систем с дискретными событиями языков Simula и Simula 67 в сборнике статей «Структурное программирование» [6]. Мы называем его соревнованием разбредающихся по разным дорогам агентов в скорости достижения цели [4].

Гипотетическое решение непосвященного опирается на понятие запретного города, т.е. такого, в который попадать агенту не разрешается: город считается запретным, если в нем есть или ранее побывал какой-либо агент. Первоначально все города объявляются разрешенными для посещения, и единственный существующий агент находится в городе А, а его пройденный путь пуст.

Решения описывается как поведение каждого агента, находящегося в некотором городе, по следующей схеме, в качестве двух параметров которой задаются местоположение  агента (город) и длина дороги, по которой он перешел в это местоположение из предыдущего города:

1)        запомненный агентом пройденный путь пополняется его текущим местоположением, длина всего пути увеличивается на значение второго параметра;

2)        если агент находится в городе Б, то цель достигнута. В качестве результата выдается пройденный путь

иначе

a)        агент проверяет, является ли город запретным. Если это так, агент ликвидируется (понятно, что информация о системе в целом не теряется — другие агенты продолжают действовать);

b)        город, в котором стоит агент, объявляется запретным;

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

d)        каждый новый агент направляется на выделенную ему дорогу, пройдя которую он оказывается в состоянии (1).

3)        агент ликвидируется.

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

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

Из схемы видно, какие ошибки может допустить несведущий индивидуум, как, исправив ошибки, превратить идею в решение. Применительно к обучению параллелизму отметим существенную и при поверхностном рассмотрении незаметную ошибку схемы, которая связана с одновременностью действий агентов. Речь идет о возможных конфликтах, возникающих, когда два или более агентов должны действовать в одном городе одновременно. Что при этом может происходить, и каким образом ликвидировать конфликтность, знает даже не очень квалифицированный в параллельных вычислениях специалист, и на этом вопросе мы останавливаться не будем (см., например [7]). Какие средства синхронизации покажет преподаватель обучаемым на основе представленного примера — предмет  конкретной методики. Здесь же хочется обсудить другой вопрос: как реализовать предложенное решение на реальном вычислителе.

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

Дал и Хоар дают изящное решение, в котором действия динамически порождаемых и ликвидируемых процессов агентов регулируются так называемым управляющим списком — глобальной структурой данных, организуемой для упорядочивания вычислений специально. Это решение показывает возможность сохранения агентов в структуре реальных вычислений. Оно дает детерминированный порядок действий, выполняемых на единственном процессоре (подробности см. в [6]). Решение строится как динамическое отображение разбредающихся агентов на линейно упорядоченную структуру управляющего списка, напрямую связанного с управлением вычислениями.

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

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

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

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

Представленный пример можно подвергнуть критике, указывая на то, что потоковое программирование не совсем параллельное именно в связи с тем, что оно предполагает неограниченность процессорного ресурса. Это утверждение отвергается по следующей причине. Гипотетическое решение непосвященного строится в условиях игнорирования ресурсных ограничений, а варианты доведения его до программы демонстрируют возможности построения отображения на реальный вычислитель. Эту работу можно и нужно рассматривать как самостоятельную деятельность, отличную он «неограниченного» программирования. Совмещение указанных видов деятельности для человека всегда сложнее последовательного их выполнения. Оно особенно нежелательно в учебном процессе, т.к. резко снижает его эффективность (см. [8, 9]).

Именно поэтому мы провозглашаем раннее обучение параллелизму, рассматривая программирование как двухэтапную схему:

·      сначала выполняется ничем не ограниченная разработка алгоритма. Степень близости полученного результата к реальной программе может быть различна, и чтобы не отвлекаться на обсуждение того, каким должен быть алгоритм без ограничений, в дальнейшем будем называть его эскизом (решения, программы или алгоритма);

·      затем строится отображение эскиза на реальный вычислитель, т.е. учет ограничений. Если иметь ввиду какой-либо критерий качества, то второй этап можно считать оптимизационным, и об этом нужно говорить при преподавании программирования.

Важный аспект такого подхода — снятие ограничений на объем выделяемой памяти. Здесь обычная практика преподавания явно или неявно следует положению о разделении деятельностей (по-видимому, по той причине, что так проще рассказывать, а также потому, что последовательные языки не препятствуют разделению). В качестве задачи, часто используемой для проверки того, как начинающие владеют комбинационными методами, укажем на пример Гриса, в котором требуется переставить местами две последовательные части массива [10]. Дополнительное условие, требующее минимизации операций чтения и записи элементов, рассматривается лишь в качестве некоторой меры качества, а не как ограниченность времени работы процессора — этого для учебной задачи достаточно. Неограниченность памяти приводит к тривиальному решению: к переписи на новое место. Именно так поступают начинающие. А критика его есть ничто иное, как предлож??ние построить отображение тривиального эскизного решения на вычислитель с дефицитом памяти.

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

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

aii = (aii + ai-1i + ai-1i-1 + aii-1) / 4                                                                                    (1)

Эта постановка задачи с самого начала ориентирует на строковое и столбцовое структурирование матрицы, поскольку апеллирует к индексам. Как следствие, программист, не очень задумываясь над постановкой, сразу же готов к реализации последовательного циклического алгоритма (по-видимому, он достаточно быстро сообразит, что нужно организовывать цикл от больших значений i к меньшим). Такое решение приводит к тому, что для реализации его на многопроцессорных архитектурах требуется специальный анализ, с помощью которого выясняется, что фактически итерации цикла не зависят друг от друга.

В то же время, данная задача очень естественно формулируется в рамках другого структурирования данных, непосредственно следующего определению трехдиагональной матрицы. В такой матрице (будем считать, что ее размерность ´ n) информативными являются три вектора:

·          главная диагональ — вектор, длина которого равна n;

·          диагональ, расположенная над главной, — вектор, длина которого равна n-1;

·          диагональ, расположенная под главной, — вектор, длина которого равна n-1.

Для краткости обозначим их, соответственно, как A, A+ и A-.

Если определить операции конкатенации векторов <вектор> · <вектор> и отбрасывания последней (первой) компоненты <вектор>¢ (¢<вектор>),[3] а также покомпонентные арифметические операции (ниже они обозначаются с подчеркиванием), то требуемое преобразование описывается как

A := [A1] · (( A+ + A- + A¢ + ¢A ) / 4),                                                                    (2)

где [<значение>]— вектор, состоящий из единственной компоненты (в данном случае используется значение A1 первой компоненты вектора A).

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

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

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

Формулировка рассмотренной задачи с использованием соотношения (1) каже??ся более понятной, чем (2). С этим придется согласиться, но только по той причине, что мы привыкли к индексной нотации aij, а оперирование составными структурными единицами считаем, если не экзотикой, то лишь специальным способом записи. Причина тому — учебные задачи, которые приходилось решать при изучении линейной алгебры, а все они, связанные с вычислениями (безразлично ручными или машинными), приводят к индексированию элементов.

Если же обратиться к истокам дисциплины, то, например, задача решения системы уравнений естественно формулируется в векторной форме:

Найти такой вектор X, что A X = B, где  A матрица, а Bвектор.

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

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

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

По своей сути это урезанное отображение:

·      «бесконечное» тотальное дерево не может быть сохранено, значит, вместо этого нужно локальное построение фрагментов, фактически используемых при выборе (эта подзадача естественно параллельна);

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

Реализация представленной идеи оказывается очень выразительной при использовании средств функционального программирования, которое базируются на концепции ленивых вычислений, позволяющих задавать функции оперирования бесконечными структурами. Ленивость функции проявляется в том, что она никогда не исполняется «до конца», а выдает результаты порциями, определяемыми потребностью другой функции, вызывающей данную. В статье Дж. Хьюза [12] приведены подробности оперирования такими функциями, в частности, в применении к игровым программам, где функция построения тотального дерева «склеивается» с функцией выбора хода. Функциональное структурирование упрощает отображение эскизной программы в части оперирования бесконечной структурой, отделяя его от выбора хода. Для программы выбора хода построение отображения на ограниченные ресурсы вычислителя строится независимо от оперирования тотальным деревом, но с учетом того, что выбор нужных фрагментов дерева обеспечен.

***

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

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

ЛИТЕРАТУРА

[1] The OpenMP® API specification for parallel programming. URL: http://openmp.org/wp/ 

[2] Message Passing Interface Forum. URL: http://www.mpi-forum.org/

[3] Backus J. Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs // Communications of the ACM. – 1978. – V. 21. – № 8. – P. 613– 641.

[4] Непейвода Н.Н. Скопин И.Н. Основания программирования. – М.-Ижевск: Институт компьютерных исследований, 2003. – 868 с.

[5] Wooldridge M.J. An Introduction to MultiAgent Systems. – University Of Liverpool, 2009. – 484 p.

[6] Дал У.-И., Дейкстра Э., Хоор К. Структурное программирование. – М.: Мир, 1975. – 246 с.

[7] Хоар Ч. Взаимодействующие последовательные процессы. – М.: Мир, 1989. – 264 с.

[8] Скопин И.Н. Ролевые игры в мет??дике обучения руководству проектной деятельностью // Наука и образование. – 2010. – № 1 (57). – С. 74–77.

[9] Гальперин П.Я. Четыре лекции по психологии. – М.: Юрайт. 2000. – 112 с.

[10] Грис Д. Наука программирования. – М.: Мир. 1984. – 416 с.

[11] Скопин И.Н. Множественное структурирование данных // Программирование. – 2006. – Т.32. – № 1. –  С. 54–77.

[12] Hughes J. Why Functional Programming Matters // The Computer Journal. – 1989. – 32(2). – P. 98–107.



[1] Ситуация похожа на то, когда программист не учитывает, что из-за округлений арифметические операции вычислителя не приводят к результатам, соответствующим хорошо изученным алгебраическим структурам. Здесь фундаментальные знания вступают в противоречие с тем, что временами случается при численных расчетах.

[2] Наилучший путь можно определять по-разному. Корректно считать, что это любая интегральная характеристика пути из А в Б, складывающаяся из локальных характеристик дорог. В частности, в качестве такой характеристики можно выбрать длину пути. Если предположить, что скорость перемещения для всех дорог одинакова, то этот критерий эквивалентен времени, за которое можно пройти весь путь. Представленная ниже схема не зависит от выбора критерия. Для определенности далее говорится о минимизации длины пути.

[3] Эти операции использованы для выравнивания векторов с разным количеством компонент, которое требуется для покомпонентных действий. Быть может, правильнее было бы определить непосредственно операции выравнивания, но эта задача находится за рамками рассмотрения настоящей работы.


Автор оригинала: И.Н. Скопин
Источник оригинала: Журнал Вестник РУДН серия «Информатизация образования», №4, 2011

Новости
16.06.2017

Российский университет дружбы народов объявляет о проведение первой волны вступительных испытаний среди иностранных граждан для обучения на программах магистратуры на контрактной основе. Первая ...

13.10.2016

26 октября-27 октября 2016 года Российский университет дружбы народов проводит Международную конференцию «Сетевые университеты и международный рынок труда (пространства БРИКС, СНГ, ШОС)».

19.05.2016

The Peoples’ Friendship University of Russia (PFUR) announces the beginning of admission of foreign citizens who graduated from Bachelor and Specialist Degree programs of PFUR and other Russian and ...

19.05.2016

Российский университет дружбы народов (РУДН) объявляет о наборе иностранных граждан -выпускников бакалавриата и специалитета РУДН и других российских и зарубежных ВУЗов на программы магистратуры на ...

11.12.2015

Проект рекомендаций Семинара-совещания научной общественности по проблемам международного научно-технического и образовательного сотрудничества