Pull to refresh

Как избавиться от пристрастия к синхронности

Reading time 6 min
Views 5.6K
Original author: Eric Florenzano
При сравнении асинхронное программирование превосходит синхронное, как по потреблению памяти, так и по производительности. Мы знакомы с этим фактом уже годы. Если посмотреть на Django или Ruby on Rails, возможно два самых многообещающих веб-фреймворка, появившихся за последние несколько лет, оба написаны из расчета на синхронный стиль. Почему даже в 2010 году мы пишем программы, полагающиеся на синхронное программирование?

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

Асинхронное программирование слишком сложно



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

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

У нас есть несколько действительно хороших фреймворков, нацеленных на то, чтобы сделать работу с циклами обработки событий проще. В Python это Twisteв, и, несколько новее, Tornado. В Ruby есть EventMachine. В Perl есть POE. То, что эти фреймворки делают двояко: предоставляют конструкции для более простой работы с циклом событий (как например, Задержки (Deferreds) или Обещания (Promises), и предоставляют асинхронные реализации обычных задач, к примеру, HTTP или DNS клиенты.

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

function handleBlogPostRequest(request, response, postSlug) {
    var db = new DBClient();
    var post = db.getBlogPost(postSlug);
    var comments = db.getComments(post.id);
    var html = template.render('blog/post.html',
        {'post': post, 'comments': comments});
    response.write(html);
    response.close();
}


А теперь кусок кода, демонстрирующий, как это может быть в асинхронном фреймворке. Необходимо сразу отметить несколько вещей: код специально написан так, чтобы не требовалось 4 уровня вложенности. Мы так же написали колбеки внутри handleBlogPostRequest, чтобы получить преимущества замыканий, такие, как доступ к объектам запроса и ответа, контексту шаблона, и клиенту базы данных. Как избежать вложенности и замыкания — это то, чем мы должны думать, пока пишем такой код. Но это даже не подразумевается в синхронной версии.

function handleBlogPostRequest(request, response, postSlug) {
    var context = {};
    var db = new DBClient();
    function pageRendered(html) {
        response.write(html);
        response.close();
    }
    function gotComments(comments) {
        context['comments'] = comments;
        template.render('blog/post.html', context).addCallback(pageRendered);
    }
    function gotBlogPost(post) {
        context['post'] = post;
        db.getComments(post.id).addCallback(gotComments);
    }
    db.getBlogPost(postSlug).addCallback(gotBlogPost);
}


Между прочим, я выбрал JavaScript чтобы показать точку зрения. Люди сейчас очень довольны node.js, и это очень клевый фреймворк, но он не скрывает всей сложности тянущейся за асинхронностью. Он только прячет некоторые детали реализации цикла событий.

Вторая причина почему эти фреймворки не достаточно хороши — то, что не весь I/O может быть обработан должным образом на уровне фреймворка, и в этом случае надо обращаться к хакам. К примеру, MySQL не предоставляет асинхронных драйверов, так что большинство известных фреймворков используют нити (threads), чтобы быть уверенным, что эта коммуникация будет работать из коробки.

Полученный неудобный API, добавочная сложность, и простой факт, что большинство разработчиков не меняют свой стиль кодирования, приводит нас к заключению, что этот тип фреймвокров не есть желанное финальное решение проблемы (я допускаю мысль, что вы может выполнить Реальную Работу сегодня используя эти техники, как и уже многие программисты). Это приводит нас к размышлению: а какие другие варианты у нас есть для асинхронного программирования? Сопрограммы (сoroutines) и легковесные процессы, что приводит нас к новой важной проблеме.

Языки не поддерживают более легких асинхронных парадигм



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

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

Вот пример (на Питоне, но я думаю понятно):
def download_pages():
    google = urlopen('http://www.google.com/').read()
    yahoo = urlopen('http://www.yahoo.com/').read()


Обычно это работает так: новый сокет открывается, подключатся к Google, HTTP заголовок отправляется, полный ответ считывается, буфферизуется и назначается переменной google. Затем тоже самое для переменной yahoo.

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

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

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

Затем строчка с Yahoo вернет все свои данные.

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

Языки как PHP, Python, Ruby, Perl, просто не имеют встроенных сопрограмм достаточно быстрых для фоновой реализации такой трансформации. Так что там с лекговесными процессами?

Легковесные процессы это, то что Erlang использует как основной примитив многопоточности. По существу эти процессы по большей мере реализованы в Erlang VM. Каждый процесс имеет примерно 300 слов избыточности(overhead) и его выполнние планируется, главным образом, в Erlang VM, не разделяя состояние между всеми процессами. По сути, нам не нужно задумываться о создании процесса, это практически бесплатно. Уловка в том что все процессы могут взаимодействовать только посредством передачи сообщений.

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

С этой моделью легковесных процессов возможно опять вернуться к общепринятой модели использования разных процессов для всех наших асинхронных нужд. Вопрос становится следующим: может ли понятие легковесного процесса быть реализовано на языках помимо Erlang? Ответ: «Я не знаю.» Как я считаю, Erlang использует некоторые особенности языка (такие, как отсутствие изменяющихся структур данных — Прим. ред.: в нём нет переменных) для своей релизации легковесных проецессов.

И куда же двигаться дальше



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

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

Прим. пер.: Между тем, сопрограммы уже активно используются. По крайней мере в питоне:
Tags:
Hubs:
+52
Comments 129
Comments Comments 129

Articles