Форум программистов, компьютерный форум, киберфорум
Наши страницы
Evg
Войти
Регистрация
Восстановить пароль
Рейтинг: 1.00. Голосов: 2.

Потоки vs процессы

Запись от Evg размещена 14.04.2012 в 11:36
Обновил(-а) Evg 20.04.2012 в 20:16

Изначально я просто попытался ответить на вопрос в теме Процесс VS Потоки, но ответ получился слишком развёрнутым, а потому затолкал его в блог. Вообще тут надо всё аккуратно вылизать, чтобы это дело потянуло на самостоятельную статью, но пока оставлю всё как есть

1. Предисловие

Сейчас процессоры стали многоядерными. Т.е. физический процессор в количестве одной штуки на самом деле работает как два процессора. Чтобы постоянно не говорить одно и то же, далее я буду считать процессорное ядро эквивалентом процессора. Т.е. под "однопроцессорной машиной" будет подразумеваться машина с одним одноядерным процессором, а под "многопроцессорной машиной" будет подразумеваться либо многопроцессорная машина, либо машина с одним многоядерным процессором.

2. Примеры случаев использования потоков

2.1. Пример на использование потоков N1

Нужно ускорить выполнение программы за счёт разбиения одной большой задачи на несколько независимых подзадач, которые можно исполнять параллельно. Тупой искусственный пример. Есть массивы A, B, C по миллиону элементов. В цикле от 0 до 999999 надо выполнить операцию "A[i] = B[i] + C[i]". Каждая итерация цикла является независимой по отношению к другим итерациям цикла. Поэтому процесс вычисления можно разбить на два цикла: от 0 до 499999 и от 500000 до 999999. Циклы эти можно разнести по двум разным потокам, в результате чего они будут исполняться параллельно и скорость вычисления увеличится почти в два раза. "Почти", потому что на создание и удаление потока есть накладные расходы, которые являются константными по времени. А потому чем длиннее цикл, тем меньшую долю в общем времени будут занимать накладные расходы. Обратное тоже верно. А потому если, например, изначальный цикл был на 10 итераций, то при разбиении его на два потока по 5 итераций будет работать гораздо медленнее, т.к. накладные расходы на создание потока будут гораздо выше, чем ускорение за счёт параллельного вычисления.

На современных процессорах мой пример со сложением элементов массива и без потоков вычислится достаточно быстро, а накладные расходы на создание потоков скорее всего будут соизмеримы с выигрышем по скорости. Здесь я просто привёл простой пример, чтобы было понятно. Реально на потоки разбиваются более сложные (ресурсоёмкие) вычисления.

При разбиении на потоки с целью ускорить программу нужно понимать следующее. После того, как вычисления закончатся, нужно все потоки вычисления засинхронизировать. Т.е. в программе будет точка, в которой нужно будет дождаться окончания работы всех потоков. И пока потоки не завершатся, исполнение программы дальше не пойдёт. Поэтому потоки должны быть сбалансированными по времени. Т.е если один поток будет работать 1 секунду, а второй 10 секунд, то первый поток будет 9 секунд вхолостую ждать завершения второго потока и все вычисления отработают за 10 секунд против 11 секунд без разбиения на потоке. Т.е. ускорение будет не в два раза, а всего на 10%

Вычисления можно разбивать и на большее количество подзадач. Мы цикл из 1000000 итераций разбили на 2 подцикла по 500000 итераций, но могли бы разбить, например, на 4 цикла по 250000 итераций. То, насколько широко нужно рапараллеливать программу, зависит от того, сколько процессоров в системе. Т.е. на двухпроцессорной машине программа, разбитая на 4 потока, будет работать с такой же скоростью, как и программа, разбитая на 2 потока (на самом деле чуть медленнее из-за накладных расходов). "Правильно" написанная программа должна в run-time вычислять, сколько процессоров в системе и автоматически разбивать себя на нужное количество потоков. Но можно обойтись и разбиением на фиксированное количество потоков, т.к. технически код программы будет выглядеть намного проще и понятнее. При этом на однопроцессорной и машине код будет работать чуть медленнее, чем без разбиения на потоки, а на двухпроцессорной - чуть медленнее, чем удвоенная скорость на однопроцессорной машине. Зато на четырх и более процессорах программа будет работать чуть медленнее, чем учетверённая скорость на однопроцессорной машине. Когда мы разбивали программу на 4 потока, мы не знали, на какой машине будет она работать. Но зато наша программа готова к работе на многопроцессорных машинах, а в отсутствии многопроцессорности она будет работать с небольшим замедлением

