Распределенные вычисления

Часть 6. Приложения BOINC

Узнайте как управлять проектом BOINC удаленно через специальный интерфейс администрирования

Comments

Серия контента:

Этот контент является частью # из серии # статей: Распределенные вычисления

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Распределенные вычисления

Следите за выходом новых статей этой серии.

1. Введение

Перед вами новая — шестая — статья серии, посвященной изучению платформы для организации распределенных вычислений BOINC. Сегодня мы начинаем новую тему — создание приложений, способных использовать всю мощь volunteer computing. Если вы не знаете что такое BOINC, тогда прочтите первую статью серии (п. 1 раздела Ссылки). Разрабатывая приложения, необходимо ясно представлять себе архитектуру платформы BOINC, основные компоненты серверной части, последовательность обработки задания службами и многое другое. Основные сведения по этому вопросу вы найдете во второй статье (п. 2 раздела Ссылки). В других статьях серии рассказывалось о том, как установить, настроить и сопровождать свой собственный сервер распределенных вычислений на базе платформы BOINC (пп. 3-5 раздела ссылки).

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

2. Простейший пример распределенного приложения

Первый пример, который мы будем рассматривать можно найти среди исходных кодов Boinc в каталоге samples/example_app. Этот пример представляет собой простейшее однопоточное приложение BOINC. Программа выполняет простую работу (например, проверку), которая может быть завершена либо успешно, либо неудачно. Пример можно использовать как основу для собственных приложений — нужно лишь заменить реализацией собственного алгоритма ту часть, которая относится к вычислениям.

Исходный код примера разбит на заголовочный файл, два файла с кодом и Makefile для сборки проекта (не считаем файлы, содержащие суффикс «mac»):

ls ~/boinc/samples/example_app
Makefile 
uc2.cpp  
uc2_graphics.cpp  
uc2.h

Далее мы подробнее изучим исходный текст программы-примера. Однако лучше иметь перед глазами оригинальный исходный файл — отдельные фрагменты и незначимые для целей изучения BOINC блоки кода мы будем пропускать или рассматривать в другом порядке. Среди всего прочего будут пропущены те части, которые связаны с взаимодействием процессов в рамках одного компьютера и весь вывод графики. Ни в том, ни в другом нет особенностей, связанных с BOINC. В разделе 2.3 дана таблица функций API BOINC, используемых в программе. К этой таблице можно обращаться по мере появления в исходном коде соответствующих функций (в тексте статьи они будут выделены курсивом).

2.1 Программа-пример UpperCase

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

  • run_slow: приложение «засыпает» на 1 секунду после каждого обработанного символа;
  • cpu_time N: по завершении обработки программа дополнительно использует N секунд времени процессора;
  • early_exit: принудительное завершение программы после обработки 30 символов;
  • early_crash: аварийное завершение программы после обработки 30 символов;
  • early_sleep: программа «засыпает» после обработки 30 символов.

Естественно, исходный код программы начинается с подключения заголовочных файлов:

#ifdef _WIN32
#include "boinc_win.h" // этот заголовочный файл содержит определения констант 
                       // для компиляции под ОС семейства Windows
#else

#include "config.h"    // здесь содержатся различные настройки 
                       // для платформы GNU/Linux… <пропустим подключение стандартных библиотек С++>
#endif

Затем подключаются различные заголовочные файлы платформы, содержащие определения функций API BOINC.

#include "str_util.h"
#include "util.h"
#include "filesys.h"
#include "boinc_api.h"
#include "mfile.h"
#include "graphics2.h"

Все указанные заголовочные файлы (за исключением стандартных для C++) в каталоге исходных кодов BOINC находятся в подкаталогах lib и api. Там же можно найти множество определений (и реализаций) других функций API BOINC.

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

#define CHECKPOINT_FILE "upper_case_state"
#define INPUT_FILENAME "in"
#define OUTPUT_FILENAME "out"

Теперь перейдем к основной функции приложения.

