Справедливое сравнение fork() Vs Thread
Я обсуждал относительную стоимость fork() Vs thread() для распараллеливания задачи.
Мы понимаем основные различия между процессами Vs Thread
Тема:
- Простая связь между потоками
- Быстрое переключение контекста.
Процессы:
- Отказоустойчивость.
- Общение с родителем не является реальной проблемой (откройте канал)
- Связь с другими дочерними процессами жесткая
Но мы не согласились на стартовую стоимость процессов Vs-потоков.
Поэтому для проверки теорий я написал следующий код. Мой вопрос: Является ли это действительным тестом на измерение начальной стоимости или чего-то не хватает. Также мне будет интересно, как каждый тест выполняется на разных платформах.
fork.cpp
#include <boost/lexical_cast.hpp>
#include <vector>
#include <unistd.h>
#include <iostream>
#include <stdlib.h>
#include <time.h>
extern "C" int threadStart(void* threadData)
{
return 0;
}
int main(int argc,char* argv[])
{
int threadCount = boost::lexical_cast<int>(argv[1]);
std::vector<pid_t> data(threadCount);
clock_t start = clock();
for(int loop=0;loop < threadCount;++loop)
{
data[loop] = fork();
if (data[looo] == -1)
{
std::cout << "Abort\n";
exit(1);
}
if (data[loop] == 0)
{
exit(threadStart(NULL));
}
}
clock_t middle = clock();
for(int loop=0;loop < threadCount;++loop)
{
int result;
waitpid(data[loop], &result, 0);
}
clock_t end = clock();
std::cout << threadCount << "\t" << middle - start << "\t" << end - middle << "\t"<< end - start << "\n";
}
Thread.cpp
#include <boost/lexical_cast.hpp>
#include <vector>
#include <iostream>
#include <pthread.h>
#include <time.h>
extern "C" void* threadStart(void* threadData)
{
return NULL;
}
int main(int argc,char* argv[])
{
int threadCount = boost::lexical_cast<int>(argv[1]);
std::vector<pthread_t> data(threadCount);
clock_t start = clock();
for(int loop=0;loop < threadCount;++loop)
{
if (pthread_create(&data[loop], NULL, threadStart, NULL) != 0)
{
std::cout << "Abort\n";
exit(1);
}
}
clock_t middle = clock();
for(int loop=0;loop < threadCount;++loop)
{
void* result;
pthread_join(data[loop], &result);
}
clock_t end = clock();
std::cout << threadCount << "\t" << middle - start << "\t" << end - middle << "\t"<< end - start << "\n";
}
Я ожидаю, что Windows ухудшится при создании процессов.
Но я ожидал бы, что современные системы Unix будут иметь довольно легкую стоимость вилки и быть по крайней мере сопоставимы с потоком. В более старых системах стиля Unix (до того, как fork() был реализован с использованием копии на страницах записи), что это будет хуже.
В любом случае Мои результаты времени:
> uname -a
Darwin Alpha.local 10.4.0 Darwin Kernel Version 10.4.0: Fri Apr 23 18:28:53 PDT 2010; root:xnu-1504.7.4~1/RELEASE_I386 i386
> gcc --version | grep GCC
i686-apple-darwin10-gcc-4.2.1 (GCC) 4.2.1 (Apple Inc. build 5659)
> g++ thread.cpp -o thread -I~/include
> g++ fork.cpp -o fork -I~/include
> foreach a ( 1 2 3 4 5 6 7 8 9 10 12 15 20 30 40 50 60 70 80 90 100 )
foreach? ./thread ${a} >> A
foreach? end
> foreach a ( 1 2 3 4 5 6 7 8 9 10 12 15 20 30 40 50 60 70 80 90 100 )
foreach? ./fork ${a} >> A
foreach? end
vi A
Thread: Fork:
C Start Wait Total C Start Wait Total
==============================================================
1 26 145 171 1 160 37 197
2 44 198 242 2 290 37 327
3 62 234 296 3 413 41 454
4 77 275 352 4 499 59 558
5 91 107 10808 5 599 57 656
6 99 332 431 6 665 52 717
7 130 388 518 7 741 69 810
8 204 468 672 8 833 56 889
9 164 469 633 9 1067 76 1143
10 165 450 615 10 1147 64 1211
12 343 585 928 12 1213 71 1284
15 232 647 879 15 1360 203 1563
20 319 921 1240 20 2161 96 2257
30 461 1243 1704 30 3005 129 3134
40 559 1487 2046 40 4466 166 4632
50 686 1912 2598 50 4591 292 4883
60 827 2208 3035 60 5234 317 5551
70 973 2885 3858 70 7003 416 7419
80 3545 2738 6283 80 7735 293 8028
90 1392 3497 4889 90 7869 463 8332
100 3917 4180 8097 100 8974 436 9410
Изменить:
Выполнение 1000 детей вызвало отказ версии fork.
Поэтому я уменьшил количество детей. Но выполнение одного теста также кажется несправедливым, поэтому здесь представлен ряд значений.
Ответы
Ответ 1
mumble... Мне не нравится ваше решение по многим причинам:
-
Вы не учитываете время выполнения дочерних процессов/потоков.
-
Вы должны сравнить использование процессора не годовое истекшее время. Таким образом, ваша статистика не будет зависеть, например, от перегрузки на диске.
-
Пусть ваш ребенок обрабатывает что-то. Помните, что "современная" вилка использует механизмы копирования на запись, чтобы избежать выделения памяти дочернему процессу до тех пор, пока это не понадобится. Слишком легко выйти немедленно. Таким образом, вы избегаете совершенно всех недостатков вилки.
-
Время процессора - это не единственная стоимость, которую вы должны учитывать. Потребление памяти и медлительность IPC являются недостатками решения fork.
Вы можете использовать "rusage" вместо "clock" для измерения реального использования ресурсов.
P.S. Я не думаю, что вы действительно можете измерить процесс/поток накладных, написание простой тестовой программы. Слишком много факторов, и, как правило, выбор между потоками и процессами обусловлен другими причинами, чем просто использование процессора.
Ответ 2
В Linux fork
есть специальный вызов sys_clone
, либо внутри библиотеки, либо внутри ядра. Clone имеет много переключателей для включения и выключения, и каждый из них влияет на то, как дорого начать.
Фактическая функция библиотеки clone
, вероятно, дороже, чем fork
, хотя, поскольку она делает больше, хотя большая часть из них находится на дочерней стороне (свопинг стека и вызов функции по указателю).
Ответ 3
Что показывает этот микро-бенчмарк, так это то, что создание и объединение потоков (нет данных о вилке, когда я пишу это) занимает от нескольких десятков или сотен микросекунд (если ваша система имеет CLOCKS_PER_SEC = 1000000, что, вероятно, требование XSI).
Поскольку вы сказали, что fork() занимает в 3 раза больше стоимости потоков, мы все равно говорим десятки миллисекунд в худшем случае. Если это заметно в приложении, вы можете использовать пулы процессов/потоков, как это делал Apache 1.3. В любом случае, я бы сказал, что время запуска - спорный вопрос.
Важное отличие потоков от процессов (от Linux и большинства Unix-подобных) - это то, что на процессах вы явно выбираете, что делиться, используя IPC, общую память (SYSV или mmap-стиль), трубы, сокеты (вы можете отправить файловые дескрипторы над сокетами AF_UNIX, то есть вы можете выбрать, какой fd для общего доступа),... В то время как на потоках почти все по умолчанию используется совместно, нужно ли делиться им или нет. Фактически, именно по этой причине в Plan 9 была rfork(), а у Linux есть clone() (и недавно unshare()), поэтому вы можете выбрать, что поделиться.