Многопоточность в .NET 4.0 и производительность

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

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

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

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

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

person Chris    schedule 12.01.2010    source источник


Ответы (3)


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

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

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

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

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

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

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

person Simon P Stevens    schedule 12.01.2010
comment
Немного добавляя к тому, что сказал Саймон, оптимальное количество потоков трудно найти, поскольку оно часто зависит от загрузки системы в целом и того, что делает ваш код, мой лучший совет - поэкспериментировать с высокой нагрузкой, чтобы увидеть, что дает лучшее выступление. - person Lazarus; 12.01.2010
comment
@Лазарь. Да, я полностью согласен, это очень верно. И это может быть очень специфично для системы. Это то, что вы, вероятно, захотите каким-то образом выставить в качестве настроек, чтобы их можно было настраивать в каждом конкретном случае с некоторыми хорошими значениями по умолчанию. - person Simon P Stevens; 12.01.2010

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

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

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

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

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

person Kevin Jones    schedule 12.01.2010

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

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

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

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

person Eamon Nerbonne    schedule 12.01.2010