int main(int argc, char **argv) {
    int i;
    int c, nchars = 0, retval, n;
    double fsize, fd;
    char input_path[512], output_path[512], chkpt_path[512];
    MFILE out;
    FILE* state, *infile;

	  <пропустим разбор параметров командной строки>

В качестве подготовительного этапа необходимо инициализировать подсистему BOINC. В нашем случае проводится инициализация однопоточной программы:

	retval = boinc_init();
    if (retval) {
        fprintf(stderr, "%s boinc_init returned %d\n",
            boinc_msg_prefix(), retval);
        exit(retval);
    }

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

Для чтения открывается входной файл. Функция boinc_resolve_filename используется, чтобы определить физический путь к файлу, заданному логическим именем INPUT_FILENAME:

boinc_resolve_filename(INPUT_FILENAME, input_path, sizeof(input_path));

Затем открывается для чтения файл, располагающийся по выясненному раннее физическому пути:

    infile = boinc_fopen(input_path, "r");
    if (!infile) {
        fprintf(stderr,
            "%s Couldn't find input file, resolved name %s.\n",
            boinc_msg_prefix(), input_path
        );
        exit(-1);
    }

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

boinc_resolve_filename(OUTPUT_FILENAME, output_path, sizeof(output_path));
    boinc_resolve_filename(CHECKPOINT_FILE, chkpt_path, sizeof(chkpt_path));
    state = boinc_fopen(chkpt_path, "r");

Необходимость задействования вспомогательного файла определяется самой сутью «volunteer computing». Программа (клиент BOINC) могла быть завершена до окончания расчета задания. Однако было бы неразумным в этом случае начинать всю работу заново (особенно, если для завершения требуется несколько часов непрерывных вычислений). Для восстановления текущего состояния используется вспомогательный файл:

    if (state) {

Читаем из вспомогательного файла число уже обработанных символов:

        n = fscanf(state, "%d", &nchars);
        fclose(state);
    }
    if (state && n==1) {

Пропускаем во входном файле все обработанные символы:

        fseek(infile, nchars, SEEK_SET);

«Обрезаем» выходной файл по количеству обработанных символов (в противном случае при аварийном завершении работы — например, при выключении питания или «зависания» операционной системы — в выходном файле может оказаться еще непосчитанный обработанный символ, что приведет к получению неправильного результата!):

boinc_truncate(output_path, nchars);

Для работы с выходным файлом разработчики посчитали более удобным использовать класс MFILE (которому принадлежит переменная out) — он определен в файле lib/mfile.h.

        retval = out.open(output_path, "ab");
    } else {
        retval = out.open(output_path, "wb");
    }
    if (retval) {
        fprintf(stderr, "%s APP: upper_case output open failed:\n",
            boinc_msg_prefix()
        );
        fprintf(stderr, "%s resolved name %s, retval %d\n",
            boinc_msg_prefix(), output_path, retval
        );
        perror("open");
        exit(1);
    }

Наконец, когда выполнена вся подготовительная работа, настало время основного цикла программы:

      for (int i=0; ; i++) {
        c = fgetc(infile);

        if (c == EOF) break;
        c = toupper(c);
        out._putchar(c);
        nchars++;

Если в командной строке был указан какой-либо из параметров (см. выше), то программа модифицирует свое поведение:

        if (run_slow) {
            boinc_sleep(1.);
        }

        if (early_exit && i>30) {
            exit(-10);
        }

        if (early_crash && i>30) {
            boinc_crash();
        }
        if (early_sleep && i>30) {
            g_sleep = true;
            while (1) boinc_sleep(1);
        }

Сохранение текущего состояния выполняется с помощью периодического вызова функции API BOINC boinc_time_to_checkpoint(). Эта функция, сверяясь с пользовательскими настройками клиента BOINC, определяет настало ли время для фиксации промежуточных результатов работы (тогда в нашей программе вызывается функция do_checkpoint).

        if (boinc_time_to_checkpoint()) {

Функция do_checkpoint записывает во вспомогательный файл количество обработанных символов.

            retval = do_checkpoint(out, nchars);
            if (retval) {
                fprintf(stderr, "%s APP: upper_case checkpoint failed %d\n",
                    boinc_msg_prefix(), retval
                );
                exit(retval);
            }

Клиент информируется о завершении сохранения состояния (промежуточных результатов) с помощью вызова функции boinc_checkpoint_completed():

boinc_checkpoint_completed();
        }

Теперь отчитываемся о прогрессе выполнения задания — переданное клиенту значение будет отображаться как процент выполнения работы.

        fd = nchars/fsize;
        if (cpu_time) fd /= 2;
        boinc_fraction_done(fd);
    }

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

    if (cpu_time) {
        double start = dtime();
        for (int i=0; ; i++) {
            double e = dtime()-start;
            if (e > cpu_time) break;
            fd = .5 + .5*(e/cpu_time);
            boinc_fraction_done(fd);

            if (boinc_time_to_checkpoint()) {
                retval = do_checkpoint(out, nchars);
                if (retval) {
                    fprintf(stderr, "%s APP: upper_case checkpoint failed %d\n",
                        boinc_msg_prefix(), retval
                    );
                    exit(1);
                }
                boinc_checkpoint_completed();
            }

Функция do_a_giga_flop выполняет триллион операций с плавающей запятой (Gflop).

            comp_result = do_a_giga_flop(i);
        }
    }

Задание выполнено на 100%

boinc_fraction_done(1);

Завершаем работу приложения BOINC

boinc_finish(0);
}

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