2.2. Пример на использование потоков N2

Ещё одно из возможных применений потоков - это техническое упрощение программы, которая работает с блокируемыми внешними устройствами, файлами, сокетами и т.п. Например, программа с графическим интерфейсом, которая ковыряется в интернете (типа браузера). Если делать "в лоб", то программа получается примерно такая. После того, как пользователь ввёл адрес, программа начинает устанавливать соединение с сервером, чтобы скачать данные. Соединение с сервером - это процесс, который может выполняться долго, например, из-за медленной работы сети, из-за перегрузки сервера и т.п. И в течение времени, пока программа устанавливает соединение и скачивает данные, исполнение программы находится внутри операционной системы в работе с сокетом. А потому программа не может отрабатывать нажатия на кнопки или клавиши, потому что управление на соответствующие обработчики событий не дойдёт (из-за того, что программа находится в процессе ожидания работы с сокетом). И получится такой браузер, что в момент скачивания данных ничего делать нельзя: нельзя лазить по настройкам браузера, нельзя подвинуть или свернуть окно браузера. Короче программа как бы умирает. Думаю, что многие видели подобное поведение даже на коммерческих программах. Причиной такого поведения является то, что текущее управление программы застряло на каком-то блокируемом устройстве (или файле, или сокете, или чём-то ещё) и коды по обработке нажатия на клавиатуру или кнопку мыши попросту не отрабатывают.

Один из способов побороть такое поведение является использование неблокируемых устройств (файлов, сокетов). Точнее, устройство (файл, сокет) остаётся таким, какое оно есть, но на него навешивается дополнительный признак, что оно не блокируется в случае неготовности данных. И дальнейшая работа ведётся по принципу: опросили устройство, если не готово, то занимаемся своими делами, если готово, то считали порцию данных и отправили за чтением следующей порции. При таком подходе код программы становится более громоздким, менее удобным в понимании и отладке.

Другой способ побороть проблему - это вынести работу с блокируемым устройством в поток. Т.е. после того, как пользователь ввёл адрес, программа создаёт поток и уже в потоке занимается установлением соединения и скачиванием данных. А когда это всё завершится, то поток "свистнет", что у него всё готово и основная ветвь программы просто заберёт у потока данные. При этом в момент тормозной работы потока, основная ветвь исполняется в обычном режиме и все кнопочки и прочие причиндалы работают независимо от того, как тормозит поток. Дополнительным действием является лишь периодическая проверка того, не "свистнул" ли поток. В данном случае поток используется не для ускорения программы, а для разведения потока исполнения на тормозной поток (скачивание данных) и на поток быстрого реагирования (обработка событий при отрисовке GUI). Поток здесь используется исключительно с целью упрощения логики работы программы. Для таких целей вполне достаточно одноядерной машины, т.к. поток - это всего лишь средство, чтобы перераспределение процессорного времени переложить на ядро операционной системы

2.3. Пример на использование потоков N3

Имеется компьютер, к которому подключено несколько локаторов и имеется программа, которая обрабатывает данные с локаторов. Программа должна быть написана следующим образом. Нужно каждый локатор опросить на предмет того, готов ли он отдать данные, далее эти данные забрать, забрать данные с локатора, затем данные обработать. Получается много действий в одном флаконе, да к тому же ещё и получается, что процесс изымания данных с локатора довольно медленный, что дополнительно тормозит работу программы (т.е. много времени теряется на выемку данных и остаётся мало времени на их обработку).

