Главная

Dianne Hackborn o YellowTab и BMessage

Мы приводим перевод комментариев Дианны Хэкборн — одного из лучших инженеров ныне не существующей Be Inc, на статью YellowTab «Send messages in a post PostMessage() API»

В результате у нее получилась целая статья в стиле старых легендарных BeNewsLetter, посвященная одному из оснований BeOS — BMessage. Может использоваться нашими читателями в качестве дополнения/апгрейда к статье Стаса Максимова «Используем BMessage в мирных целях»

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

С.Д.
----------------------------------------–


    «Не могу удержаться. Эххх.

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

    («Мы» здесь в значительной степени означает «я». Дианна была именно тем человеком, кто переписывал код, связанный с сообщениями. — С.Д.)

    К моему сожалению мне так и не пришлось написать статьи об этом новом коде, хотя бы ради заголовка «BMessage: Быстрее, длиннее, полнее», так что изложу кое–что здесь. (Ну хорошо, признаюсь, еще не могу удержаться по причине моего возрастающего возмущения утверждениями (От YellowTab про «их» OS Zeta — C.Д.) о том, как много «нового» этих релизах BeOS, в том время как подавляющая часть этой работы была в действительности сделана в BeInc)

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

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

    Так что отсылка сообщения обработчику (handler), все равно, каким путем это делалось, предполагала «выравнивание» (flattening) сообщения, запись выравненного сообщения в порт, затем, в другом потоке, чтение и реконструкцию. Это происходило как для локальных (в пределах одного приложения) так и для межпроцессных сообщений. Кроме очевидных проблем с производителностью, это приводило и более критической проблеме — размер порта BLooper (класс — диспетчер/приемник сообщений, основа BApplication и BWindow — C.Д.) был ограничен 100 сообщениями, поэтому во многих ситуациях порт очень быстро переполнялся, что приводило к потере сообщений.

    Для решения проблем с производительностью мы полностью переписали код BMessage. В EXP(Dano) данные сразу сохраняются в выравненной форме.
    Поэтому отпадает необходимость операций декомпозиции/реконструкции на передаче/приеме. (В заголовочных файлах для BMessage в EXP есть новые функции, как можно увидеть — WritePort() и ReadPort().) Это намного эффективнее даже при запаковке сообщения в файл или буфер, так как по сути происходит просто вызов memcpy().

    Естественно, это значит, что выравненный формат BMessage изменился — и это очень важно знать при использования EXP(Dano). Поскольку любое приложение, сохраняющее сообщения на диске или отсылающее его через сеть, создаст данные, нераспаковываемые предыдущими версиями BeOS. Новый выровненный формат больше в размерах, так как он рассчитан на прямую эффективную обработку прямо в памяти (по сути, как должно быть понятно из предыдущего, реальной упаковки не происходит — C.Д.). Во многих местах происходит просто дополнение размеров полей сообщения до 64–битного «слова».

    В новом BMessage API есть методы, позволяющие упаковывать сообщения в старый формат (а также распаковывать сообщения, записанные в старом формате), однако по умолчанию все делается в новом формате. Если бы мы когда–нибудь дошли до релиза этой версии BeOS, я бы прошлась по всему коду BeOS, чтобы использовать новый формат, где это необходимо, но при этом вернуть умолчание к старому формату.

    Кроме того, производительность был улучшена за счет того, что теперь поля в BMessage отсортированы по имени, поэтому поиск нужного поля заметно более убыстрился. ( Для тех кто понимает в алгоритмах сортировки — теперь это O(log(n)) — что намного лучше чем O(n) в старом коде). Кроме того, используется семантика copy-on–write, так что в действительности происходит только игра со ссылками, при которой ничего реально не копируется до тех пор, пока оригинал или копия не будут модифицированы. Статья в YellowTab вводит в заблуждение в этом отношении.

    Наконец, существует особенность в рассылке сообщений диспетчерам (loopers). В коде EXP для локальных сообщений все теперь происходит совершенно по–другому — есть возможности прямого добавления сообщений в в очередь сообщений BLooper–а из любого потока (того же самого пучка/team). Так что теперь при вызове SendMessage() для локального обработчика происходит просто копирование сообщения с добавлением его прямо (без использования портов) в BMessageQueue, откуда поток данного обработчика его может и вытащить. А в связи с семантикой copy-on–write, это практически всегда означает даже отсутствие реального копирования (данных). Приктически, при передаче большого числа сообщения (очень важно с точки зрения производительности) все это дело происходит без единого обращения к ядру.

    Так как теперь отпала необходимость работы с портами для локальных сообщений, исчезли и все проблемы с их пересылокй. По сути, единственный случай неудачи в пересылке локальных сообщений — это запредельный случай, когда система не сможет выделить жалкие 60 байтов памяти, необходимых для нового объекта BMessage (ну плюс еще очевидный случай, когда адресата еще/уже не существует).

    Естественно, это не решило проблем с пересылкой сообщений между процессами. У меня были планы на этот счет, но я не успела. Так что тут вы по–прежнему должны быть осторожны.

    Также были добавлены очень полезные методы в API BMessenger для отправки сообщений «по графику». Это позволяет послать сообщение обработчику таким образом, что оно проявится в строго назначенное время в будущем. Крайне удобно для таких вещей, как анимация, помигивание курсора, слежение за нажатой кнопкой и т. д. В связи с этим в BView APIs появились методы DelayedInvalidate() и InvalidateAtTime(), основанные на новых возможностях BMessage.

    Но я действительно не понимаю (этой статьи YellowTab) в смысле запрета на использования PostMessage(). В действительности этот метод всего лишь прозрачным образом создает временный конструктор BMessenger (с соответствующими BLooper и BHandler в качестве параметров конструктора) и вызывает SendMessage() из этого временного BMessager. Да, конечно, в PostMessage() имеется код, который может привести к обращению в неактуальную область памяти, если к этому времени обработчик–адресат был уже разрушен…но такой же кот имеется и в самом BMessenger. На практике это не должно создавать каких–либо проблем.
    (Если кто не прочитал «статью» на сайте YellowTab — там рекомендуют отказаться от использования PostMessage и всегда использовать «явный» путь — специально создавать BMessanger и отсылать сообщения с его помощью. С весьма смутными пояснениями причин сего совета. — С.Д.)

    Что я однако рекомендую — это использовать постоянные объекты BMessenger вместо голых указателей на обработчики (с созданием/удалением BMessenger при каждой необходимости отсылки сообщения), поскольку создание BMessnger из BHandler (и его производных классов) — довольно дорогостоящая операция. (При всей той оптимизации, что была сделана в EXP(Dano), cоздание BMessenger затратнее реального процесса отсылки сообщений.)

    Теперь об этой фразе из статьи YellowTab: «Другая приятная вещь в BMessenger, это то, что происходит отправка BMessage с использованием ссылки, а не указателя.» Гмм, привет. Я вообще–то и вправду добавила функции, использующие синтаксис ссылок, когда я работала в Be Inc. Они работают точно так же, как и те, что используют указатели, однако при использовании методики copy-on–write BMessage работать с простыми объектами (вроде BRect) удобнее. Удобнее в том смысле, что разработчику теперь не надо самому возится с размещением/удалением объектов… однако было бы очень трудно распространить новые возможности BMessage на весь API без отказа от обратной совместимости. (Не говоря уж о всем необъятном API, который по–прежнему использует «BMessage*" вместо хотя бы «const BMessage*".) В любом случае, используете вы ссылки или указатели в качестве параметров в новом BMessage API, на деле внутри все происходит совершенно одинаково.

    (Маленькое пояснение для пришельцев как из С, так и из Java, c их противоположными, хотя и последовательными подходами.
    В языке С при передаче объекта в фунцию создается его копия. Поэтому, если есть необходимость внутри функции изменять сам первоначальный объект, приходится передавать указатель на него. Одна из причин головоломности С для начинающих.
    В С++ введена возможность работы со ссылками — то есть синтаксически все выглядит для ссылочной переменной как работа с «обычной» переменной, со всеми ее удобствами, без необходимости на каждом шагу использовать "*", “&" или “–>", но при этом действительно работать с оригинальным объектом при передаче его в функцию — С.Д.
    )

    Ну и в заключение… “majik mumbo jumbo” (цитата из статьи YT про загадочные скрытые возможности BMessenger — C.Д.)? Охх, я пока присяду. Ребятки, если у вас есть исходники, взгляните на src/inc/app_p/token.h. У вас бы заняло не больше абзаца объяснить как в действительности работает токен обработчика — я уверена, что народу это было бы интересно, при том что это в самом деле очень просто. Я даже переписала это дело заново, когда работала в Be Inc, так что теперь этот код прост и легок для понимания.

    В общем, посмотрим… что я поняла из этой статьи, так это то, что YellowTab изменили заголовочный файл Looper.h таким образом, что метод PostMessage() невидим до тех пор, пока его специально не разрешишь при помощи #define. Крайне сомнительной ценности изменение. Yay, YellowTab!

    –- Dianne, которая несомненно пожалеет о содеянном на следующее утро."