2.2 Структура программы BOINC

Теперь вернемся к структуре стандартного приложения. Первая функция API BOINC, которая может быть вызвана в программе, - это функция инициализации boinc_init(). Соответственно, завершиться приложение должно функцией boinc_finish(). В процессе работы необходимо обновлять информацию о степени выполнения задания (функция boinc_fraction_done()) и сохранять результаты промежуточных вычислений (связка boinc_time_to_checkpoint() и boinc_checkpoint_completed()).

boinc_init()
<основной цикл вычислений>
   ...
   boinc_fraction_done(x)
   if boinc_time_to_checkpoint()
      write checkpoint file
      boinc_checkpoint_completed()
boinc_finish(0)

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

2.3 Функции API BOINC

В этом разделе представлен список функций API BOINC, встречавшихся в программе-примере.

Таблица 1. Список используемых функций API BOINC
Определение функцииНазначение
1int boinc_init()Инициализация однопоточной программы; первая функция boinc, которая может быть вызвана.
2boinc_msg_prefix()Функция возвращает дату и идентификатор процесса. Этот префикс должен сопровождать все сообщения, записываемые приложением на стандартный вывод.
3int boinc_resolve_filename(char *logical_name, char *physical_name, int len)

или

int boinc_resolve_filename_s(char *logical_name, std::string& physical_name);
Назначение функции — преобразование логического имени файла в физическое. Например, для открытия файла my_file на чтение вместо
f = fopen("my_file", "r");

приложение должно иметь примерно такой код:

string resolved_name;
retval = boinc_resolve_filename_s("my_file", resolved_name);
if (retval) fail("can't resolve filename");
f = fopen(resolved_name.c_str(), "r");

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

4boinc_fopen(char* path, char* mode);Вместо fopen() рекомендуется все-таки использовать функцию boinc_fopen(char* path, char* mode);

Это связано со специфическими проблемами платформ: например, в Windows программы безопасности или индексации могут кратковременно заблокировать файл. boinc_fopen() выполняет несколько попыток открытия файла с односекундными интервалами; в Unix-системах сигнал может получить ошибку EINTR при использовании fopen()— boinc_fopen проверяет на наличие такой ошибки и повторяет попытку. Также boinc_fopen устанавливает флаг 'close-on-exec'.

5int boinc_truncate (const char * path, double size) «Обрезает» файл, делая его размер равным size. Отличие от обычной функции truncate заключается лишь в отдельной обработке при работе в ОС Windows.
6boinc_graphics_make_shmem()Функция, которая вызывается из основного приложения, создает сегмент разделяемой памяти указанного размера. Если сегмент не может быть выделен (например, из-за нехватки памяти), то приложение завершится ошибкой.
7typedef void (*FUNC_PTR)(); void boinc_register_timer_callback(FUNC_PTR); Регистрируется функция, которая вызывается каждую секунду по сигналу таймера.
8void boinc_sleep(double time)Бездействие программы в течение time секунд
9void boinc_crash ( ) Функция вызывает аварийный останов программы; интересен также метод, которым это делается (см. файл lib/util.cpp):
void boinc_crash() {
#ifdef _WIN32
	DebugBreak();
#else
	*(int*)0 = 0;
#endif
}
10boinc_fraction_done(double fraction_done);Клиент BOINC отображает процент выполнения задания. Эта информация передается клиенту приложением с помощью функции boinc_fraction_done(double fraction_done), где fraction_done — это число от 0 до 1. Функция выполняется быстро и может вызываться часто.
11int boinc_time_to_checkpoint();При проведении больших вычислений приложение BOINC может периодически сохранять текущее состояние выполнения работы. При этом используемый файл состояния должен содержать всю информацию, необходимую для продолжения выполнения задания.

Удобная пользователю частота сохранения состояния указывается в настройках клиента BOINC. Функция boinc_time_to_checkpoint() вызывается для того, чтобы определить подошло ли время сохранения состояния. В паре с этой функцией должна быть вызвана функция boinc_checkpoint_completed() - она сообщает клиенту о завершении сохранения состояния.

Функция boinc_time_to_checkpoint() выполняется быстро, поэтому может вызываться часто (сотни или тысячи раз в секунду).

12void boinc_checkpoint_completed()boinc_checkpoint_completed() - это парная функция к boinc_time_to_checkpoint(), ее назначение — уведомление клиента BOINC о завершении сохранения состояния программы.
13int boinc_finish(int status)Функция должна быть вызвана по завершении программы. Ненулевой статус говорит об ошибке.

3. Заключение

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

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=776816
ArticleTitle=Распределенные вычисления: Часть 6. Приложения BOINC
publish-date=11242011