Параллельное программирование: взаимодействие процессов и нитей
Использование программного канала для передачи данных
между процессами: pipe
Для иллюстрации передачи данных между процессами
рассматривается простейшая игра крестики-нолики,
реализованная в виде оконной программы над X-Window,
тексты проекта в архиве
"XO.zip".
Исходный процесс создает два односторонних
программных канала (канал крестиков и канал ноликов)
с помощью двух вызовов pipe:
int fdX[2], fdO[2];
if (pipe(fdX) < 0) {
perror("Pipe X error"); exit(-1);
}
if (pipe(fdO) < 0) {
perror("Pipe O error"); exit(-1);
}
Затем процесс раздваивается с помощью вызова fork:
int pid = fork();
if (pid < 0) {
perror("Fork error"); exit(-1);
}
Родительский процесс, отвечающий за игру крестиками,
в дальнейшем использует первый канал fdX для чтения,
а второй канал fdO для передачи ходов партнеру по игре.
Соответственно процесс-ребенок, отвечающий за игру
ноликами, использует первый канал fdX для передачи,
а второй канал fdO для чтения ходов. Поэтому процессы
первым делом закрывают неиспользуемые концы каждой трубы,
а номера каналов для чтения и записи запоминаются
в переменных-членах readChannel и writeChannel
класса MyWindow, представляющего
окно программы на экране:
if (pid != 0) { // Parent process
close(fdO[0]);
close(fdX[1]);
w.readChannel = fdX[0];
w.writeChannel = fdO[1];
w.IAmX = true; // I am X
} else { // Child process (I am O)
close(fdO[1]);
close(fdX[0]);
w.readChannel = fdO[0];
w.writeChannel = fdX[1];
}
Затем каждый из процессов создает окно, в котором
отображается поле игры char field[3][3] в крестики-нолики размером 3x3.
При щелчке мыши в родительском процессе в соответствующую
клетку ставится крестик, и информация о сделанном ходе передается
парному процессу. В процессе-ребенке в клетку ставится нолик.
Логическая переменная-член IAmX графического окна
указывает, является ли данный
процесс родительским, т.е. играет ли он крестиками.
// Process mouse click
void MyWindow::onButtonPress(XEvent& event) {
int x = event.xbutton.x;
int y = event.xbutton.y;
int ix = x / DX; // Coordinates of field cell
int iy = y / DY;
int c = 1; // Cross
if (!IAmX)
c = 2; // or Null
field[ix][iy] = c; // Put cross/null into a cell
char line[3];
line[0] = (char) ix;
line[1] = (char) iy;
line[2] = (char) c;
if (!finished) { // Send data to peer process
int res = write(writeChannel, line, 3);
if (res < 0) {
finished = true;
printf("Write: res=%d (connection broken)\n", res);
}
}
redraw();
}
Для приема сделанного хода от парного процесса используется
функция select, которая вызывается в цикле обработки
событий, когда очередь оконных событий пуста. Функция select
реализует псевдо-асинхронный режим ввода-вывода (если данные еще
не пришли, функция завершается по таймауту и, таким образом,
не блокирует работу графической программы). Используемый
таймаут равен 0.05 сек:
// Message loop
XEvent e;
while (GWindow::m_NumCreatedWindows > 0) {
if (GWindow::getNextEvent(e)) {
GWindow::dispatchEvent(e);
} else {
int maxFD = w.readChannel;
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(w.readChannel, &readSet);
fd_set* r = &readSet;
if (w.finished) {
maxFD = 0;
r = NULL;
}
timeval dt; // Timeout
dt.tv_sec = 0;
dt.tv_usec = 50000; // Maximal sleeping time 0.05 sec
int res = select(maxFD + 1, r, 0, 0, &dt);
if (res > 0) {
printf("Select: res=%d\n", res);
w.readMove(); // Read information sent by peer process
}
}
}
Если фунция select возвращает положительное число, то
это означает, что пришли данные от парного процесса, т.е.
информация о сделанном ходе. Эти данные считываются в
методе readMove,
в игровом поле ставится соответствующий знак и окно перерисовывается:
void MyWindow::readMove() {
if (!finished) {
char move[3];
int res = read(readChannel, move, 3);
if (res == 3) {
int x = move[0]; int y = move[1];
int c = move[2];
field[x][y] = (char) c;
redraw();
} else if (res <= 0) { // End of file or read error
finished = true;
printf("Read: res=%d (connection broken).\n", res);
}
}
}
Параллельное программирование: нити
Два простейших примера на создание
и синхронизацию нитей
-
Пример 1: синхронизация с помощью мьютекса.
Создаются две нити, каждая из них
10 раз печатает строку "Thread N", где N -- номер нити,
и затем завершает работу. В перерывах между печатями нить
засыпает на случайное время от одной до трех секунд.
Обе нити используют общий мьютекс для синхронизации доступа
к экрану терминала (чтобы исключить кашу при печати).
Пример 2: синхронизация с помощью семафоров.
Создаются две нити. Первая
вводит строку с клавиатуры терминала и
помещает ее в массив в статической памяти.
После этого вторая нить инвертирует введенную строку и
печатает перевернутую строку на экран.
Эти действия повторяются в бесконечном цикле.
Первая нить сообщает второй о том, что строка введена,
с помощью семафора "inputReady". Вторая нить информирует первую о том,
что она готова принять следующую строку, используя
семафор "invertReady"
Обе программы содержатся в архиве
Threads.zip
Список задач.