Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Многопоточное програмирование.doc
Скачиваний:
3
Добавлен:
14.11.2019
Размер:
332.8 Кб
Скачать

4. Многопоточные программы

Коль скоро с примерами уже покончено -- самое время перейти к программам. Удивительно, но факт: все из них (кроме mtftext, естественно) я ежедневно использую на практике! Т.е. даже этот функционально неполный инструментарий derslib, специально написанный для данной статьи, вполне подходит для создания реально полезных программ!

4.1. Mtftext.Exe: учебный пример

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

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

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

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

  1. Просматриваем все имена заданной директории.

  2. Если найденное имя является директорией, то нужно произвести и ее просмотр.

  3. Если найденное имя является файлом и удовлетворяет маске, то нужно просмотреть содержимое данного файла для поиска подходящих строк.

Операции 2 и 3, очевидно, можно выполнять параллельно операции 1, а сам алгоритм, при этом, будет выглядеть следующим образом:

  1. Помещаем в очередь сообщений первое сообщение типа FindFiles, содержащее имя корневой директории поиска.

  2. С помощью одного или нескольких потоков начинаем обрабатывать очередь, а именно:

  3. Пока очередь не пуста и не находится в "прерванном" состоянии, начинаем извлекать сообщения.

    1. Если извлечено сообщение типа FindFiles -- обрабатываем директорию, порождая сообщения FindFiles и ScanFile.

    2. Если извлечено сообщение типа ScanFile -- обрабатываем файл, выводя найденные строки.

    3. В случае возникновения ошибки -- переводим очередь в "прерванное" состояние.

  4. Проверяем состояние очереди и завершаем работу с соответствующим кодом возврата.

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

/** @file

* Main file of mtftext program.

*/

#include <vector>

#include <stdlib.h>

#include <ders/dir.hpp>

#include <ders/file.hpp>

#include <ders/text_buf.hpp>

#include <ders/thread_pool.hpp>

#include <ders/wldcrd_mtchr.hpp>

#include "msg.hpp"