В такой ситуации тоже можно работать через потоки. Программа создаёт по одному потоку на каждый локатор. Поток проверяет готовность локатора, забирает у него данные, копирует в свой буффер, а затем "свистнет", что у меня, мол, готова порция данных. Основная ветвь программы просто ожидает, пока какой-нибудь поток "свистнет", заберёт данные, обработает их и ожидает следующего "свистка". Такое техническое разбиение на подзадачи сильно упрощает код программы. Основная программа занимается только обработкой данных плюс небольшие расходы на ожидание "свистков". При таком подходе в основной программе гораздо проще реализовать логику по игнорированию данных с локатора: когда с одного из локаторов поступает много важных данных, то основная программа может понять, что приоритет вычислений должны иметь данные именно с этого локатора, а потому на остальные пока можно забить. Всё это можно реализовать и "в лоб", но тормоза от этого не пропадут, т.к. данные с локатора надо забрать в любом случае, а это потеря времени для программы. Таким образом, разбивая программу на потоки мы делаем сильное техническое упрощение кода программы, а так же решаем проблемы с устранением тормозов, связанных с медленным чтением данных из внешних устройств. Все проблемы, связанные с правильным перераспределением процессорного времени между подзадачами мы перекладываем на операционную систему, которая изначально призвана решать такие задачи. Здесь точно так же всё будет нормально работать и на одноядерном процессоре: медленная работа чтения данных с локатора будет исполняться параллельно с работой вычислительной ветви нашей программы (именно так будет работать любая вменяемая ОС)

3. Примеры случаев использования процессов

3.1. Пример на использование процессов N1

В качестве примера приведу программу, которая использовалась для проброса соединений через прокси. Несколько лет назад получилась такая ситуация, что мы сидели в закрытой сети, для машин в которой не было наружу выхода напрямую, но любое соединение можно было установить через прокси-сервер. Беда была в том, что в те времена с проксями по большому счёту не работала ни одна программа, кроме браузеров. Поэтому пришлось написать самоделку, которая запущена на моей машине. Принцип действия был следующий. Несколько машин хотели подключиться к irc-серверу. На своей машине я запускаю программу, настроенную на конкретный irc-сервер. Программа ожидает к себе клиентского подсоединения, далее присоединяется к irc-серверу, а потом просто пробрасывает данные между клиентским соединением и сервером. При настройке irc-клиента в качестве irc-сервера прописывается адрес моей машины и порт, на котором висит моя программа

Программу можно было устроить следующим образом. Запоминать каждое клиентское соединение, запоминать каждое соединение с сервером, запоминать, какому серверному соединению соответствует какое клиентское соединение и при поступление данных с серверного соединения перекидывать их на соответствующее клиентское соединение, а при поступлении данных с клиентского соединения перекидывать их на соответствующее серверное соединение.

Таким образом получается одна программа, которая за раз обслуживает сразу несколько соединений. У программ такого уровня есть неизбежный геморрой. Нужно постоянно поддерживать динамический список серверных соединений, список клиентских соединений и их соответствие. Когда клиент отвалится, то нужно закрыть соответствующее серверное соединение и вычистить места в списках. То же самое, если отвалится сервер. Если какой-то из клиентов или серверов начнёт тормозить или подвисать при передаче данных, то это всё начнёт сказываться и на других соединениях, а потому это безобразие нужно отслеживать. Все эти проблемы решаемы, но технически усложняют программу. Именно в таком виде поначалу я и написал программу. При этом вместо динамических списков были статические, какого-то нормального контроля за подвисаниями не было. Т.е. получилась программа исключительно для персонального использования и проще было временами перезапускать программу, чем переписывать её для решения проблем

Но потом пришла мысль, что можно поступить гораздо проще. Все соединения являются полностью независимыми друг от друга. А потому можно их развести по разным процессам. И тогда программа получается предельно простой. Дождались клиентского соединения, после чего fork'нулись. Родительский процесс остался ожидать следующего клиентского соединения. А дочерний процесс подсоединился к серверу и начал пробрасывать данные между единственным сервером и единственным клиентом (т.е. не надо запоминать динамические списки и поддерживать их). В случае проблем или подвисаний с любой из сторон программа просто завершается и всё (при этом другие клиентские соединения в других программах успешно работают).

