Подтвердить что ты не робот

Как многопоточная экономия времени?

Я изучаю threading в С#. Однако я не могу понять, какие аспекты потоков фактически улучшают производительность.

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

4b9b3361

Ответ 1

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

Вы совершенно правы, чтобы скептически относиться к любому заявленному ускорению здесь.

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

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

  • Выполняет 100% работы 1. Предположим, что это занимает 1000 мс.
  • Выполняйте 100% работы задания 2. Предположим, что это занимает 1000 мс.

Общее время: две секунды. Всего выполненных работ: два. Но вот важный бит: клиент, который ждал задания 1, выполнил свою работу за одну секунду. Клиент, ожидавший задания 2, должен был ждать две секунды.

Теперь, если у нас есть два потока и один процессор, он выглядит следующим образом:

  • Выполняйте 10% работы задания 1 за 100 мс.
  • Сделайте 10% работы задания 2 за 100 мс.
  • Выполнять 10% работы 1
  • Сделайте 10% работы 2 ...

Опять же, общее время в две секунды, но на этот раз клиент, который ждал задания 1, выполнил свою работу за 1,9 секунды, почти на 100% медленнее, чем однопоточный сценарий!

Итак, мораль этой истории здесь, что вы совершенно правильно указываете. Если выполнены следующие условия:

  • Задания привязаны к ЦП
  • Есть больше потоков, чем процессоры.
  • Работа полезна только для ее конечного результата.

Затем добавление большего количества потоков только замедляет вас.

Библиотеки, такие как параллельная библиотека задач, предназначены для этого сценария; они пытаются понять, когда добавление большего количества потоков ухудшит ситуацию, и попытайтесь установить только столько потоков, сколько есть процессоров для их обслуживания.

Теперь, если какое-либо из этих условий не выполняется, добавление большего количества потоков - хорошая идея.

  • Если задания не связаны с ЦП, то добавление большего количества потоков позволяет процессору работать, если в противном случае он был бы бездействующим, ожидая сети или диска.

  • Если есть свободные CPU, тогда добавление большего количества потоков позволяет планировать эти CPU.

  • Если результаты с частичным вычислением полезны, то добавление большего количества потоков улучшает ситуацию, потому что у клиентов больше возможностей для использования частично вычисляемых результатов. Например, во втором сценарии клиенты обоих заданий получают частичные результаты каждые 200 миллисекунд, что справедливо.

Ответ 2

В одном ядре CPU преимущество, которое вы получаете, - это асинхронность. Использование потоков - один из способов достижения этого (хотя и не единственный способ).

Представьте себе процесс приготовления еды. Как вы думаете, что быстрее:

  • Начните немного кипения воды. Подождите, пока он закончит.
  • Добавьте немного лапши. Подождите, пока они будут готовы к приготовлению.
  • Вымойте/приготовите некоторые овощи.
  • Жарьте овощи.
  • Наденьте тарелку и подавайте.

Или вместо этого:

  • Начните с кипения воды.
  • Пока вода кипит, мытье/приготовление некоторых овощей.
  • Добавьте немного лапши в горшок с кипящей водой.
  • Жарьте овощи во время приготовления лапши.
  • Наденьте тарелку и подавайте.

Из моих переживаний вторая быстрее.

Общая идея здесь заключается в том, что во многих ситуациях при программировании у вас будет операция, которая занимает некоторое время, но она не требует завершения работы с CPU. Общим примером является IO. Когда вы отправляете запрос в базу данных, чтобы получить некоторую информацию, которую обычно нужно делать, пока вы ждете ответа на этот запрос. Возможно, вы можете отправить несколько запросов, а затем дождаться их завершения, а не запускать их, ожидая его, затем начать следующий, ждать и т.д. (Хотя иногда вам приходится делать последние).

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

Ответ 3

Большинство комментариев, которые у вас есть, верны, но я также выброшу свои два цента (и перечислите здесь комментарии):

Jonesy: "Threading наиболее эффективна в многоядерных средах" → Да, но это одноядерный процессор... поэтому я вернусь к этому.

KooKiz и Джон Сибли: Они оба упоминают ввод/вывод. Ваша машина не хрустеть на полную мощность в 100% случаев. Происходит довольно много других событий, которые занимают много времени, и во время этих событий ваш процессор получает перерыв.

(точка привязки: I/O может быть передачей по сети, считыванием жесткого диска /RAM, SQL-запросом и т.д. Все, что приводит к новым данным в CPU или выгружает данные из ЦП)