----------------------------------------------------------–

I can't hold myself back. *sigh*

At Be after R5 we completely rewrote almost all of the user–space messaging code. Given that Zeta apparently comes from a snapshot that includes these extensive messaging changes, this article is incredibly superficial. At best.

And since I was somewhat disappointed that I never got to write a newsletter article about the new message code — if for no other reason than to use the title “BMessage: Faster, Longer, and Uncut” — here is some real information. (And okay, I admit it, I also can't resist writing this because I am increasingly annoyed at how much stuff is mentioned being in these “new” releases of BeOS, when the vast majority of it is actually work that was done at Be.)

First you need to know about all of the messaging problems that existed in R5, which the rewrite was intended to address.

An old BMessage stored its data in a linked list, one entry in the list for each named field in the message. This had a lot of performance problems: finding a field involved a linear search through the message; copying a message required copying the entire linked listed; sending a message through a port required flattening that linked list into a (small and thus inefficient to manipulate) flattened representation, and on the other side parsing that representation from which a new data list is generated.

Sending a message to a handler — no matter how you did it — required flattening the message, writing that flattened representation into a port, and in the other thread reading and reconstructing the message. This happened for both local and cross–process message delivery. Besides the obvious performance issues, this had an even more significant problem: the looper's port size was set to 100 messages, so in many situations it was very easy to swamp the port and as a result start dropping messages.

In order to address the performance problems, we completely rewrote the BMessage code. In the EXP code base, it stores its data in its flattened form; as a result, the object is able to read and write itself from a port without any intermediate flattening or copies at all. (If you look at the EXP BMessage, you will see the new functions WritePort() and ReadPort().) Even when flattening the message to a buffer or file it is significantly more efficient, since it essentially boils down to a memcpy().

Of course this means that BMessage's flattened format has changed — which is a very important thing to be aware of when using EXP, because it means that any applications that store flattened messages on disk or send them across a network will be generating data that previous versions of the OS can't unflatten. The new flattened format is quite a bit larger than the old one, because it is designed to be efficiently accessed directly in memory — so there is no data packing, and in fact padding is used in various places to make data line up on nice 8–byte boundaries.

There are some new BMessage APIs that allow you to request flattening in the old format (and the new BMessage can of course unflatten the old format), but the default is to write in the new format. In fact if we had ever gotten closer to releasing this OS, I probably would have gone through the BeOS code to explicitly request the new format where appropriate, and changed the default back to the old format.

Another performance improvement is that the new BMessage sorts its data by name, so looking up a field can use a binary search. (An O(log(n)) operation — much better than O(n) of the old message code.) In addition the message data uses copy-on–write semantics, so copying a message only involves twiddling a reference count, with nothing actually copied until one of the two messages is modified. The Zeta article is misleading in this in that BMessage doesn't use general “reference counting” per–se, but specifically implements copy-on–write. (And the copy itself is of course just a simple malloc() and memcpy().)

Finally there is the issue of posting messages to loopers. In EXP the code path for local message delivery was completely rewritten — new facilities were added to be able to directly enqueue a message in a BLooper's message queue from any thread. So now when you SendMessage() to a local handler, all that happens is a copy of your message is added directly to the looper's BMessageQueue, from that looper thread will pick it up. And because of our copy-on–write semantics, this almost always means no copying at all of the data. In fact, when delivering a lot of messages (which is when performance really matters), the message delivery will usually not require any kernel calls at all.

Because we no longer go through the port for local messages, there are no longer any problems being able to deliver local messages. In fact the only way local message delivery can fail (beyond normal problems such as the handler no longer existing) is if the system is unable to allocate the 60 bytes or so it needs for a new BMessage object.

Of course this doesn't address message delivery problems when going across processes, in which case we still must send the message data through the port. I did have some plans to address this, but never got around to it. It's something you still need to be careful about.

One really useful new feature in EXP are some new APIs on BMessenger to deliver timed events. This allows you to post a message to a handler, where that message will actually show up at some time you pick in the future. This is incredibly useful when you need to do some easy timing for things like simple animations, flashing a cursor, detecting a button hold, etc. There are new BView APIs, DelayedInvalidate() and InvalidateAtTime(), that are simple implementations on top of timed messages.

I don't really understand all the stuff about removing PostMessage(). In fact almost all that function does is create a temporary BMessenger (with the constructor that takes a handler and looper) and calls SendMessage() on it. PostMessage() has some yucky code that can end up reading stale memory if the target handler has been destroyed… but so does this BMessenger constructor. And in practice it won't actually cause a problem. I would also strongly suggest keeping around BMessenger objects instead of raw pointers to handlers — it is quite expensive to construct a BMessenger from a handler. (With all of the messaging optimizations in EXP, constructing the BMessenger is probably more expensive now than actually sending the message.)

And how about this: “Another nice thing about BMessenger, is that you send BMessage refrences, rather than pointers.” Um, hello, I added those functions that take a reference while I was at Be. They work exactly like the ones that take pointers, but I put these in because once BMessage switched to copy-on–write it made a lot more sense to deal with it as a simple object (like BRect). This is great because it means the developer doesn't need to deal with memory management… though of course it would be really hard to completely spread this new BMessage model through the API without breaking compatibility. (And don't get me started on all the APIs that still take a «BMessage*" when the should at least take a «const BMessage*".) At any rate, in all the message sending APIs whether you pass a pointer or a reference they work the same way.

And one final thing… “majik mumbo jumbo”? Give me a break. Hey if you guys have the source, take a look at src/inc/app_p/token.h. It'll take you about a paragraph to actually describe how the handler token works — I am sure people would be interested, and it is in fact really simple. I even rewrote that thing while I was at Be to clean it up, so now the code should be nice and easy to understand.

So let's see… from what I gather from this article, YellowTab has modified the Looper.h header so PostMessage() isn't visible unless you set a #define. A change of questionable value, anyway. Yay, YellowTab!

–- Dianne, who is sure she will regret this in the morning.

И рыбку покушать и покататься ....

Да, бедным ЖТ–программерам и «умом блеснуть» не дают. :–)) Не позавидуешь — и сорцов нету и выкручиваться как–то надо и умное лицо делать при этом. мнда… =–))

Отправить комментарий

Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Allowed HTML tags: <a> <em> <i> <img> <strong> <b> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Строки и параграфы переносятся автоматически.

Подробнее о форматировании

CAPTCHA
Введите перечисленные символы, чтобы мы убедились, что вы не робот. Не требуется для зарегистрированных пользователей.
W
W
X
r
W
B
Enter the code without spaces and pay attention to upper/lower case.