Можно было использовать потоки вместо процессов? Можно. Но при этом мы имели бы следующее. Если основной поток программы, который ожидает клиентских соединений по каким-то причинам фатально умер (из-за ошибки в программе, например, или из-за каких-то неучтённых внешних факторов), то автоматически бы умерли все потоки. В примерах с потоками мы имели дело со случаями, когда целую задачу мы разбивали на фрагменты, которые являются подзадачами одной большой задачи, но НЕ являются самостоятельными независимыми задачами. А вот в случае с нашим сервером процесс пробрасывания данных между клиентом и сервером является полностью независимой задачей и с остальными такими же задачами больше не пересекается. С точки зрения потребляемых ресурсов несколько процессов являются неэффективными по сравнению с несколькими потоками. Но в моём случае надо было обслужить всего 7-8 машин, а потому использование нескольких процессов не создавало сколь бы то ни было значимой нагрузки, но при этом техническая реализация программы была предельно простой

*** update

Вспомнил, что код программы я выкладывал на форум: раз и два. Но это код "неправильной" версии (без создания процессов). Не знаю, сохранился ли у меня код "правильной" версии, если найду - то выложу. Чисто для сравнения, насколько он получился проще

4. Ссылки на темы, где обсуждался данный вопрос
Просмотров 4015 Комментарии 12
Всего комментариев 12
Комментарии
  1. Старый комментарий
    Аватар для KuKu
    Давно это было, но спасибо за ответ) На момент задания вопроса интересовало возможно ли ситуация, когда что-то технически можно реализовать через процессы, но нельзя через потоки. Про irc, да код раздуется, но по сути через потоки это вполне реализуемо.
    Запись от KuKu размещена 14.04.2012 в 14:52 KuKu вне форума
  2. Старый комментарий
    Аватар для Evg
    Если ставить вопрос как "когда что-то технически можно реализовать через процессы, но нельзя через потоки", то тут с ходу затрудняюсь что-то придумать. Может что-то и есть, но сразу не вижу

    Единственное, что видно сразу - это ограничение по памяти. Потоки живут в одном адресном пространстве, а потому их суммарное потребление памяти ограничено памятью под один процесс. В то время, как 10 процессов могут употребить в 10 раз больше памяти. Но это актуально только в 32-битном режиме, где память ограничена двумя гигабайтами на процесс. В 64-битном режиме надо очень сильно постараться, чтобы упереться в ограничение по памяти (разумеется, виртуальной)
    Запись от Evg размещена 14.04.2012 в 17:40 Evg вне форума
  3. Старый комментарий
    Аватар для Pure
    для пущей глубины все же надо расписать ПОТОКИ vs OVERLAPPED. что же лучше? оверлаппед это не потоки ли? просто управляемые системой а не руками. ну или нити. могу ошибаться. Но если ты распишешь будет ОЧЕНЬ полезно всем.

    пардон там под линукс все. ну может все же есть сходство с виндой?
    Запись от Pure размещена 14.04.2012 в 18:31 Pure вне форума
  4. Старый комментарий
    А в чём смыл сравнения? Разве они где нибудь взаимозаменяемы? Пусть даже замена влечёт недостатки. Но чтоб была просто возможна. Потоки - части одной проги, параллельно решающие некоторые подзадачи. А параллельные процессы - это право пользователя решать одновременно в разных программах разные задачи. Зачем смешивать одно с другим?
    Запись от размещена 14.04.2012 в 20:36
  5. Старый комментарий
    [QUOTE]Можно было использовать потоки вместо процессов? Можно. Но при этом мы имели бы следующее. Если основной поток программы, который ожидает клиентских соединений по каким-то причинам фатально умер (из-за ошибки в программе, например, или из-за каких-то неучтённых внешних факторов), то автоматически бы умерли все потоки. [/QUOTE]А если умерло что то в системе? С другой стороны, это отдельные задачи при всей их однотипности. Если я в многопользовательской системе с 10 терминалов открою текстовый редактор и приглашу за них ещё 10 юзверей, но в систему со всех терминалов зайду под одним логином, то надо ли в этом случае собирать все редакторы в потоки одного процесса? А если это разные редакторы? Назначение одно, а программы разные? Возможно даже интерфейс у них один, но внутренности не совпадают? Зачем здесь вообще потоки? Можно даже ещё смешней: вручную открыть для каждого по штуке. И что?
    Запись от размещена 14.04.2012 в 20:54
  6. Старый комментарий
    [QUOTE]Если ставить вопрос как "когда что-то технически можно реализовать через процессы, но нельзя через потоки", то тут с ходу затрудняюсь что-то придумать. Может что-то и есть, но сразу не вижу[/QUOTE]Ну элементарно же: работа с экземплярами прикладной программы под разными логинами пользователей операционной системы не может быть реализована потоками этой прикладной программы.
    Запись от размещена 14.04.2012 в 20:58
  7. Старый комментарий
    Аватар для KuKu
    Надо было назвать fork vs pthread, а то название темы лишает способности к чтению.
    Запись от KuKu размещена 14.04.2012 в 21:20 KuKu вне форума
  8. Старый комментарий
    Аватар для alex_x_x
    Цитата:
    Сообщение от KuKu Просмотреть комментарий
    Надо было назвать fork vs pthread, а то название темы лишает способности к чтению.
    по сути в линуксе потоки все те же процессы, созданные другим syscall'ом и имеющие некоторые особенности
    так что vs нету)
    Запись от alex_x_x размещена 14.04.2012 в 21:34 alex_x_x вне форума
  9. Старый комментарий
    Аватар для Evg
    Да я просто отвечал на вопрос слишком обстоятельно и увидел, что написал слишком много, а потому и закинул в блог, шоб добро не пропадало

    > пардон там под линукс все. ну может все же есть сходство с виндой?

    Мне казалось, что потоки и процессы в винде и линуксе принципиально не отличаются. Правда я не знаю, что такое overlapped
    Запись от Evg размещена 14.04.2012 в 21:56 Evg вне форума
  10. Старый комментарий
    Аватар для KuKu
    Цитата:
    по сути в линуксе потоки все те же процессы, созданные другим syscall'ом и имеющие некоторые особенности
    так что vs нету)
    Про то что потоки это частный случай процесса, это ясно. Вопрос про то имеет ли этот "частный случай" какие-нибудь недостатки по сравнению с процессом. Просто умя фантазии не хватает, нафига вообще форкаться. Потом гораздо больше геморроя будет с синхронизацией и передачей данных между процессами.
    Запись от KuKu размещена 14.04.2012 в 22:16 KuKu вне форума
  11. Старый комментарий
    Аватар для Evg
    > Просто умя фантазии не хватает, нафига вообще форкаться

    Потоки нужны, когда выполняются отдельные фрагменты одной задачи. Т.е. в конечном итоге все потоки сходятся в одной точке и основной процесс собирает все данные, полученные в разных потоках. Процессы нужны тогда, когда у тебя действительно независимые задачи. Основное применение fork'а - это порождение новых процессов. Типа запустил программу-сервер из консоли, программа форкнулась, дочерний процесс (реальный сервер) начал работать и отрезался от консоли, а родительский процесс отрапортовал, что дескать сервер запущен и завершил работу.

    Абсолютно все процессы в системе являются потомками главного процесса init. Любой новый процесс порождается fork'аньем с последующим запуском exec'а в дочернем процессе. Когда ты в консоли запускаешь какую-то программу - это всегда делается через fork
    Запись от Evg размещена 15.04.2012 в 12:12 Evg вне форума
  12. Старый комментарий
    Аватар для Evg
    Вот тут пример того, как делается запуск приложений из shell'а и связываются между собой pipe'ом
    http://www.cyberforum.ru/c-linux/thread167695.html
    Запись от Evg размещена 15.04.2012 в 12:16 Evg вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru