Олег Цилюрик - QNX/UNIX: Анатомия параллелизма
• У захваченного мьютекса всегда есть поток-владелец, и только он может освободить его в дальнейшем. Именно поэтому мьютекс может использоваться для синхронизации потоков, но только синхронизации в смысле разграничения временной последовательности доступа к фрагменту кода — к тому, что часто называют критической секцией кода. Функциональность семафора значительно выше: при возможности (почти всегда) его применения в том контексте, в котором используется и мьютекс (только нужно ли это делать?), он может применяться и для синхронизации потоков в смысле координации последовательности их взаимодействия в качестве элемента, управляющего порядком выполнения. Покажем это на примере. Для этого незначительно трансформируем код предыдущего теста для семафора (файл sy21.cc):
Синхронизация потоков семафорами#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <iostream.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <semaphore.h>
unsigned long N = 1000;
unsigned int T = 2;
static sem_t* sem;
static bool debug = false;
static char* str; // строка диагностики
static volatile int ind = 0;
uint64_t *t;
void* threadfunc(void* data) {
unsigned long i = 0;
char tid[8];
sprintf(tid, "%X", pthread_self());
// временная метка начала во всех потоках устанавливается
// на время достижения этой точки в последнем (активном) потоке
if ((int)data == T - 1) {
uint64_t с = ClockCycles();
for (int i = 0; i < T; i++ ) t[i] = c;
}
// рабочий цикл переключений за счет синхронизации
while (i++ < N) {
sem_wait(sem + (int)data);
if (debug) str[ind++] = *tid;
sem_post(sem + ((int)data +1) % T);
}
t[(int)data] = ClockCycles() - t[(int)data];
return NULL;
}
int main(int argc, char *argv[]) {
int opt, val;
while ((opt = getopt(argc, argv, "n:t:v")) != -1) {
switch(opt) {
case 'n':
if (sscanf(optarg, "%i", &val) != 1)
cout << "parse command line error" << endl, exit(EXIT_FAILURE);
if (val > 0) N — val;
break;
case 't':
if (sscanf(optarg, "%i", &val) != 1)
cout << "parse command line error" << endl, exit(EXIT_FAILURE);
if (val > 0) T = val;
break;
case 'v':
debug = true;
break;
default:
exit(EXIT_FAILURE);
}
}
if (debug) str = new char[T * N + 1];
pthread_t* tid = new pthread_t[T];
sem = new sem_t[T];
t = new uint64_t[T];
for (int i = 0; i < T; i++) {
// все потоки, кроме последнего, будут заблокированы
// на своих семафорах сразу же после старта
if (sem_init(sem + i, 0, (i == (T - 1)) ? 1 : 0))
perror("semaphore init"), exit(EXIT_FAILURE);
if (pthread_create(tid + i, NULL, threadfunc, (void*)i ! = EOK)
perror( "thread create error"), exit(EXIT_FAILURE);
}
for (int i=0; i < T; i++)
pthread_join(tid[i], NULL);
for (int i = 0; i < T; i++) sem_destroy(sem + i);
delete [] sem;
for (int i = 0; i < T; i++)
cout << tid[i] << "t: cycles - " << t[i] << ";ton semaphore - " <<
t[i] / T / N << endl;
delete [] tid;
delete [] t;
if (debug) {
str[ind] = " "; cout << str << endl;
delete [] str;
}
exit(EXIT_SUCCESS);
}
Логически приложение изменилось следующим образом:
• Теперь у нас может быть не 2 идентичных (симметричных) потока, а произвольное их количество (ключ -t при запуске приложения).
• Потоки синхронизируются не на одном семафоре — введен массив семафоров по числу потоков: каждый поток блокируется на «своем» семафоре, но разблокирует его (после очередного выполнения своего фрагмента) семафор заблокированного «соседа».
• Теперь нам нет нужды использовать барьер для одновременного старта всех созданных потоков: семафоры всех создаваемых потоков инициализируются нулевым значением; стартующий поток тут же блокируется на своем семафоре, и только последний из запущенных выполняется, не блокируясь на семафоре.
• Из кода исключены какие бы то ни было средства принудительной передачи управления (sched_yield()) — все управление логикой ветвления осуществляется только состояниями семафоров.
Посмотрим, что у нас получилось. Запускаем приложение с диагностическим выводом идентификаторов потоков (ключ -v; он у нас был в тестах и ранее, только мы о нем не упоминали):
# nice -n-19 sy21 -n20 -t12 -v
2 : cycles - 664874; on semaphore - 2770
3 : cycles - 649150; on semaphore - 2704
4 : cycles - 638906, on semaphore - 2662
5 : cycles - 622987; on semaphore - 2595
6 : cycles - 611781; on semaphore - 2549
7 : cycles - 594515; on semaphore - 2477
8 : cycles - 571003; on semaphore - 2379
9 : cycles - 552834; on semaphore - 2303
10 : cycles - 536817; on semaphore - 2236
11 : cycles - 519357; on semaphore - 2163
12 : cycles - 500388; on semaphore - 2084
13 : cycles - 296633; on semaphore - 1235
D23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABCD23456789ABC
В строке диагностики (внизу) хорошо видно регулярное чередование выполняющихся потоков, причем оно начинается с индекса последнего созданного потока (13 в показанном примере). Теперь проделаем то же самое, но на большой выборке и с отключенной диагностикой, чтобы получить «чистое» время контекстных переключений потоков, не искаженное затратами на операции формирования диагностики (результаты для нескольких различных размерностей задачи при разном количестве потоков):
# nice -n-19 sy21 -n100000 -t12
2 : cycles - 1509597589; on semaphore - 1257
3 : cycles - 1509581545; on semaphore - 1257
4 : cycles - 1509570283; on semaphore - 1257
5 : cycles - 1509552472; on semaphore - 1257
6 : cycles - 1509537934; on semaphore - 1257
7 : cycles - 1509519299; on semaphore - 1257
8 : cycles - 1509502312; on semaphore - 1257
9 : cycles - 1509482667; on semaphore - 1257
10 : cycles - 1509466343; on semaphore - 1257
11 : cycles - 1509449264; on semaphore - 1257
12 : cycles - 1509431112; on semaphore - 1257
13 : cycles - 1509222808, on semaphore - 1257
# nice -n-19 sy21 -n100000 -t7
2 : cycles - 859768389; on semaphore - 1228
3 : cycles - 859756956; on semaphore - 1228
4 : cycles - 859745649; on semaphore - 1228
5 : cycles - 859736698; on semaphore - 1228
6 : cycles - 859724685; on semaphore - 1228
7 : cycles - 859707720; on semaphore - 1228
8 : cycles - 859554045; on semaphore — 1227
# nice -n-19 sy21 -n50000 -t13
2 : cycles - 832789852; on semaphore - 1281
3 : cycles - 832813231; on semaphore - 1281
4 : cycles - 832835011; on semaphore - 1281
5 : cycles - 832851360; on semaphore - 1281
6 : cycles - 832868482; on semaphore - 1281
7 : cycles - 832884308; on semaphore - 1281
8 : cycles - 832900935; on semaphore - 1281
9 : cycles - 832916093; on semaphore - 1281
10 : cycles - 832931944; on semaphore - 1281
11 : cycles - 832946479; on semaphore - 1281
12 : cycles - 832962202; on semaphore - 1281
13 : cycles - 832976433; on semaphore - 1281
14 : cycles - 832782465; on semaphore - 1281
# nice -n-19 sy21 -n50000 -t17
2 : cycles - 1142879872; on semaphore - 1344
3 : cycles - 1142906138; on semaphore - 1344
4 : cycles - 1142927650; on semaphore - 1344
5 : cycles - 1142943675; on semaphore - 1344
6 : cycles - 1142959582; on semaphore - 1344
7 : cycles - 1142974919; on semaphore - 1344
8 : cycles - 1142991068; on semaphore - 1344
9 : cycles - 1143005896; on semaphore - 1344
10 : cycles - 1143021518, on semaphore - 1344
11 : cycles - 1143036136; on semaphore - 1344
12 : cycles - 1143053448; on semaphore - 1344
13 : cycles - 1143068415; on semaphore - 1344
14 : cycles - 1143083676; on semaphore - 1344
15 : cycles - 1143098361; on semaphore - 1344
16 : cycles - 1143114009; on semaphore - 1344
17 : cycles - 1143128525; on semaphore - 1344
18 : cycles - 1142872665; on semaphore - 1344
Есть некоторая корреляция времени переключения контекста с размером выборки и количеством обрабатывающих потоков, но она в широком диапазоне этих параметров не превышает 8%. В данном приложении эта численная величина включает в себя: блокирование на семафоре, переключение на контекст другого потока и разблокирование семафора. Если вспомнить, что раньше мы получали оценки для принудительного (посредством sched_yield()) переключения контекста потоков в 375 процессорных циклов, а для захвата-освобождения семафора — порядка 870, то эти цифры хорошо согласуются с полученными сейчас результатами.
Рассматриваемые примитивы служат принципиально различным целям. Мьютекс, как уже было сказано ранее, предназначен в первую очередь для регламентации доступа к участкам программного кода. Семафоры же больше предназначены для регламентации порядка доступа к определенным объектам данных. Классическими задачами этого класса являются задачи «производитель-потребитель», когда M производителей создают некоторые объекты данных (читая эти данные с реальных внешних устройств, или создавая их как результат только внутренних вычислений, или любым другим способом), а N потребителей независимо берут произведенные объекты данных на последующую обработку.