Хотя потоки могут ускорить выполнение кода, нужны ли они на самом деле? Может ли каждый фрагмент кода быть выполнен с использованием одного потока, или существует что-то, что может быть достигнуто только с использованием нескольких потоков?
Хотя потоки могут ускорить выполнение кода, нужны ли они на самом деле? Может ли каждый фрагмент кода быть выполнен с использованием одного потока, или существует что-то, что может быть достигнуто только с использованием нескольких потоков?
Ответы:
Прежде всего, потоки не могут ускорить выполнение кода. Они не заставляют компьютер работать быстрее. Все, что они могут сделать, это повысить эффективность компьютера, используя время, которое в противном случае было бы потрачено впустую. В определенных типах обработки эта оптимизация может увеличить эффективность и уменьшить время выполнения.
Простой ответ - да. Вы можете написать любой код для запуска в одном потоке. Доказательство: однопроцессорная система может выполнять инструкции только линейно. Наличие нескольких строк выполнения выполняется операционными системами, обрабатывающими прерывания, сохраняющими состояние текущего потока и запускающими другой.
Комплекс ответ ... более сложный! Причина, по которой многопоточные программы часто могут быть более эффективными, чем линейные, связана с аппаратной «проблемой». Процессор может выполнять вычисления быстрее, чем ввод-вывод памяти и жесткого диска. Так, например, инструкция «add» выполняется намного быстрее, чем «fetch». Кэши и извлечение инструкций специальной программы (не уверенный в точном значении здесь) могут до некоторой степени бороться с этим, но проблема скорости остается.
Потоки - это способ борьбы с этим несоответствием, используя ЦП для команд, связанных с ЦП, пока выполняются инструкции ввода-вывода. Типичный план выполнения потока, вероятно, будет следующим: выборка данных, обработка данных, запись данных. Предположим, что выборка и запись занимают 3 цикла, а обработка - один для наглядности. Вы видите, что когда компьютер читает или пишет, он ничего не делает по 2 цикла каждый? Очевидно, это лениво, и нам нужно взломать наш оптимистический кнут!
Мы можем переписать процесс, используя многопоточность, чтобы использовать это потерянное время:
И так далее. Очевидно, что это несколько надуманный пример, но вы можете видеть, как этот метод может использовать время, которое в противном случае было бы потрачено на ожидание ввода-вывода.
Обратите внимание, что многопоточность, как показано выше, может только повысить эффективность процессов с интенсивным вводом-выводом. Если программа в основном рассчитывает вещи, то не будет много «дыр», в которых мы могли бы проделать больше работы. Кроме того, при переключении между потоками накладные расходы накладываются на несколько инструкций. Если вы запускаете слишком много потоков, центральный процессор будет тратить большую часть своего времени на переключение и практически не будет работать над проблемой. Это называется избиением .
Это все хорошо для одноядерного процессора, но большинство современных процессоров имеют два или более ядер. Потоки по-прежнему служат той же цели - максимизировать использование ЦП, но на этот раз у нас есть возможность выполнять две отдельные инструкции одновременно. Это может сократить время работы в то время, как доступно много ядер, потому что компьютер фактически выполняет многозадачность, а не переключение контекста.
В случае нескольких ядер потоки предоставляют метод разделения работы между двумя ядрами. Выше все еще относится к каждому отдельному ядру, хотя; Программа, которая работает с максимальной эффективностью с двумя потоками на одном ядре, скорее всего будет работать с максимальной эффективностью с примерно четырьмя потоками на двух ядрах. (Эффективность здесь измеряется минимальным выполнением команды NOP.)
Проблемы с запуском потоков на нескольких ядрах (в отличие от одного ядра) обычно решаются аппаратно. Процессор будет уверен, что он заблокирует соответствующие области памяти перед чтением / записью в него. (Я читал, что для этого он использует специальный бит флага в памяти, но это может быть достигнуто несколькими способами.) Как программист с языками более высокого уровня, вам не нужно больше беспокоиться о двух ядрах, пока вы пришлось бы с одним.
TL; DR: потоки могут разделяться, чтобы позволить компьютеру обрабатывать несколько задач асинхронно. Это позволяет компьютеру работать с максимальной эффективностью, используя все доступное время обработки, а не блокируя, когда процесс ожидает ресурс.
Что может сделать несколько потоков, чего не может один поток?
Ничего.
Простой контрольный эскиз:
Обратите внимание, однако, что здесь скрыто большое предположение: именно язык, используемый в одном потоке, является тьюрингово-полным.
Итак, более интересным вопросом будет: «Может ли добавление только многопоточности к языку, не являющемуся полным по Тьюрингу, сделать его полным по Тьюрингу?» И я верю, ответ «Да».
Давайте возьмем тотальные функциональные языки. [Для тех, кто не знаком: так же, как функциональное программирование - это программирование с использованием функций, общее функциональное программирование - это программирование с использованием общих функций.]
Тотальные функциональные языки, очевидно, не являются полными по Тьюрингу: вы не можете написать бесконечный цикл в TFPL (на самом деле, это почти то же самое, что определение «всего»), но вы можете в машине Тьюринга, так что, по крайней мере, существует одна программа, которая не может быть записано в TFPL, но может быть в UTM, поэтому TFPL менее мощны в вычислительном отношении, чем UTM.
Однако, как только вы добавляете многопоточность в TFPL, вы получаете бесконечные циклы: просто выполняйте каждую итерацию цикла в новом потоке. Каждый отдельный поток всегда возвращает результат, следовательно, это Total, но каждый поток также порождает новый поток, который выполняет следующую итерацию, до бесконечности.
Я думаю, что этот язык будет полным по Тьюрингу.
По крайней мере, он отвечает на оригинальный вопрос:
Что может сделать несколько потоков, чего не может один поток?
Если у вас есть язык, который не может делать бесконечные циклы, то многопоточность позволяет вам делать бесконечные циклы.
Обратите внимание, конечно, что порождение потока является побочным эффектом, и поэтому наш расширенный язык уже не только не тотален, он даже не функционален.
Теоретически, все, что делает многопоточная программа, может быть выполнено и с помощью однопоточной программы, только медленнее.
На практике разница в скорости может быть такой большой, что невозможно использовать однопотоковую программу для этой задачи. Например, если у вас есть задание пакетной обработки данных, выполняемое каждую ночь, и для его завершения в одном потоке требуется более 24 часов, у вас нет другого выбора, кроме как сделать его многопоточным. (На практике пороговое значение, вероятно, еще меньше: часто такие задачи обновления должны завершаться к раннему утру, прежде чем пользователи снова начнут использовать систему. Кроме того, от них могут зависеть другие задачи, которые также должны завершиться в ту же ночь. доступное время выполнения может составлять всего несколько часов / минут.)
Выполнение вычислительной работы над несколькими потоками является формой распределенной обработки; Вы распределяете работу по нескольким потокам. Другим примером распределенной обработки (с использованием нескольких компьютеров вместо нескольких потоков) является заставка SETI: обработка большого количества данных измерений на одном процессоре займет очень много времени, и исследователи предпочли бы увидеть результаты до выхода на пенсию ;-) Однако они у нас нет бюджета, чтобы арендовать суперкомпьютер так долго, поэтому они распределяют работу по миллионам домашних ПК, чтобы сделать его дешевым.
Хотя потоки кажутся небольшим шагом от последовательных вычислений, на самом деле они представляют собой огромный шаг. Они отбрасывают наиболее важные и привлекательные свойства последовательных вычислений: понятность, предсказуемость и детерминизм. Потоки, как модель вычислений, дико недетерминированы, и работа программиста превращается в обрезание этого недетерминизма.
- Проблема с потоками (www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf).
Несмотря на то, что при использовании потоков можно добиться некоторых преимуществ в плане производительности, которые позволяют распределять работу между несколькими ядрами, они часто имеют высокую цену.
Одним из недостатков использования потоков, которые еще не упомянуты здесь, является потеря разделения ресурсов, которые вы получаете с однопоточными пространствами процессов. Например, допустим, вы столкнулись с проблемой сегфоута. В некоторых случаях это можно исправить в многопроцессорном приложении, в котором вы просто позволяете погибающему ребенку умереть и возродите новое. Так обстоит дело в бэкэнде prefork в Apache. Когда один экземпляр httpd выходит из строя, в худшем случае конкретный HTTP-запрос может быть отброшен для этого процесса, но Apache порождает нового потомка и часто этот запрос, если его просто повторно отправить и обслужить. Конечным результатом является то, что Apache в целом не снесен с неисправным потоком.
Другое соображение в этом сценарии - утечки памяти. В некоторых случаях вы можете изящно обработать сбой потока (в UNIX возможно восстановление по некоторым специфическим сигналам - даже segfault / fpviolation -), но даже в этом случае вы могли вытечь всю память, выделенную этому потоку (malloc, new и т. д.). Таким образом, несмотря на то, что процесс может работать, он с течением времени теряет все больше и больше памяти при каждом сбое / восстановлении. Опять же, есть до некоторой степени способы минимизировать это, например использование Apache пулов памяти. Но это по-прежнему не защищает от памяти, которая могла быть выделена сторонними библиотеками, которые, возможно, использовал поток.
И, как отмечают некоторые люди, понимание примитивов синхронизации, пожалуй, самая сложная вещь, чтобы по-настоящему понять все правильно. Эта проблема сама по себе - просто правильная логика для всего вашего кода - может быть огромной головной болью. Таинственные тупики могут возникать в самые странные времена, а иногда даже до тех пор, пока ваша программа не будет запущена в производство, что делает отладку еще более сложной. Добавьте к этому тот факт, что примитивы синхронизации часто сильно различаются в зависимости от платформы (Windows и POSIX), и отладка часто может быть более сложной, а также возможность возникновения условий гонки в любое время (запуск / инициализация, время выполнения и завершение работы), программирование с потоками действительно мало для начинающих. И даже для экспертов, по-прежнему мало пощады только потому, что знание потоков само по себе не уменьшает сложность в целом. Кажется, что каждая строка многопоточного кода экспоненциально усугубляет общую сложность программы, а также увеличивает вероятность появления скрытого тупика или странного состояния гонки в любое время. Также может быть очень сложно написать контрольные примеры, чтобы найти эти вещи.
Вот почему некоторые проекты, такие как Apache и PostgreSQL, большей частью основаны на процессах. PostgreSQL запускает каждый внутренний поток в отдельном процессе. Конечно, это все еще не облегчает проблему синхронизации и условий гонки, но она добавляет немного защиты и в некотором смысле упрощает вещи.
Несколько процессов, каждый из которых выполняет один поток выполнения, могут быть намного лучше, чем несколько потоков, запущенных в одном процессе. А с появлением большей части нового однорангового кода, такого как AMQP (RabbitMQ, Qpid и т. Д.) И ZeroMQ, стало намного проще разделять потоки по разным пространствам процессов и даже машинам и сетям, что значительно упрощает работу. Но все же, это не серебряная пуля. Есть еще сложность для решения. Вы просто перемещаете некоторые свои переменные из пространства процесса в сеть.
Суть в том, что решение войти в область потоков не является легким. Как только вы вступаете на эту территорию, почти мгновенно все становится более сложным, и в вашу жизнь входят новые виды проблем. Это может быть весело и круто, но это похоже на ядерную энергетику - когда дела идут плохо, они могут идти плохо и быстро. Я помню, как много лет назад посещал занятия по критике, и они показали фотографии некоторых ученых в Лос-Аламосе, которые играли с плутонием в лабораториях еще во Второй мировой войне. Многие предпринимали мало или вообще никаких мер предосторожности в случае воздействия, и в мгновение ока - одной яркой, безболезненной вспышкой, все это было бы для них закончено. Дни спустя они были мертвы. Ричард Фейнман позже назвал это « щекоткой хвоста дракона».Это похоже на игру с потоками (по крайней мере, для меня, во всяком случае). Сначала это кажется довольно безобидным, и к тому времени, как ты укусишься, почесал голову, как быстро все пошло плохо. Но, по крайней мере, темы победили. не убью тебя.
Во-первых, однопоточное приложение никогда не будет использовать преимущества многоядерного процессора или гиперпоточности. Но даже на одном ядре однопоточный процессор, выполняющий многопоточность, имеет свои преимущества.
Обдумайте альтернативу и сделайте ли это вас счастливым. Предположим, у вас есть несколько задач, которые должны выполняться одновременно. Например, вы должны поддерживать связь с двумя разными системами. Как вы делаете это без многопоточности? Вы, вероятно, создадите свой собственный планировщик и позволите ему вызывать различные задачи, которые необходимо выполнить. Это означает, что вам нужно разделить ваши задачи на части. Возможно, вам необходимо выполнить некоторые ограничения в реальном времени, вы должны убедиться, что ваши детали не занимают слишком много времени. В противном случае таймер истечет в других задачах. Это усложняет задачу разделения задачи. Чем больше задач вам нужно решить, тем больше вам нужно разделиться и тем сложнее станет ваш планировщик, чтобы удовлетворить все ограничения.
Когда у вас есть несколько потоков, жизнь может стать проще. Упреждающий планировщик может остановить поток в любое время, сохранить его состояние и перезапустить другой. Он перезапустится, когда ваша нить получит свою очередь. Преимущества: сложность написания планировщика уже сделана для вас, и вам не нужно разделять свои задачи. Кроме того, планировщик способен управлять процессами / потоками, о которых вы сами даже не подозреваете. А также, когда потоку ничего не нужно делать (он ожидает какого-то события), он не займет циклов ЦП. Это не так легко сделать, когда вы создаете свой однопоточный планировщик. (уложить что-то в сон не так сложно, но как оно просыпается?)
Недостатком многопоточной разработки является то, что вам необходимо понимать проблемы параллелизма, стратегии блокировки и так далее. Разработка безошибочного многопоточного кода может быть довольно сложной. И отладка может быть еще сложнее.
Есть ли что-то, что может быть достигнуто только с помощью нескольких потоков?
Да. Вы не можете запускать код на нескольких процессорах или ядрах процессора в одном потоке.
Без нескольких процессоров / ядер потоки все еще могут упростить код, который концептуально выполняется параллельно, например, обработку клиента на сервере, но вы можете сделать то же самое без потоков.
Темы не только о скорости, но и о параллелизме.
Если у вас нет пакетного приложения, как предлагал @Peter, а вместо этого инструментарий с графическим интерфейсом, например, WPF, как вы можете взаимодействовать с пользователями и бизнес-логикой всего одним потоком?
Также предположим, что вы создаете веб-сервер. Как бы вы обслуживали более одного пользователя одновременно только с одним потоком (не считая других процессов)?
Есть много сценариев, когда одного простого потока недостаточно. Вот почему последние достижения, такие как процессор Intel MIC с более чем 50 ядрами и сотнями потоков, имеют место.
Да, параллельное и параллельное программирование сложно. Но необходимо.
Многопоточность может позволить графическому интерфейсу по-прежнему реагировать во время длительных операций обработки. Без многопоточности пользователь застрял бы, наблюдая за заблокированной формой, пока выполняется длинный процесс.
Многопоточный код может заблокировать логику программы и получить доступ к устаревшим данным так, как это невозможно для отдельных потоков.
Потоки могут извлечь неясную ошибку из того, что может ожидать отладчик среднего программиста, и перенести ее в область, где рассказывают истории об удаче, необходимой для исправления той же самой ошибки, когда она утомлена, когда программист по предупреждению смотрел только на подходящий момент
приложения, работающие с блокировкой ввода-вывода, которые также должны реагировать на другие входы (графический интерфейс или другие соединения), не могут быть однопоточными
В этом может помочь добавление методов проверки в библиотеку ввода-вывода, чтобы увидеть, сколько можно прочитать без блокировки, но не все библиотеки дают полную гарантию на этот счет.
Множество хороших ответов, но я не уверен, что какая-то фраза будет такой же, как я бы - Возможно, это предлагает другой взгляд на это:
Потоки - это просто упрощение программирования, такое как Objects или Actors, или для циклов (Да, все, что вы реализуете с помощью циклов, вы можете реализовать с помощью if / goto).
Без потоков вы просто реализуете движок состояния. Мне приходилось делать это много раз (когда я впервые это делал, я никогда об этом не слышал - просто сделал большой оператор switch, управляемый переменной State). Конечные автоматы все еще довольно распространены, но могут раздражать. С нитями огромный кусок шаблона исчезает.
Также случается, что для языка легче разбить его исполнение во время выполнения на несколько процессоров (я полагаю, что и Actors).
Java предоставляет «зеленые» потоки в системах, где ОС не предоставляет НИКАКОЙ поддержки потоков. В этом случае легче понять, что они явно не более чем абстракция программирования.
ОС использует концепцию среза времени, когда каждый поток получает время для запуска, а затем получает приоритет. Подобный подход может заменить многопоточность в ее нынешнем виде, но написание собственных планировщиков в каждом приложении было бы излишним. Более того, вам придется работать с устройствами ввода-вывода и так далее. И потребует некоторой поддержки со стороны аппаратного обеспечения, чтобы вы могли запускать прерывания, чтобы запустить планировщик. По сути, вы будете писать новую ОС каждый раз.
В общем случае многопоточность может повысить производительность в тех случаях, когда потоки ожидают ввода-вывода или спят. Это также позволяет вам создавать интерфейсы, которые реагируют и позволяют останавливать процессы, пока вы выполняете длинные задачи. А также многопоточность улучшает работу настоящих многоядерных процессоров.
Во-первых, потоки могут делать две или более вещи одновременно (если у вас более одного ядра). Хотя вы также можете сделать это с несколькими процессами, некоторые задачи просто не очень хорошо распределяются между несколькими процессами.
Кроме того, в некоторых задачах есть пробелы, которые вы не можете легко избежать. Например, трудно прочитать данные из файла на диске, а также сделать так, чтобы ваш процесс делал что-то еще одновременно. Если ваша задача обязательно требует много чтения данных с диска, ваш процесс будет тратить много времени на ожидание диска, независимо от того, что вы делаете.
Во-вторых, потоки могут позволить вам избежать необходимости оптимизировать большие объемы кода, который не критичен для производительности. Если у вас есть только один поток, каждый фрагмент кода критичен к производительности. Если это блокирует, вы потоплены - никакие задачи, которые будут выполнены этим процессом, не смогут продвинуться вперед. С потоками, блок будет влиять только на этот поток, и другие потоки могут появиться и работать над задачами, которые должны быть выполнены этим процессом.
Хорошим примером является нечасто выполняемый код обработки ошибок. Скажем, задача сталкивается с очень редкой ошибкой, и код для обработки этой ошибки должен быть помещен в память. Если диск занят, и процесс имеет только один поток, продвижение вперед невозможно, пока код для обработки этой ошибки не будет загружен в память. Это может вызвать бурный ответ.
Другой пример - если вам очень редко приходится искать в базе данных. Если вы ждете ответа от базы данных, ваш код столкнется с огромной задержкой. Но вы не хотите делать асинхронный весь этот код, потому что это настолько редко, что вам нужно выполнять эти поиски. С потоком, чтобы сделать эту работу, вы получаете лучшее из обоих миров. Поток для выполнения этой работы делает его не критичным к производительности, как это должно быть.