namespace mtftext { // ::mtftext

Весь исходный код, кроме функции main(), естественно, заключен в namespace, совпадающий с именем программы.

using namespace ders;

struct CmdLineParser {

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

bool isS;

int numThr;

sh_text word;

sh_text mask;

CmdLineParser(mem_pool& mp) : isS(false), numThr(0), word(nt(mp)), mask(

nt(mp)) {}

void parse(int argc, char** argv);

};

struct MainTask : public task {

Главная структура программы, параллельно выполняющая свою функцию proc() с помощью класса ders::thread_pool.

bool exitOnErr;

sh_text srchPatt;

wldcrd_mtchr fileMchr;

MainTask(bool eoe, sh_text sp, sh_text mk) : exitOnErr(eoe),

srchPatt(sp), fileMchr(sp.pool(), mk) {}

virtual void destroy(mem_pool& mp2) { destroy_this(this, mp2); }

Типичная реализация чистой виртуальной функции destroy, косвенно унаследованной от интерфейса ders::destroyable.

virtual void proc(mem_pool& mp, const dq_vec& dqv, void* arg, task_opers&

to);

void doFindFiles(mem_pool& mp, data_queue& dq, const FindFilesMsg& msg);

void doScanFile(mem_pool& mp, data_queue& dq, const ScanFileMsg& msg);

};

void CmdLineParser::parse(int, char** argv)

{

const char* usage="mtftext [-s] num_threads word mask";

Командная строка имеет необязательный параметр -s (stop), предписывающий сразу же завершать работу при обнаружении ошибок. По умолчанию же в stderr записывается сообщение об ошибке и работа продолжается.

mem_pool& mp=word.pool();

char** it=argv;

if (!*++it) throw newExitMsgException(mp, _FLINE_, usage, 1);

Возбуждаем исключение ExitMsgException в случае ошибки, которое используется для выхода из программы с заданным кодом возврата и, возможно, текстом сообщения. Похожего результата можно добиться и с помощью пары fprintf()/exit(), но в этом случае не будут вызваны деструкторы локальных объектов, что, вообще говоря, неприемлемо.

if (*it==ch_rng("-s")) {

isS=true;

if (!*++it) throw newExitMsgException(mp, _FLINE_, usage, 1);

}

numThr=atoi(*it);

if ( !(numThr>=1 && numThr<=100) ) {

throw newExitMsgException(mp, _FLINE_, "num_threads must be in [1, 100]",

1);

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

}

if (!*++it) throw newExitMsgException(mp, _FLINE_, usage, 1);

*word=*it;

if (!*++it) throw newExitMsgException(mp, _FLINE_, usage, 1);

*mask=*it;

if (*++it) throw newExitMsgException(mp, _FLINE_, usage, 1);

}

void MainTask::proc(mem_pool& mp, const dq_vec& dqv, void*, task_opers&)

{

data_queue& dq=*dqv[0];

for (;;) {

Этот объемлющий цикл вокруг цикла обработки сообщений встречается только в программе mtftext, т.к. только в ней у пользователя есть возможность не прерывать работу в случае обнаружения ошибок.

shException exc(mp, 0);

try {

for (MsgIO mio(mp, dq); ; ) {

sh_ptr<Msg> msg=mio.read();

if (!msg.get()) break;

Отсутствие прочитанного сообщения может означать как исчерпание всех сообщений, так и is_intr() состояние очереди.

switch (msg->getType()) {

case Msg::FindFiles: {

doFindFiles(mp, dq, msg->to<FindFilesMsg>());

break;

}

case Msg::ScanFile: {

doScanFile(mp, dq, msg->to<ScanFileMsg>());

break;

}

}

}

return;

}

catch (shException she) { exc=she; }

catch (...) { exc=recatchException(mp, _FLINE_); }

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

file(mp, fd::err).write(text_buf(toTextAll(exc))+'\n');

Используем объект text_buf для удобного объединения строки и символа.

if (exitOnErr) {

dq.set_intr(true);

break;

}

}

}

void MainTask::doFindFiles(mem_pool& mp, data_queue& dq, const FindFilesMsg&

msg)

{

MsgIO mio(mp, dq);

sh_dir shd=new_dir(mp, msg.dirName);

for (dir::entry dent(mp); shd->find_next(dent); ) {

if (dent.name=="." || dent.name=="..")

continue;

sh_text fname=shd->full_name(dent);

if (dent.isdir) {

FindFilesMsg m(fname);

mio.write(m);

continue;

}

if (fileMchr.match(dent.name)) {

ScanFileMsg m(fname);

mio.write(m);

}

}

}

void MainTask::doScanFile(mem_pool& mp, data_queue&, const ScanFileMsg& msg)

{

file out(mp, fd::out);

Создаем файл для вывода, привязанный к дескриптору stdout.

file fin(mp, msg.fileName, file::rdo, 0);

buf_reader br(mp, fin, 64*1024);

sh_text line(nt(mp));

for (int num=1; br.read_line(line); num++) {

if (line->find(srchPatt)!=line->end())

out.write(text_buf(mp)+msg.fileName+':'+num+':'+line+'\n');

}

}

} // namespace ::mtftext

int main(int argc, char** argv)

{

using namespace ders;

using namespace mtftext;

mem_pool mp;

file err(mp, fd::err);

file out(mp, fd::out);

shException exc(mp, 0);

try {

CmdLineParser clp(mp);

clp.parse(argc, argv);

MainTask mt(clp.isS, clp.word, clp.mask);

sh_data_queue dq=new_data_queue(mp);

MsgIO mio(mp, *dq);

FindFilesMsg m(nt(mp, ""));

mio.write(m);

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

sh_thread_pool tp=(clp.numThr>1) ? new_thread_pool(mp, clp.numThr-1) :

new_thread_pool(mp);

Создаем thread_pool с указанным пользователем количеством рабочих потоков: если numThr равен одному, то никаких потоков создавать не требуется и для работы используется специальная однопоточная реализация thread_pool интерфейса.

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

tp->exec(mt, dq_vec(1, dq.get()));

Запускаем одновременное выполнение функции MainTask::proc() всеми потоками thread_pool-а + вызвавшим exec() потоком функции main(). Именно поэтому в new_thread_pool() передается значение numThr-1.

return (dq->is_intr()) ? 2 : 0;

Проверяем состояние очереди для определения причины окончания обработки и передаем ОС соответствующий код возврата.

}

catch (shException she) { exc=she; }

catch (...) { exc=recatchException(mp, _FLINE_); }

ExitMsgException* em=exc->is<ExitMsgException>();

if (em) {

if (em->message->size())

(em->exitCode ? err : out).write(text_buf(em->message)+'\n');

return em->exitCode;

}

Типичный блок обработки исключения ExitMsgException.

err.write(text_buf(toTextAll(exc))+'\n');

return 2;

}