Процеси и нишки (1/3)
- Процес — поток на изпълнение на програма, записана в
оперативната памет, с единствена текуща точка
- Всяка програма, която се изпълнява, е отделен процес.
- Всеки процес си има:
- отделен идентификатор — цяло число (process ID)
- отделен сегмент памет
- отделна програма за изпълнение (сегмент за код, понякога
споделен, но това не се вижда)
- отделни привилегии (потребителски account, с чиито права
е пуснат, допълнителни привилегии)
- отделна таблица с отворени файлове (рядко споделена с
родителския и сродни процеси)
- отделна работна памет (сегмент за данни)
- отделна точка на изпълнение (следваща инструкция, която
да бъде изпълнена)
Процеси и нишки (2/3)
- Нишка — поток на изпълнение на програма, записана в
оперативната памет, с единствена текуща точка…
но в рамките на един процес!
- В рамките на един процес могат да се изпълняват няколко нишки
- Всички нишки споделят:
- идентификатора на процеса (process ID)
- сегмента памет на процеса
- програмата за изпълнение на процеса
- привилегиите на процеса
- таблицата с отворени файлове на процеса
- сегмента за данни на процеса
Процеси и нишки (3/3)
- Всяка нишка си има:
- отделен идентификатор в рамките на
процеса — цяло число (thread ID)
- отделна точка на изпълнение (следваща инструкция,
която да бъде изпълнена)
- отделна част от сегмента данни, заделена за локални
за нишката данни (thread-local storage)
Многопроцесен модел, времеделене
- Във всеки момент операционната система изпълнява множество
процеси „едновременно“, макар че процесорът може
да изпълнява само една инструкция в даден момент... как?
- Времеделене: „квант“ време, даван на всеки процес
по определен алгоритъм. След изтичането му ОС му „взима
думата“ и дава квант на следващия процес
- Модели на многопроцесна работа:
- кооперативен — всеки процес решава кога да
предаде управлението
- preemptive — ОС разпределя квантите време
„Роднински отношения“ между процеси
- ОС разглежда всички процеси в едно дърво
- Всеки процес си има родител (parent) и нула или повече
наследници (child-процеси)
- Всички процеси са наследници (преки или непреки) на
init (под Windows обикновено се казва
другояче)
- $$ — връща идентификатора на текущия
процес (getpid() в POSIX)
- getppid() — връща идентификатора на
родителския процес
Създаване на нов процес
- fork() — създава нов процес, наследник
на текущия
- fork() копира:
- сегмента код (при някои ОС го споделя като
read-only)
- привилегиите
- сегмента за данни (copy-on-write)
- точката на изпълнение
- fork() споделя:
- таблицата с отворени файлове
- fork() създава нов:
- идентификатор на процес (process ID)
- fork() връща като резултат:
- в родителския процес: идентификатора на новия
наследник (child process ID)
- в наследника (новия процес) — нула
(идентификаторът на родителския процес може да бъде
получен с getppid())
Обработка на завършил процес (1/2)
- exit(code) — завършва процес със
зададения код на завършване (обикновено 0, ако всичко е наред,
> 0 за сигнализация за грешка)
- След завършване на всеки процес ОС не освобождава паметта му,
докато родителят не се поинтересува от него!
- „Зомби“ (zombie process) — процес, който е
завършил, но родителят още не е разбрал за това
- Ако родителят умре, без да се поинтересува за
„здравето“ на наследниците си, те стават наследници
на init, който събира информацията за тях, без
да прави с нея каквото и да било, и ги оставя „да си отидат
с мир“
Обработка на завършил процес (2/2)
- waitpid(pid, flags) — взима информацията
за завършил процес
- pid — process ID на процеса или
-1 за всеки
- flags — WNOHANG за неблокираща
работа — ако няма процеси, не чакай
- Връща process ID на наследника, 0 ако няма наследници, -1 при
грешка
- Ако всичко е наред, в $? е exit code на наследника
Пример: fork(), waitpid()
my $pid;
if (!defined($pid == fork()) {
die "Could not fork: $!\n";
} elsif ($pid == 0) {
print "This is the child process\n";
sleep(5);
print "Exiting.\n";
exit(1);
}
# Parent process
do {
$kid = waitpid -1, WNOHANG;
} until ($kid > 0);
print "Process $kid exited with status $?\n";
Предаване на данни между процеси
- IO::Handle — универсален файлов
манипулатор / дескриптор
- Създаване на обект от тип IO::Handle
обикновено изпълнява системното извикване
pipe(), което създава файлов дескриптор
(всъщност два, но това няма значение), който може да бъде
използван след това за IPC (Interprocess communication)
- Отворените файлове се споделят между процеси, но файловите
дескриптори могат да бъдат използвани само
еднопосочно — информация се предава само от
родителя към наследника или само от наследника към родителя
- Използване: отварят се два обекта IO::Handle,
един от които се използва в едната посока, а другият —
в другата
- Родителят пише от единия и чете другия, наследникът чете от
първия и пише във втория
Пример: IO::Handle за IPC
use IO::Handle;
my $pid, $reader, $parent2child, $child2parent;
$parent2child = new IO::Handle; $child2parent = new IO::Handle;
$parent2child->autoflush(1); $child2parent->autoflush(1);
if (!($pid = fork())) {
die "Could not fork: $!\n";
} elsif ($pid == 0) {
# Child process
print "$$ starting\n";
# Wait until the parent tells us what to do
$data = <$parent2child>;
print "$$ received $data\n";
print $child2parent "OK";
# Still the child process
print "$$ exiting\n";
exit(0);
}
# Parent process
print "$$ parent process continuing\n";
print $parent2child "REQUEST";
print "$$ waiting for the child's response\n";
$data = $child2parent;
print "$$ received $data from the child\n";
exit(0);
Какво е thread
- Thread (нишка) е поток от операции, който се случва в рамките
на програма с единствена стартова позиция.
- Всеки процес има поне по една нишка, но може да има и повече
от една. Работейки в рамките на един процес, нишките могат
много лесно да си споделят данни, общи за изпълнението на
програмата.
Threading модели
- boss/worker — един основен контролен
процес (boss) се грижи за създаването/ликвидирането на множество
други (workers). Boss-ът комуникира с worker процесите и
обикновено само чака да се случат някакви събития, които после
прехвърля на worker процесите.
- work crew — при този модел няколко процеса
си поделят различни (най-често независими) части от работата.
Полезен е, ако приложението се изпълнява на многопроцесорна
система.
- pipeline — при този модел всеки процес
се занимава с дадена част от работата и след като приключи,
предава изпълнението на друг процес. Всеки от процесите
извършва дадена операция върху данните, с които се работи,
и предава управлението на следващия. Може да се използва и за
реализиране на рекурсивни процеси, които на всяко ниво създават
нов процес.
Threading модели в Perl
- Perl-ските нишки не са като никои други, макар че приличат
на pthreads (POSIX threads) и общите принципи
важат и за тях
- В Perl съществуват два модела за нишки — стар (от версия
5.005 до 5.6) и нов (след версия 5.6). Новият се нарича
ithreads и се приема за стабилен в сравнение
със стария, наречен 5005threads
- ithreads произлиза от interpreter threads
- За да може да използвате thread функционалност в Perl програма,
е необходимо:
- Архитектурата, на която се изпълнява въпросната Perl
програма, да позволява използване на нишки.
- Perl да е компилиран с поддръжка на нишки
С помощта на модула Config и проверка на
$Config{useithreads} може да получите
информация за това дали могат да се ползват нишки.
Създаване на нишка
- use threads;
$thr = threads->new(\&sub1, @ParamList)
sub sub1
{
my @initParams = @_;
print "In the thread\n";
}
- Нишката в този пример започва изпълнение в подпрограма sub1.
- Основната нишка може да създаде множество нишки, които работят
с един и същ код.
- Могат да се подават параметри на новосъздадената нишка.
Изчакване на нишка
- use threads;
$thr = threads->new(\&sub1);
@ReturnData = $thr->join;
print "Thread returned @ReturnData";
sub sub1 { return "Fifty-six", "foo", 2; }
- Основната нишка може да изчаква всяка създадена от нея нишка
да приключи работа, ако резултатът е нужен за по-нататъшното
изпълнение на приложението.
- Това става с помощта на метода join, приложен
върху обекта на въпросната нишка. Изчакването води и до
освобождаване на ресурсите след приключване на нишката.
Изоставяне на thread
- use threads;
$thr = threads->new(\&sub1);
$thr->detach; # "изоставяне"
sub1
{
$a = 0;
while (1)
{
$a++;
print "\$a is $a\n";
sleep 1;
}
}
- Когато не се интересуваме от резултата от изпълнението на
нишката, тя може да бъде „изоставена“.
- Това става с помоща на метода detach.
Извикването му указва на Perl интерпретатора да освободи
заеманите ресурси след приключване на нишката.
- След detach не можем да изпълняваме
join върху нишката.
Споделяне на данни (sharing)
- Основна разлика между ithreads модела и
5005threads (както и останалите threading
модели) е, че по подразбиране не се share-ват никакви данни
- Всички данни се копират при създаване на нова нишка.
- Използването на споделени данни в Perl се указва изрично с
помощта на атрибута
„: shared“,
който може да бъде прилаган към всяка променлива, след като бъде
включен модул threads::shared
- use threads;
use threads::shared;
my $sharedVar : shared;
Race Conditions
- Въпреки че нишките са полезни в много случаи, те са
предпоставка за възникване на „race conditions“
— ситуации на несинхронизиран достъп до данни от няколко
нишки.
- В подобни случаи не е ясно коя нишка ще промени данните
първо.
- Perl предоставя различни методи за синхронизация:
- lock — заключване на споделените
данни
- еnqueue — използване на
специален thread-safe обект, който работи като опашка
за прехвърляне на данни между нишките
- semaphore — общ механизъм за
контрол на изпълнението
Заключване с lock
- use threads;
use threads::shared;
my $total : shared;
sub set1 {lock($total); for (1..10) { print $total+=10; print "\n"} };
sub set2 {lock($total); for (1..10) { print $total+=20 ; print "\n"} };
my $thr1 = threads->new(\&set1);
my $thr2 = threads->new(\&set2);
$thr1->join(); $thr2->join();
- lock се прилага върху дадена shared
променлива. Може да бъде приложен рекурсивно при навлизане в
по-долен блок
- lock-ната променлива продължава да бъде такава до края на
блока код (докато lock не излезе от scope). Няма явен механизъм
за unlock.
Потенциален пропблем — deadlock
- use threads;
my $a : shared = 4;
my $b : shared = "foo";
my $thr1 = threads->new(sub { lock($a);
sleep 20;
lock($b);
});
my $thr2 = threads->new(sub { lock($b);
sleep 20;
lock($a);
});
- Deadlock е ситуация, която възниква, когато няколко нишки се
опитват да lock-нат едновременно данни, които другите ползват.
- Горният пример няма да завърши никога, тъй като ще се получи
deadlock при опита на която и да е от двете нишки да заключи
данни, които другата вече е заключила.
Предаване на данни през опашки
-
use threads;
use Thread::Queue;
my $DataQueue = Thread::Queue->new;
$thr = threads->new(sub {
while ($DataElement = $DataQueue->dequeue)
{
print "Popped $DataElement off the queue\n"; } });
$DataQueue->enqueue(12);
$DataQueue->enqueue("A", "B", "C");
$DataQueue->enqueue(\$thr);
sleep 10;
$DataQueue->enqueue(undef);
$thr->join;
- С помощта на enqueue и dequeue,
приложени върху обекта на нишката, могат да се предават данни
през виртуална „опашка“
Семафори
- use threads;
use Thread::Semaphore;
my $semaphore = Thread::Semaphore->new(5);
# Creates a semaphore with the counter set to five
$thr1 = threads->new(\&sub1);
$thr2 = threads->new(\&sub1);
sub sub1 { $semaphore->down(5);
# Намаляваме брояча
# Правим нещо
$semaphore->up(5);
# Увеличаваме брояача
}
$thr1->detach; $thr2->detach;
- Семафорите са специални обекти на класа Thread::Semaphore,
които имат два метода: up и
down, с които се намалява и увеличава
вътрешният брояч на семафора.
- Семафорът позволява „преминаване“ през него само
когато броячът е нула.
Преглед на всички нишки
- # Loop through all the threads
foreach $thr (threads->list)
{
# Don't join the main thread or ourselves
if ($thr->tid && !threads::equal($thr, threads->self))
{
$thr->join;
}
}
- С помощта на метода threads->list може да
се получи списък с всички създадени до момента thread обекти
- $thr->tid връща id-то на нишката, която се
идентифицира през обекта $thr.
- threads->self връща id-то на текущата
нишка
Сравнение fork() — ithreads
- При многопроцесно програмиране с fork():
- Създаването на нов процес е бързо и оптимизирано
- Споделянето на паметта е copy-on-write, което е по-бързо
и по-евтино
- Стабилно и изтествано във времето
- Управлението на процесите може да бъде по-сложно, следят
се сигнали
- Споделянето на данни между процесите е по-трудно
- Може да се достигне горна граница на позволения брой
на процесите в системата
- Не е реализирано на абсолютно всички ОС
- Подходящо при много на брой, краткротрайни задачи
Сравнение fork() — ithreads
- Многонишково програмиране с ithreads:
- Създаването на нова нишка е бавно поради копирането
на всички локални променливи
- Достъпът до споделените променливи е малко по-бавен от
този до стандартните
- Локалните променливи увеличават разхода на памет
- Управлението на задачите е по-лесно
- Споделянето на данни е значително по-лесно и елегантно
- Не се счита за стабилно на всички ОС и не е реализирано
за всички
- Подходящо при много на брой, дългоизпълянващи се
задачи