Эти перерывы - это время, когда ваш процессор может делать другие вещи. Если у вас есть один процессор ядра (в настоящий момент мы будем игнорировать гиперпоточность) и однопоточное приложение, тогда он работает как можно дольше. Однако он не работает постоянно. Планирование CPU даст ему цикл или два, затем перейдет к чему-то другому, а затем через некоторое время вернется к вашей программе, дайте ему еще несколько циклов, перейдите и т.д. Это дает ИЛЛЮЗИИ возможность делать "множественные вещи сразу" на одном ядре процессора.

Теперь, поскольку это обычная программа, а не какая-то безумно маленькая программа сборки, в которой вы непосредственно записываете значения в кеш, ваша программа хранит данные в ОЗУ... относительно медленный носитель данных по сравнению с кэшем процессора. Из-за этого значения загрузки требуют времени.

В течение этого времени возможно, что вашему процессору нечего делать. Здесь вы можете увидеть ускорение в многопоточном приложении даже на одном ядре. Другой поток заполнит эти дополнительные циклы CPU, где CPU в противном случае был бы простаивающим.

Обратите внимание, что маловероятно, что вы увидите ускорение 2: 1. Гораздо более вероятно, что ваша 2-поточная программа увидит только повышение скорости на 10-20%, если это произойдет. Помните, что "другой" поток (который в любой заданной точке является потоком, который НЕ выполняет ввод-вывод) будет действительно работать только на полную мощность, а первый поток выполняет ввод-вывод.

Часто, однако, вы можете увидеть время WORSE. Это связано с тем, что ваш процессор теперь должен тратить больше времени на переключение между потоками в вашем процессе (помните, что мы можем запускать только одно за раз!). Это называется накладными расходами. Второй поток создает больше накладных расходов, чем может компенсировать, и поэтому процесс в целом замедляется.

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

Тогда, конечно, у вас есть многопроцессорные программы, которые выполняются над кластером, но мы сохраним это в другое время.

Ответ 4

Если вычисление делится на параллельные потоки управления, это изменяет время обработки.

Пример 1: потоки ухудшают

Предположим, что мы хотели бы выполнить два расчета, которые занимают 10 минут каждый.

Если мы планируем это последовательно (без многопоточности), то через 10 минут мы получим результат от одного вычисления, а через 10 минут получим результат от другого вычисления.

Если промежуток времени между вычислениями, тогда нам придется подождать 20 минут, после прохождения которых мы получим оба результата.

Пример 2: потоки делают его лучше

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

Если мы просто запускаем два задания один за другим, может случиться так, что сначала запланировано 59-минутное задание. Итак, мы должны подождать 59 минут для одного результата, а затем еще одну минуту для второго результата. В основном час ожидания обоих результатов.

Если нам повезет, мы сначала запускаем более короткую работу и получаем первый результат за 1 минуту, а второй через 59 минут: гораздо лучшее среднее время обработки.

Но предположим, что мы занимаем время между заданиями с потоками. Затем мы получаем результаты первой работы за 2 минуты, а вторую - через 58 минут. Это почти так же хорошо, как и второй сценарий, но без необходимости предсказать, какая работа будет короткой.

Threading с временным разделением для чисто задач, связанных с CPU, помогает избежать патологических случаев, когда действительно большая работа задерживает все остальное за полный рабочий день, требуемый для завершения этой большой работы.

Ответ 5

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

Основной смысл потоковой передачи - многозадачность с доступными ресурсами, как отмечает KooKiz, так же, как и использование оставшегося времени процессора, когда доступно. Но вы правы, могут быть случаи, когда использование потоковой обработки не улучшит время выполнения.

Однако даже для одноядерных систем существуют случаи, когда многопоточность может повысить производительность. Когда один процесс ждет чего-то, он не блокирует другие процессы, запущенные в тандеме. В зависимости от того, как долго происходит ожидание, ваше одиночное ядро ​​может перепрыгнуть между другими независимыми процессами, чтобы в целом сэкономить время.

Ответ 6

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

Но общее время процессора составляет лишь половину истории...

Fluid UI

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

Обработка, не связанная с ЦП

Кроме того, не вся обработка связана с ЦП. Если вы делаете что-то вроде чтения файла, доступа к базе данных или вызова веб-службы, поток будет заблокирован (а ЦПУ недоиспользуется), ожидая внешнего ресурса. Если есть другие потоки, которые должны выполнять некоторую работу, они могут использовать циклы процессора, пока первый поток заблокирован.

TPL

В случае С# вы, вероятно, захотите использовать параллельную библиотеку задач для concurrency (и для асинхронности), вместо того, чтобы сами пытаться управлять нитями низкого уровня. По умолчанию, Task будет запланировано в пуле потоков, избегая опасности слишком большого количества потоков (и контекстных переключателей).

Посмотрите Параллельное программирование с Microsoft.NET для получения дополнительной информации.