Многопоточность в .NET 4.0 и производительность - PullRequest
3 голосов
/ 12 января 2010

Я играл с библиотекой Parallel в .NET 4.0. Недавно я разработал собственный ORM для некоторых необычных операций чтения / записи, которые должна использовать одна из наших больших систем. Это позволяет мне украшать объект с помощью атрибутов и позволяет понять, какие столбцы он должен извлечь из базы данных, а также какой XML он должен выводить при записи.

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

Теперь я усвоил урок, связанный с многопоточностью. Многопоточность заставляет его работать медленнее. Из прочтения кажется, что он интуитивно понятен людям, которые занимаются этим в течение длительного времени, но на самом деле он мне не понятен: как может медленный запуск метода 30 раз одновременно чем запустить его 30 раз подряд?

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

  • Хотя я делаю это в основном как учебное упражнение, это пессимизация? Для тривиальных задач, не связанных с вводом-выводом, многопоточность излишня? Моя главная цель - скорость, а не отзывчивость интерфейса или что-то еще.
  • Будет ли выполнение того же многопоточного кода в IIS причиной его ускорения из-за уже созданных потоков в пуле потоков, тогда как сейчас я использую консольное приложение, которое, как я предполагаю, будет однопоточным, пока я не сообщу об этом иначе? Я собираюсь провести некоторые тесты, но я полагаю, что есть некоторые базовые знания, которые мне не хватает, чтобы знать , почему это будет так или иначе. Мое консольное приложение также работает на моем настольном компьютере с двумя ядрами, в то время как сервер для веб-приложения будет иметь больше, поэтому мне, возможно, придется использовать это и в качестве переменной.

Ответы [ 3 ]

8 голосов
/ 12 января 2010

Все потоки на самом деле не работают одновременно.

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

Если вы создали 30 потоков, ОС придется переключать контекст между этими 30 потоками, чтобы все они работали. Переключение контекста довольно дорого, поэтому замедление.

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

Некоторые из новых функций в библиотеке параллельных задач .net 4.0 позволяют вам делать вещи, которые учитывают масштабируемость в количестве потоков. Например, вы можете создать кучу задач, и библиотека параллельных задач сама определит, сколько процессоров у вас есть, и оптимизирует количество создаваемых / используемых потоков, чтобы не перегружать процессоры, чтобы вы могли создать 30 задач, но на двухъядерной машине библиотека TP все равно будет создавать только 2 потока и ставить в очередь. Очевидно, это очень хорошо масштабируется, когда вы запускаете его на более крупной машине. Или вы можете использовать что-то вроде ThreadPool.QueueUserWorkItem(...), чтобы поставить в очередь кучу задач, и пул будет автоматически управлять тем, сколько потоков используется для выполнения этих задач.

Да, создание потоков занимает много времени, но если вы используете пул потоков .net (или библиотеку параллельных задач в 4.0), .net будет управлять созданием потоков, и вы можете обнаружить, что он создает меньше потоков, чем количество задач, которые вы создали. Это внутренне поменяет ваши задачи на доступные потоки. Если вы действительно хотите контролировать явное создание реальных потоков, вам необходимо использовать класс Thread.

[Некоторые процессоры могут делать умные вещи с потоками и могут иметь несколько потоков, работающих на одном процессоре - см. Гиперпоточность - но посмотрите ваш менеджер задач, я был бы очень удивлен, если у вас более 4- 8 виртуальных процессоров на современных компьютерах]

2 голосов
/ 12 января 2010

Есть так много проблем с этим, что стоит понять, что происходит под одеялом. Я очень рекомендую книгу Джо Даффи «Параллельное программирование в Windows» и книгу «Практический параллелизм Java». Последний говорит об архитектуре процессора на уровне, который вам необходимо понимать при написании многопоточного кода. Одной из проблем, с которой вы столкнетесь, которая повредит ваш код, является кеширование, или, скорее всего, его отсутствие.

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

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

Мой коллега записал скринкаст о проблеме производительности с Parallel.For и Parallel.ForEach, который может помочь:

http://rocksolidknowledge.com/ScreenCasts.mvc/Watch?video=ParallelLoops.wmv

1 голос
/ 12 января 2010

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

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

Но это только предположение; невозможно реально определить причину замедления, не зная больше.

Хотя создание потоков «чрезвычайно дорого» по сравнению с добавлением двух чисел, обычно это не то, что вы легко переусердствуете. Если ваши операции очень короткие (скажем, миллисекунды или меньше), использование пула потоков, а не новых потоков, заметно сэкономит время. Однако, как правило, если ваши операции такие короткие, вам все равно следует пересмотреть гранулярность параллелизма; возможно, вам лучше разделить вычисления на более крупные куски: например, имея довольно небольшое количество рабочих задач, которые обрабатывают целые партии меньших рабочих элементов за раз, а не каждый элемент в отдельности.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...