Многозадачность и синхронизация в Plan 9: Часть 2
В этой статье речь пойдет о библиотеке thread(2) которая расширяет возможности по организации многозадачных программ в Plan 9. Библиотека предоставляет набор функций для создания процессов и потоков внутри них. Процесс ни что иное, как системный процесс Plan 9, создается посредством rfork. Нить - процедура, которая выполняется в контексте своего процесса, но с отдельным стеком, нитей в процессе может быть несколько. Нить исполняется сколь угодно долго, в контексте своего процесса, пока явно не передаст управление другой нити. Так например вызов sleep(1000) в одной из нитей процесса, не означает что остальные нити получат управление - весь процесс погрузится в "сон" на 1 секунду.
Следует заметить, что использование QLock возможно благодаря тому, что libc которая предоставляет этот примитив, имеет поддержку для libthread. Файл /sys/src/libc/9sys/qlock.c содержит указатель на функцию _rendezvousp которая по умолчанию указывает на rendezvous, а при использовании библиотеки libthread переустанавливается на собственный вариант, так как взаимодействие может происходить как между нитями разных процессов (что поддерживает и стандартный rendezvous) так и между нитями в одном процессе.
Чтобы понять, как работает библиотека можно «поиграть» со следующим примером:
01 #include <u.h>
02 #include <libc.h>
03 #include <thread.h>
04
05 void thread(void* data)
06 {
07 int a,i;
08
09 a=(int)data;
10
11 for(i=0;i<15;i++)
12 {
13 print("%d\n",a);
14 // if (!(i % 2))
15 // yield();
16 }
17
18 threadexits(0);
19 }
20
21 void threadmain(int ,char** )
22 {
23 int i;
24
25 threadcreate(thread,(void*)1,1024);
26 threadcreate(thread,(void*)2,1024);
27 //proccreate(thread,(void*)3,1024);
28
29 //yield();
30
31 for (i=0;i<10;i++)
32 print("main\n");
33
34 threadexits(0);
35 //threadexitsall(0);
36 }
Исполнение программы начинается со строки 21, процедура threadmain, которая вызывается после инициализации библиотеки libthread. В строках 25 и 26 создаются две нити, для них выделяется стек (посредством malloc), заполняются необходимые структуры, они помечаются как готовые к выполнению и ставятся в очередь (FIFO). Далее выполнение продолжается со строки 31, экран заполняется 10 строчками «main», и посредством threadexit, строка 34 текущая нить завершается, далее из очереди изымается следующая «готовая к исполнению» нить и управление передается ей, экран заполняется 10 строчками «1», после чего нить так же завершается. Таким же образом управление передается последней нити, и печатаются строки «2». Когда и эта нить завершается, и в очереди больше нет нитей, то полностью завершается весь процесс. Данное поведение похоже на последовательный вызов:
main;
thread(1);
thread(2);
В этом примере так и есть, но в любой момент любая нить может передать управление следующей нити, обратившись к yield, proccreate, procexec, procexecl, threadexits, alt, send, recv. Что бы посмотреть, как работает yield можно поэкспериментировать со строчками 29 или 14-15. Yield перемещает нить в конец очереди, оставляя ее «готовой к исполнению». Так если «освободить» строчку 29 то наша нить «main» после создания двух нитей поставит себя в конец очереди, и на экране мы увидим такую очередность: 1-2-main. А если создать дополнительно еще один процесс, строчка 27, то мы увидим, как он исполняется параллельно. Для знакомства с Каналами предлагаю вам такой пример:
01 #include <u.h>
02 #include <libc.h>
03 #include <thread.h>
04
05 int mainstacksize=1024;
06
07 Channel *t;
08
09 void timerproc(void* idx)
10 {
11 ulong index=(ulong)idx;
12
13 for(;;)
14 {
15 sleep(index);
16 sendul(t,index);
17 }
18 }
19
20 void threadmain(int, char**)
21 {
22 ulong idx,i;
23 t=chancreate(sizeof(ulong),0);
24
25 proccreate(timerproc,(void*)1000,1024);
26 proccreate(timerproc,(void*)500,1024);
27
28 for(i=0;i<10;i++)
29 {
30 idx=recvul(t);
31 print("Timer %uld\n",idx);
32 }
33
34 threadexitsall(0);
35 }
Программа состоит из трех процессов, один основной, и два процесса-таймера. Основной процесс создает канал t, с размером элемента (шириной, если можно так сказать) sizeof(ulong) – 4 байта. Посредством proccreate порождаются два процесса, которые с заданным интервалом передают в канал сообщение. Основной поток 10 раз принимает сообщение, при этом, печатая принятое значение, после чего завершается. Следует заметить, что threadexitsall завершает полностью все потоки, а соответственно и процессы которые созданы библиотекой libthread, поэтому «вечный цикл» в timerproc не является проблемой.
Продолжение следует...