Как можно избежать потоков? - PullRequest
6 голосов
/ 20 декабря 2008

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

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

Я знаю, что Google MapReduce должен помочь с этой проблемой, но я не видел краткого объяснения этого.

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

Я пришел к этому вопросу, когда написал простой веб-сканер в качестве учебного упражнения. Это работает довольно хорошо, но это медленно. Большая часть узких мест связана с загрузкой страниц. В настоящее время он является однопоточным и, следовательно, загружает только одну страницу за раз. Таким образом, если страницы можно загружать одновременно, это значительно ускорит процесс, даже если сканер будет работать на одном процессоре. Я решил использовать темы, чтобы решить проблему, но они меня пугают. Любые предложения о том, как добавить параллелизм к этому типу проблемы, не развязывая ужасный ночной кошмар?

Ответы [ 14 ]

22 голосов
/ 20 декабря 2008

Функциональное программирование помогает в параллелизме не потому, что оно избегает использования потоков.

Вместо этого функциональное программирование проповедует неизменность и отсутствие побочных эффектов.

Это означает, что операция может быть масштабирована до N потоков или процессов, не беспокоясь о том, чтобы связываться с общим состоянием.

9 голосов
/ 20 декабря 2008

На самом деле потоки довольно просты в обращении, пока вам не потребуется их синхронизировать. Обычно вы используете пул потоков, чтобы добавить задачу и подождать, пока они не будут завершены.

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

8 голосов
/ 20 декабря 2008

Я добавлю пример того, как функциональный код можно использовать для безопасного одновременного выполнения кода.

Вот некоторый код, который вы могли бы сделать параллельно, поэтому вам не нужно ждать, пока один файл завершит загрузку следующего:

void DownloadHTMLFiles(List<string> urls)
{
    foreach(string url in urls)
    {
         DownlaodOneFile(url);  //download html and save it to a file with a name based on the url - perhaps used for caching.
    }
}

Если у вас есть несколько файлов, пользователь может потратить минуту или больше, ожидая их всех. Мы можем функционально переписать этот код следующим образом, и он в основном делает то же самое:

urls.ForEach(DownloadOneFile);

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

Оказывается, в .Net такая функция доступна только при использовании Параллельные расширения . Таким образом, используя функциональное программирование, вы можете изменить одну строку кода и внезапно запустить что-то параллельно, что раньше выполнялось последовательно. Это довольно мощно.

4 голосов
/ 20 декабря 2008

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

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

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

Другой пример из реальной жизни - система, которая должна была выполнить ряд других исполняемых файлов и ожидать результатов. Это может быть сделано в потоках, но также значительно проще и почти так же эффективно просто запустить несколько внешних приложений, как объекты Process, а затем периодически проверять, пока все они не завершат выполнение. Это помещает части с интенсивным использованием ЦП (исполняемый код во внешние исполняемые файлы) в свои собственные процессы, но обработка данных обрабатывается асинхронно.

lib ftp-сервер Python, над которым я работаю, pyftpdlib использует асинхронную библиотеку Python для обработки обслуживающих клиентов FTP только с одним потоком и асинхронного обмена сокетами для передачи файлов и команды / ответа.

См. Для дальнейшего чтения страницу библиотеки Python Twisted на Асинхронное программирование - хотя это и несколько специфично для использования Twisted, оно также вводит асинхронное программирование с точки зрения новичка.

1 голос
/ 19 февраля 2009

Есть несколько хороших библиотек.

java.util.concurrent.ExecutorCompletionService будет принимать коллекцию Futures (то есть задач, возвращающих значения), обрабатывать их в фоновых потоках, а затем помещать их в очередь для дальнейшей обработки по мере их завершения. , Конечно, это Java 5 и новее, поэтому не везде доступно.

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

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

1 голос
/ 20 декабря 2008

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

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

1 голос
/ 20 декабря 2008

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

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

1 голос
/ 20 декабря 2008

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

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

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

Полагаю, лучшее место для начала - статья в википедии о параллелизме и далее оттуда.

0 голосов
/ 27 декабря 2008

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

Метод выполнения - через средство select () или эквивалентный системный вызов. Основной процесс заключается в открытии нескольких сокетов (для загрузок веб-сканера) и файловых дескрипторов (для их хранения на диске). Затем вы устанавливаете все разные сокеты и fh в неблокирующий режим. Это означает, что вместо того, чтобы заставить вашу программу ждать, пока данные не станут доступны для чтения после отправки запроса, она сразу же возвращает специальный код (обычно EAGAIN), чтобы указать, что данные не готовы Если вы перебрали все сокеты таким образом, вы будете опрашивать, что работает хорошо, но все еще является пустой тратой ресурсов процессора, потому что ваши операции чтения и записи почти всегда возвращаются с EAGAIN.

Чтобы обойти это, все сокеты и fp будут собраны в 'fd_set', который передается в системный вызов select, затем ваша программа заблокируется, ожидая ЛЮБОГО из сокетов, и пробудит вашу программу когда есть какие-либо данные по любому из потоков для обработки.


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

0 голосов
/ 20 декабря 2008

Используйте Twisted. «Twisted - это управляемый событиями сетевой движок, написанный на Python» http://twistedmatrix.com/trac/. С его помощью я могу делать 100 асинхронных HTTP-запросов одновременно, не используя потоки.

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