Tag Archive | Python

Multitasking styles, event loops and asynchronous programming

There was a text that I’ve previously published here in this blog about asynchronous programming, but it was written in Portuguese. Now has come the time that I think this text should be available also in English. Translation follows below. Actually is not a “translation” per si, as I adapted the text to be more pleasant when written in English.

One of the subjects that interest me the most in programming is asynchronous programming. This is a subject that I got in touch since I started to play with Qt around 2010, but I slowly moved to new experiences and “paradigms” of asynchronous programming. Node.js and Boost.Asio were other important experiences worth mentioning. The subject caught me a lot and it was the stimulus for me to study a little of computer architecture and operating systems.

Motivation

Several times we stumble upon with problems that demand continuously handling several jobs (e.g. handling network events to mutate local files). Intuitively we may be tempted to use threads, as there are several “parallel” jobs. However, not all jobs executed by the computer are done so exclusively in the CPU.

There are other components beside the CPU. Components that are not programmable and that do not execute your algorithm. Components that are usually slower and do other jobs (e.g. converting a digital signal into an analog one). Also, the communication with these components usually happen serially, through the fetch-execute-check-interrupt cycle. There is a simplification in this argument, but the fact that you don’t read two different files from the same hard drive in parallel remains. Summarizing, using threads isn’t a “natural” abstraction to the problem, as it doesn’t “fit” and/or design the same characteristics. Using threads can add a complex and unnecessary overhead.

“If all you have is a hammer, everything looks like a nail”

Another reason to avoid threads as an answer to the problem is that soon you’ll have more threads than CPU/cores and will face the C10K problem. Even if it didn’t add an absurd overhead, just the fact that you need more threads than available CPUs will make your solution more restrict, as it won’t work on bare-metal environments (i.e. that lacks a modern operating system or a scheduler).

A great performance problem that threads add comes from the fact that they demand a kernel context-switch. Of course this isn’t the only problem, because there is also the cost involved in creating the thread, which might have a short lifetime and spend most of its lifetime sleeping. The own process of creating a thread isn’t completely scalable, because it requires the stack allocation, but the memory is a global resource and right here we have a point of contention.

The performance problem of a kernel context-switch reminds the performance problem of the function calling conventions faced by compilers, but worse.

Functions are isolated and encapsulated units and as such they should behave. When a function is called, the current function doesn’t know which registers will be used by the new function. The current function doesn’t hold the information of which of the CPU registers will be overridden during the new function lifetime. Therefore, the function calling conventions add two new points to do extra processing. One point to save state into the stack and one to restore. That’s why some programmers are so obsessed into function inlining.

The context-switch problem is worse, because the kernel must save the values of all registers and there is also the overhead of the scheduler and the context-switch itself. Processes would be even worse, as there would be the need to reconfigure the MMU. This multitasking style is given the name of preemptive multitasking. I won’t go into details, but you can always dig more into computer architecture and operating systems books (and beyond).

It’s possible to obtain concurrency, which is the property to execute several jobs in the same period of time, without real parallelism. When we do this tasks that are more IO-oriented, it can be interesting to abandon parallelism to achieve more scalability, avoiding the C10K problem. And if a new design is required, we could catch the opportunity to also take into account cooperative multitasking and obtain a result that is even better than the initially planned.

The event loop

One approach that I see, and I see used more in games than anywhere else, is the event loop approach. It’s this approach that we’ll see first.

There is this library, the SDL low level library, whose purpose is to be just a multimedia layer abstraction, to supply what isn’t already available in the C standard library, focusing on the game developer. The SDL library makes use of an event system to handle communication between the process and the external world (keyboard, mouse, windows…), which is usually used in some loop that the programmer prepares. This same structure is used in other places, including Allegro, which was the biggest competitor of SDL in the past.

The idea is to have a set of functions that make the bridge of communication between the process and the external world. In the SDL world, events are described through the non-extensible SDL_Event type. Then you use functions like SDL_PollEvent to receive events and dedicated functions to initiate operations that act on the external world. The support for asynchronous programming in the SDL library is weak, but this same event loop principle could be used in a library that would provide stronger support for asynchronous programming. Below you can find a sample that makes use of the SDL events:

There are the GUI libraries like GTK+, EFL and Qt which take this idea one step further, abstracting into an object the event loop that were previously written and rewritten by you. The Boost.Asio library, which focuses on asynchronous operations and not on GUIs, has a class of similar purpose, the io_service class.

To remove your need to write boilerplate code to route events to specific actions, the previously mentioned classes will handle this task for you, possibly using callbacks, which are an old abstraction. The idea is that you should bind events to functions that might be interested in handling those events. Now the logic to route these events belong to the “event loop object” instead a “real”/raw event loop. This style of asynchronous programming is a passive style, because you only register the callbacks and transfer the control to the framework.

Now that we’re one level of abstraction higher than event loops, let’s stop the discussion about event loops. And these objects that we referring to as “event loop objects” will be mentioned as executors from now on.

The executor

The executor is an object that can execute encapsulated units of work. Using only C++11, we can implement an executor that schedules operations related to waiting some duration of time. There is the global resource RTC and, instead of approaching the problem by creating several threads that start blocking operations like the sleep_for operation, we’ll use an executor. The executor will schedule and manage all events that are required. You can find a simple implementation for such executor following:

This code reminds me of sleepsort.

In the example, without using threads, it was possible to execute the several concurrent jobs related to waiting time. To do such thing, we gave the executor the responsibility to share the RTC global resource. Because the CPU is faster than the requested tasks, only one thread was enough and, even so, there was a period of time for which the CPU was idle.

There are some concepts to be extracted from this example. First, let’s consider that the executor is an standard abstraction, provided in some interoperable way among all the code pieces that makes use of asynchronous operations. When a program wants to do the wait asynchronous operation, the program request the operation start to some abstraction — in this case it’s the executor itself, but it’s more common to find these operations into “I/O objects” — through a function. The control is passed to the executor — through the method run — which will check for notification of finished tasks. When there are no more tasks on the queue, the executor will give back the control of the thread.

Because there is only one thread of execution, but several tasks to execute, we have the resource sharing problem. In this case, the resource to be shared is the CPU/thread itself. The control should go and come between some abstraction and the user code. This is the core of cooperative multitasking. There are customization points to affect behaviour among the algorithms that execute the tasks, making them give CPU time to the execution of other tasks.

One advantage of the cooperative multitasking style of multitasking is that all “switch” points are well defined. You know exactly when the control goes from one task to another. So that context switch overhead that we saw earlier don’t exist — where all register values need to be saved… A solution that is more elegant, efficient and green.

The object that we passed as the last argument to the function add_sleep_for_callback is a callback — also know as completion handler. Think about what would happen if a new wait operation was requested within one of the completion handlers that we registered. There is an improved version of the previous executor following:

This implementation detail reminds me of the SHLVL SHELL variable.

An interesting case is the case from the JavaScript language. JavaScript has a kind of “implicit executor”, which is triggered when the VM reaches the end of your code. In this case, you don’t need to write codes like “while (true) executor.run_one()” or “executor.run()“. You only would need to register the callbacks and make sure there are no infinite loops until the executor gets the chance to be in control.

With the motivation introduced, the text started to decrease the use of mentions to I/O for reasons of simplicity and focus, but keep in mind that we use asynchronous operation mostly to interact with the external world. Therefore many operation are scheduled conditionally in response to the notification of the completion of a previous task (e.g. if protocol invalid, close socket else schedule another read operation). Proposals like N3785 and N4046 define executors also to schedule thread pools, not only timeouts within a thread. Lastly it’s possible to implement executors that schedule I/O operations within the same thread.

Asynchronous algorithms represented in synchronous manners

The problem with the callback approach is that we no longer have a code that is clean and readable. Previously, the code could be read sequentially because this is what the code was, a sequence of instructions. However, now we need to spread the logic among lots of lots of callbacks. Now you have blocks of code that are related far apart from each other. Lambdas can help a little, but it’s not enough. The problem is know as callback/nesting hell and it’s similar to the spaghetti code problem. Not being bad enough, the execution flow became controverted because the asynchronous nature of the operations itself and constructs like branching and repetition control structures and even error handling assume a representation that are far from ideal, obscures and difficult to read.

One abstraction of procedures very important to asynchronous programming is coroutines. There is the procedure abstraction that we refer to under the name of “function”. This so called “function” models the concept of subroutine. And we have the coroutine, which is a generalization of a subroutine. The coroutine has two more operations than subroutine, suspend and resume.

When your “function” is a coroutine — possible when the language provides support to coroutines — it’s possible to suspend the function before it reaches the end of execution, possibly giving a value during the suspend event. One example where coroutines are useful is on a hypothetical fibonacci generating function and a program that uses this function to print the first 10 numbers of this infinite sequence. The following Python code demonstrate an implementation of such example, where you can get an insight of the elegance, readability and and reuse that the concept of coroutines allows when we have the problem of cooperative multitasking:

This code reminds me of the setjmp/longjmp functions.

One characteristic that we must give attention in case you aren’t familiarized with the concept is that the values of the local variables are preserved among the several “calls”. More accurately, when the function is resumed, it has the same execution stack that it had when it was suspended. This is a “full” implementation of the coroutine concept — sometimes mentioned as stackful coroutine. There are also stackless coroutines where only the “line under execution” is remembered/restored.

The N4286 proposal introduces a new keyword, await, to identify a suspension point for a coroutine. Making use of such functionality, we can construct the following example, which elegantly defines an asynchronous algorithm described in a very much “synchronous manner”. It also makes use of the several language constructs that we’re used to — branching, repetition and others:

Coroutines solve the complexity problem that asynchronous algorithms demand. However, there are several coroutines proposals and none of them was standardized yet. An interesting case is the Asio library that implemented a mechanism similar to Duff’s Device using macros to provide stackless coroutines. For C++, I hope that the committee continue to follow the “you only pay for what you use” principle and that we get implementations with high performance.

While you wait for the standardization, we can opt for library-level solutions. If the language is low-level and gives the programmer control enough, these solutions will exist — even if it’s going to be not portable. C++ has fibers and Rust has mioco.

Another option to use while you wait for coroutines it not to use them. It’s still possible to achieve high performance implementation without them. The big problem will be the highly convoluted control flow you might get.

Completion tokens

While there is no standardized approach for asynchronous operations in the C++ language, the Boost.Asio library, since the version 1.54, adopted an interesting concept. The Boost.Asio implemented an extensible solution. Such solution is well documented in the N4045 proposal and you’ll only find a summary here.

The proposal is assumes that the callback model is not always interesting and can be even confusing sometimes. So it should be evolved to support other models. Now, instead receiving a completion handler (the callback), the functions should receive a completion token, which adds the necessary customization point to support other asynchronous models.

The N4045 document uses a top-down approach, first showing how the proposal is used to then proceed to low-level implementation details. You can find a sample code from the document following:

In the code that you just saw, every time the variable yield is passed to some asynchronous operation (e.g. open and read), the function is suspended until the operation is completed. When the operation completes, the function is resumed in the point where it was suspended and the function that started the asynchronous operation returns the result of the operation. The Fiber library, shortly mentioned previously, provides a yield_context to the Asio extensible model, the boost::fibers::asio::yield. It’s asynchronous code written in a synchronous manner. However, an extensible model is adopted because we don’t know which model will be the standard for asynchronous operations and therefore we cannot force a single model to rule them all.

To build an extensible model, the return type of the function needs to be deduced (using the token) and the value of the return also needs to be deduced (also using the token). The return type is deduced using the token type and the returned value is created using the token passed as argument. And you still have the handler, which must be called when the operation completes. The handler is extracted from the token. The completion tokens model makes use of type traits to extract all information. If the traits aren’t specialized, the default behaviour is to treat the token as a handler, turning the approach compatible with the callback model.

Several examples are given in the N4045 document:

  • use_future
  • boost::fibers::asio::use_future
  • boost::fibers::asio::yield
  • block

The std::future approach have meaningful impact on performance and this is not cool, like explained in the N4045 document. This is the reason why I don’t mention it in this text.

Signals and slots

One alternative that was proposed to the model of callbacks is the signals and slots approach. This approach is implemented in libgsigc++, Boost, Qt and a few other libraries.

This proposal introduces the concept of a signal, which is used to notify an event, but abstracts the delivery of the notification and the process of registering functions that are interested in the event. The code that notifies events just need to worry about emitting the signal every time the event happens because the signal itself will take care of handling the set of registered slots and stuff.

This approach usually allows a very decoupled architecture, in opposition of the very verbose approach largely used in Java. An interesting effect, depending on the implementation, is the possibility to connect one signal to another signal. It’s also possible to have multiple signals connected to one slot or one signal connected to multiple slots.

The signal usually is related to one object, and when the object is destroyed, the connections are destroyed too. Just like it’s also possible to have slot objects that automatically disconnect from connected signals once destroyed, so you have a safer abstraction.

Given signals are independently implemented abstractions and usable as soon as they are exposed, it’s naturally intuitive to remove the callback argument from the operations that initiate asynchronous operations to avoid duplication of efforts. If you go further in this direction, you’ll even remove the own function to do asynchronous operations, exposing just the signal used to receive notifications, your framework you’ll be following the passive style instead active style. Examples of such style are the Qt’s socket, which doesn’t have an explicit function to request the start of the read operation, and the POCO library, which doesn’t have a function to request that receiving of a HTTP request.

Another detail which we have in signals and slots approach is the idea of access control. In the Qt case, signals are implemented in a way that demands the cooperation of one preprocessor, the own Qt executor and the QObject class. In the Qt case, the control access rules for emitting a signal follow the same rules for protected methods from C++ (i.e. all children classes can emit signals defined in parent classes). The operation of connecting a signal to another signal or a slot follows the same rules of public members of C++ (i.e. anyone can execute the operation).

In the case of libraries that implement the signal concept as a type, it’s common to observe a type that encapsulate both, the operation to emit the signal and the operation to connect the signal to some slot (different from what we see in the futures and promises proposal, where each one can have different control access).

The signals and slots approach is cool, but it doesn’t solve the problem of complexity that is solved with coroutines. I only mentioned this approach to discuss better the difference between the active style and the passive style.

Active model vs passive model

In the passive model, you don’t schedule the start of the operations. It’s what we commonly find in “productive” frameworks, but there are many questions that this style doesn’t answer quite well.

Making a quick comparison between the libraries Qt and Boost.Asio. In both libraries, you find classes to abstract the socket concept, but using Qt you handle the event readyRead using the readAll method to receive the buffer with the data. In contrast, using Boost.Asio, you start the operation async_read_some and pass the buffer as argument. Qt uses the passive style and Boost.Asio uses the active style.

The readyRead event, from Qt, acts independently from the user and requires a buffer allocation every time it occurs. Then some questions arise. “How can I customize the buffer allocation algorithm?”, “how can I customize the application to allow recycling buffers?”, “how do I use a buffer that is allocated on the stack?” and more. The passive model doesn’t answer questions like these, then you need to fatten the socket abstraction with more customization points to allow behaviours like the ones described. It’s a combinatorial explosion to every abstraction that deals with asynchronous operations. In the active model, these customizations are very natural. If there is any resource demanded by the operation that the developer is about to start, the developer just need to pass the resource as an argument. And it’s not only about resource acquisition. Another example of question that the passive model doesn’t answer well is “how do I decide whether I’m gonna accept a new connection or postpone to when the server is less busy?”. It’s a great power to applications that are seriously concerned about performance and need fine adjustments.

Besides performance and fine tuning, the passive model is also causes trouble to debugging and tests thanks to the inversion of control.

I must admit that the passive model is quite good to rapid prototyping and increasing productive. Fortunately, we can implement the passive model on top of the active model, but the opposite is not so easy to do.

Bonus references

If you like this subject and want to dig more, I suggest the following references:

Estilos de multitasking cooperativo, loop de eventos e programação assíncrona

introUm dos assuntos que mais me interessa em programação é programação assíncrona. Esse é um tema ao qual eu fui introduzido desde que comecei a brincar com Qt (framework para criar interfaces gráficas) lá por volta de 2010, mas com o tempo fui tendo experiências em vários “paradigmas” de programação assíncrona, passando, por exemplo, por Node.js e Boost.Asio. O tema muito me interessa e foi, por exemplo, o motivador para eu estudar sistemas operacionais e arquitetura de computadores.

Esse é um trabalho que eu tive a ideia de fazer já faz muito tempo. Passou até ideia de, em vez de fazer um post, evoluir mais a ideia e transformar em contribuição e também a ideia de transformar em episódio de podcast. Acho que evolui muito durante esse tempo, para quem antes não entendia nem o porquê de uma troca de contexto ser lenta.

Motivação

Muitas vezes, nos deparamos com problemas que exigem o tratamento contínuo de várias tarefas, como tratar eventos de rede e manipular arquivos locais, por exemplo. Uma intuição ao encarar esse problema seria o uso de threads, já que há várias tarefas em “paralelo”. Entretanto, nem todos os trabalhos efetuados pelo computador são executados exclusivamente pela CPU.

Há outros componentes além da CPU, mas não programáveis e que não executam seu algoritmo, que costumam ser mais lentos e possuem outras tarefas, como transformar um sinal digital em analógico, por exemplo. Além disso, a comunicação com esses componentes ocorre de forma serial, através do ciclo buscar-decodificar-executar-checar-interrupção. Há uma simplificação nesse argumento, mas o fato é que você não lê dois arquivos em paralelo do mesmo HD. Em suma, usar threads não é uma abstração natural ao problema e só introduz gargalo desnecessário e complexo.

“Para quem só sabe usar martelo, tudo parece um prego.” — velho provérbio Klingon

Outro motivo para evitar o uso de threads como resposta ao problema é que logo você terá mais threads que CPUs/núcleos e irá se deparar com o que é conhecido como problema C10K. Mesmo que não fosse um gargalo absurdo, só o fato de você necessitar de mais threads que CPUs disponíves já torna o seu programa mais restrito, pois ele não mais poderá funcionar em um ambiente bare metal, sem a presença de um sistema operacional moderno ou um scheduler.

E o grande problema de desempenho que threads introduzem decorre do fato de elas exigirem uma troca de contexto com o kernel. É claro que esse não é o único problema, pois há também o custo de criação da thread. O programa estaria realizando a criação de threads, que podem acabar tendo um tempo de vida curto e, além disso, passarem a maior parte de seu tempo de vida dormindo. A própria criação da thread não é completamente escalável, pois requer a alocação de sua própria pilha, porém a memória é um recurso global e já aí temos um ponto de contenção.

O problema de desempenho de uma troca de contexto do sistema operacional lembra o problema de desempenho da convenção de chamada de funções usada por compiladores, mas pior.

Funções são unidades isoladas, encapsuladas, e de tal forma deveriam trabalhar. Ao realizar uma chamada de função, a função atual não tem conhecimento de quais registradores serão usados pela nova função. A função atual não detém a informação de quais registradores terão seus valores sobrescritos pela nova função. Assim, as convenções de chamadas de função exigem dois pontos de processamento extra, um para salvar os valores dos registradores na pilha, e outro para restaurar os valores. É por isso que alguns programadores são tão fissurados em fazer function inlining.

O problema de desempenho da troca de contexto é maior, porque ele deve salvar o valor de todos os registradores e ainda sofre com o gargalo do scheduler e da própria troca de contexto. E se processos estiverem envolvidos, então, ainda tem o gargalo de reconfigurar a unidade de gerenciamento de memória. Esse estilo de processamento multitarefa recebe o nome de multitarefa preemptivo. Você pode aprender mais sobre esses custos estudando arquitetura de computadores e sistemas operacionais.

É possível obter concorrência, que é a propriedade de executar várias tarefas no mesmo período de tempo, sem recorrer a paralelismo real. Para essas tarefas que estão mais atreladas a I/O, é interessante abandonarmos o paralelismo durante o tratamento de I/O para alcançarmos mais escalabilidade, evitando o problema do C10K. E já que um novo design se faz necessário, é bom levar em conta multitarefa cooperativo e obter um resultado até melhor do que o inicialmente planejado.

O loop de eventos

E uma abordagem para o problema de programação, que eu vejo sendo usada mais em jogos do que em qualquer outro lugar, é a abordagem de loop de eventos. Há essas peculiaridades de que em jogos você não manipula arquivos como se fosse um banco de dados, com ambiciosos requisitos de desempenho, e também de desenvolver o jogo para quando completar, não mudar o código nunca mais, sem qualquer compromisso com flexibilidade e manutenção. E é com essa abordagem, a abordagem do loop de eventos, que começamos.

Há uma biblioteca, a biblioteca de baixo nível SDL, que tem como objetivo ser apenas uma camada de abstração multimídia, para suprir o que já não é suprido pela própria especificação da linguagem C, focando no desenvolvedor de jogos. A SDL faz uso de um sistema de eventos para tratar da comunicação entre o processo e o mundo externo (teclado, mouse, janelas, …), que é comumente usada em algum loop que o programador prepara. Essa mesma estrutura é utilizada em vários outros locais, incluindo na Allegro, que foi o maior competidor da SDL, no passado.

A ideia é ter um conjunto de funções que faz a ponte da comunicação entre o processo e o mundo externo. No mundo SDL, eventos são descritos através do tipo não-extensível SDL_Event. Então você usa funções como a SDL_PollEvent para receber os eventos e funções dedicadas para iniciar operações que atuem no mundo externo. O suporte a programação assíncrona na biblioteca SDL é fraco, mas esse mesmo princípio de loop de eventos poderia ser usado em uma biblioteca que fornecesse um suporte mais forte a programação assíncrona. Abaixo você pode ver o exemplo de um código que faz uso de eventos da SDL:

Há as bibliotecas de interface gráfica e widgets, como a GTK+, a EFL e a Qt, que levam essa ideia um passo adiante, abstraindo o loop de eventos, antes escrito e reescrito por você, em um objeto. A Boost.Asio, que não é focada em interface gráficas, mas foca exclusivamente em operações assíncronas, possui uma classe de propósito similar, a classe io_service.

Para que você deixe de fazer o código boilerplate de rotear eventos a ações específicas, essas classes tratam essa parte para você, possivelmente através de callbacks, que é uma abstração antiga. A ideia é que você associe eventos a funções que podem estar interessadas em tratar tais eventos. Toda a lógica de rotear os eventos, agora, passa a fazer parte do objeto “loop de eventos”, em vez de um loop de eventos bruto. Esse estilo de programação assíncrona é um estilo do modelo passivo, pois você só registra os callbacks e cede o controle para a framework.

Agora que estamos em um nível de abstração superior ao do loop de eventos, vamos parar a discussão sobre loop de eventos. E esses objetos que estamos mencionando como objetos “loop de eventos”, serão mencionados, a partir de agora, como executors.

O executor

O executor é um objeto que pode executar unidades de trabalho encapsuladas. Utilizando somente C++11, podemos implementar um executor que gerencie o agendamento de operações relacionadas a espera de alguma duração de tempo. Há o recurso global RTC e, em vez de criar várias e várias threads para a execução de operações bloqueantes como a operação sleep_for, vamos usar um executor, que irá agendar e tratar todos os eventos de tempo que se façam necessário. Abaixo está o código para uma possível implementação de tal executor:

Esse código de exemplo me faz lembrar do sleepsort.

No exemplo, sem o uso de threads, foi possível realizar as várias tarefas concorrentes de espera de tempo. Para tal, ao objeto executor, foi dada a responsabilidade de compartilhar o recurso global RTC. Como a CPU é mais rápida que as tarefas requisitadas, uma única thread foi suficiente e, além disso, mesmo assim houve um período de tempo no qual o programa ficou ocioso.

Há alguns conceitos que já podem ser extraídos desse exemplo. Primeiro, vamos considerar que o executor seja uma abstração padrão, já fornecida de alguma forma e interoperável entre todos os códigos que façam uso de operações assíncronas. Quando o programa deseja fazer a operação assíncrona “esperar”, ele requisita o início da operação a alguma abstração (nesse caso é o próprio executor, mas é mais comum encontrar tais interações através de “objetos I/O”) através da função que inicia a operação. O controle é passado para o executor (através do método run), que continuamente verifica notificações de tarefas concluídas. Quando uma tarefa é concluída, o executor empresta o controle da thread para o código do usuário, executando a função que havia sido registrada para tratar a notificação de finalização da tarefa. Quando não há mais nenhuma tarefa na fila, o executor cede o controle da thread, por completo, para o código do usuário.

Como só existe uma thread de execução, mas várias tarefas a executar, nós temos o problema de compartilhamento de recursos, que nesse caso é a própria CPU/thread. O controle deveria ir e voltar entre alguma abstração e o código do usuário. E aí está o princípio do estilo multitarefa cooperativo. Há pontos de customização de comportamento entre os algoritmos responsáveis por executar as tarefas, fazendo com que eles cedam e emprestem o tempo da CPU para realização de outras tarefas.

O estilo de multitarefa cooperativo tem a vantagem de que os pontos de paradas são bem definidos, pois você sabe exatamente quando o controle passa de uma tarefa a outra. Então não há aquele grande gargalo que vimos com trocas de contextos, onde todos os registradores precisam ser salvos, entre outros. Uma solução mais elegante, eficiente e verde.

O objeto que passamos como último argumento da função add_sleep_for_callback é um callback, também conhecido como completion handler. Reflita sobre o que aconteceria se uma nova operação de espera fosse requisitada dentro de um dos callbacks que registramos. Abaixo há uma versão evoluída do executor que implementamos.

Esse detalhe de implementação me lembra a variável de SHELL SHLVL.

Um caso interessante é o da linguagem JavaScript, que possui um tipo de “executor implícito”, que passa a funcionar quando chega ao fim do seu código. Nesse caso, você não precisa escrever códigos como “while (true) executor.run_one()” ou “executor.run()“, mas apenas registrar os callbacks e se assegurar que não há nenhum loop infinito que impeça que o controle passe ao executor.

Introduzida a motivação, o texto passou a prosseguir reduzindo menções a I/O, por motivos de simplicidade e foco, mas tenha em mente que usamos operações assíncronas, majoritariamente, para realizar interações com o mundo externo. Então muitas das operações são agendadas condicionalmente em resposta a notificação de término de uma tarefa anterior (ex.: se protocolo inválido, feche o socket, do contrário, agende mais uma operação de leitura). Propostas como a N3785 e a N4046 definem executors também para gerenciar thread pools, não somente timeouts dentro de uma única thread. E por último, também é possível implementar executors que agendem a execução de operações de I/O dentro de uma mesma thread.

Algoritmos assíncronos descritos de forma síncrona

O problema com essa abordagem de callbacks é que antes, possuíamos código limpo e legível. O código podia ser lido sequencialmente, pois é isso que o código era, uma sequência de instruções. Entretanto, agora precisamos espalhar a lógica entre múltiplos e múltiplos callbacks. Agora você passa a ter blocos de código relacionados longe uns dos outros. Lambdas ajudam um pouco com o problema, mas não o suficiente. O problema é conhecido como callback/nesting hell e é similar ao problema do espaguete. Não sendo o bastante, o fluxo de execução do código se tornou controvertido pela própria natureza assíncrona das operações e construções como estruturas de condição e repetição e até mesmo código de tratamento de erro ficam representáveis de formas longe do ideal, obscuras e difíceis de ler.

Uma abstração de procedimentos muito importante para programação assíncrona é a corotina. Existe a abstração de procedimentos a qual nos referimos por função, que é uma implementação do conceito de subrotina. E temos a corotina, que é uma generalização do conceito de subrotina. A corotina possui duas operações a mais que a subrotina, sendo elas suspender e resumir.

Quando sua função é uma corotina (possível quando a linguagem dá suporte a corotinas), ela pode suspender antes de chegar ao final de sua execução, possivelmente devolvendo um valor durante essa ação de suspender. Um exemplo de onde corotinas são úteis é na implementação de uma suposta função geradora fibonacci e um programa que use essa função para imprimir os dez primeiros números dessa sequência infinita. O código Python abaixo demonstra uma implementação de tal exemplo, onde se percebe a elegância, legibilidade e reaproveitamento que o conceito de corotina permite quando temos o problema de multitarefa cooperativa:

Esse código aí me lembra das funções setjmp/longjmp.

Uma característica a qual se deve dar atenção caso você não esteja familiarizado com o conceito é que o valor das variáveis locais a função fibonacci foi preservado entre as várias “chamadas”. Mais precisamente, quando a função era resumida, ela possuía a mesma pilha de execução de quando foi suspensa. Essa é a uma implementação “completa” do conceito de corotina, uma corotina com pilha. Há também implementações de corotinas sem pilha de execução, onde somente o “número da linha de código” que estava executando é restaurado.

A proposta N4286 introduz uma nova palavra-chave, await, para identificar um ponto para suspender uma corotina. Fazendo uso de tal funcionalidade, é apresentado o seguinte exemplo, que elegantemente define um algoritmo assíncrono, descrito, na minha humilde opinião, de forma bastante síncrona, e fazendo uso das várias construções da linguagem para controle de fluxo de execução (estrutura de seleção, repetição…).

Corotinas resolvem o problema de complexidade que algoritmos assíncronos demandam. Entretanto, há várias propostas para implementação de corotinas e nenhuma delas foi padronizada ainda. Um caso interessante é o da biblioteca Asio, que, usando macros e um mecanismo similar ao Duff’s device, dá suporte a corotinas que não guardem uma pilha de execução. Dentre todo o trabalho que está sendo investido, o que eu espero que aconteça na comunidade de C++ é que a padronização siga o princípio de “você só paga pelo que usa” e que a especificação permita implementações bem performáticas.

Enquanto a padronização de corotinas não acontece e ficamos sem a solução a nível de linguagem, podemos optar por soluções a nível de biblioteca. Como C++ é uma linguagem que suporta abstrações de baixo nível, você consegue acesso a várias facilidades que podem ser usadas para implementar suporte a corotinas, tipo setjmp e longjmp e até mais coisas indo para o mundo não-portável fora da especificação. Uma biblioteca que parece bem promissora e que espero ver sendo incluída na Boost esse ano é a biblioteca Fiber, que imita a API de threads a qual estamos acostumados para fornecer “threads” agendadas cooperativamente, em espaço de usuário. A biblioteca usa o conceito de fibra, análogo ao conceito de thread, e em uma thread, você pode executar várias fibras. Tal biblioteca fornece a expressividade que precisamos para escrever algoritmos assíncronos de forma síncrona.

Outra solução enquanto você não pode esperar, é não usar corotinas, pois ainda será possível conseguir performance excelente através das técnicas comentadas ao longo do texto. O grande porém vai ser o fluxo embaralhado do código-fonte (ofuscação de código, para quê te quero?).

Completion tokens

E nesse período de tempo incerto quanto a que solução tornará-se o padrão para programação assíncrona na linguagem C++, a Boost.Asio, desde a versão 1.54, adotou um princípio interessante, de implementar uma solução extensível. Tal modelo extensível é muito bem documentado na proposta N4045 e aqui nessa seção há somente um resumo do que está contido em tal proposta.

A proposta é que, dado que o modelo de callbacks é confuso, ele seja evoluído para suportar outros modelos. Então, em vez da função receber como argumento um completion handler (o callback), ela deve receber um completion token, que é a solução de customização/extensibilidade proposta.

A proposta N4045 usa uma abordagem top-down, de primeiro mostrar como a proposta é usada, para depois mostrar como ela é implementada. Abaixo há um código de exemplo retirado da proposta:

No código de exemplo anterior, cada vez que a variável yield é passada a alguma operação assíncrona (ex.: open e read), a função é suspensa até que a operação seja concluída. Quando a operação é concluída, a função é resumida no ponto em que foi suspensa e a função que iniciou a operação assíncrona retorna o resultado da operação. A biblioteca Fiber, mencionada anteriormente, fornece um yield_context para o modelo extensível da Asio, o boost::fibers::asio::yield. Código assíncrono escrito de uma forma síncrona. Entretanto, um modelo extensível é adotado, pois não sabemos qual será o padrão para operações assíncronas, então não podemos forçar o uso da biblioteca Fiber goela abaixo.

Para tornar o modelo extensível, o tipo do retorno da função precisa ser deduzido (a partir do token) e o valor do retorno da função também precisa ser deduzido (também a partir do token). O tipo do retorno da função é deduzido a partir do tipo do token e o valor retornado é criado a partir do objeto token passado como argumento. E você ainda tem o handler, que deve ser chamado quando a operação for concluída. O handler também é extraído a partir do token. O modelo de completion tokens faz uso de type traits para extrair todas essas informações e, caso tais traits não sejam especializados, o comportamento padrão é tratar o token como um handler, tornando a abordagem retrocompatível com o modelo de callbacks.

Vários exemplos de token são dados no documento N4045:

  • use_future
  • boost::fibers::asio::use_future
  • boost::fibers::asio::yield
  • block

A abordagem de usar std::future tem impactos significativos na performance e não é uma abstração legal, como o próprio N4045 explica em suas seções iniciais, então vamos evitá-la. É até por isso que eu nem comentei sobre ela até então nesse texto.

Sinais e slots

Uma alternativa que foi proposta ao modelo de callbacks é a abordagem de “signals & slots”. Essa abordagem é implementada na libsigc++, na Boost, na Qt e em várias outras bibliotecas.

A proposta de usar sinais introduz esse conceito, de sinal, que é usado para notificar algum evento, mas abstrai o processo de entregar as notificações e registrar as funções que tratem o evento. O código que notifica os eventos só precisa se preocupar em emitir o sinal toda vez que o evento acontecer, pois o próprio sinal vai cuidar do conjunto de slots, que são as porções de código a serem executadas para tratar os eventos.

Essa abordagem costuma permitir um grande desacoplamento, em oposição a abordagem verbosa muito usada em Java. Um efeito interessante, também, a depender da implementação, é que você pode conectar um sinal a outro sinal, para evitar o trabalho de escrever você próprio o código que sincronize a emissão de um sinal a emissão de outro sinal. É possível também ter muitos sinais conectados a um único slot, assim como um único sinal conectado a múltiplos slots.

O sinal costuma ser associado a um objeto, e quando tal objeto é destruído, as conexões que haviam sido feitas também o são. Assim como também é possível ter slots como métodos de um objeto, que são desconectados de todos os sinais tão logo o objeto é destruído.

Como os sinais são abstrações independentes e operantes assim que se tornam expostos, é natural ser incentivado a remover o argumento de callback das funções que iniciam operações assíncronas, pois haveria duplicação de esforços. Se você for até mais longe e remover também a própria função que inicie a operação assíncrona, expondo ao usuário apenas os sinais para receber as notificações, sua framework deixará de seguir o modelo ativo para seguir o modelo passivo. Exemplos de tais modelos passivos é o socket da biblioteca Qt, que não possui uma função explícita para iniciar a operação de leitura, e a biblioteca POCO, que não possui uma função explícita para iniciar o recebimento de uma requisição HTTP.

Outro detalhe que temos no caso da ideia de sinais, é o controle de acesso. No caso da biblioteca Qt, sinais são implementados de uma forma que exige a cooperação de um pré-processador, o executor da própria Qt e a classe QObject. No caso da biblioteca Qt, a emissão de sinais segue as regras de controle de acesso de métodos protegidos em C++, onde todas as classes-filha podem realizar a emissão de sinais declaradas nas classes-pai. Enquanto a operação de conectar um sinal a outro sinal ou a um slot segue as mesmas regras de membros públicos, onde todo mundo pode realizar.

No caso das bibliotecas que implementam o conceito de sinais como um tipo, é comum ver um tipo sinal que englobe tanto a operação de emitir o sinal, quanto a operação de conectar o sinal a algum slot (diferente do que vemos na proposta de futures e promises, onde é possível ter um controle de acesso separado para as diferentes responsabilidades/operações/entidades).

A abordagem de sinais é legal, mas ela não resolve o problema de complexidade que é resolvido por corotinas. Eu descrevi essa abordagem com o intuito de facilitar a explicação sobre a diferença entre os modelos ativo e passivo.

Modelo ativo versus passivo

No modelo passivo, você não agenda o início das operações e, apesar de ser mais comum em frameworks de produtividade, há muitas perguntas que esse modelo não responde bem (isso é só uma opinião e não um fato), que acabam exigindo o projeto de bem mais abstrações para contornar o problema.

Fazendo um contraste rápido entre a biblioteca Qt e a Boost.Asio. Em ambas as bibliotecas, você possui classes para abstraírem o conceito de socket, mas, enquanto na Qt você trata o evento readyRead e usa o método readAll para receber o buffer com o conteúdo, na Boost.Asio você inicia a operação async_read_some e passa o buffer a ser utilizado como argumento. A Qt usa o modelo passivo e a Boost.Asio usa o modelo ativo.

O evento readyRead, da Qt, age independente do usuário e requer a alocação do buffer toda vez que ocorre. Como, então, você responde a perguntas como “como eu posso customizar o algoritmo de alocação do buffer?”, “como eu posso customizar a aplicação para fazer reaproveitamento de buffers?”, “como eu faço para usar um buffer pré-alocado na stack?” e outras. Como o modelo passivo não responde a perguntas como essa, você precisa inflar a abstração de socket com mais pontos de customização para que comportamentos como esses sejam possíveis. É uma explosão combinatória que acontece para cada abstração que lide com operações assíncronas. No modelo ativo, fica bem natural. Se há algum recurso que a operação que o programador está prestes a iniciar necessita, só precisa passar o recurso como argumento, pois no modelo ativo, o programador explicitamente inicia as operações. E não é só sobre customizar obtenção de recursos. Mais um exemplo de pergunta que o modelo passivo não responde bem é “como eu decido se vou ou não aceitar uma conexão (ou adiar para depois, durante cenários de sobrecarga do servidor)?”. É um grande poder para aplicações sérias que estão seriamente preocupadas com performance e necessitam de ajustes meticulosos.

Além da questão de performance e ajuste fino, o modelo passivo também é problemático para realizar depuração e testes, pois ele faz uma inversão de controle. Esse é um dos motivos pelos quais, até hoje, eu vejo frameworks e frameworks introduzindo race condition nos testes ao depender de timeout para garantir a “robustez” de suas implementações. Essa solução de timeout não resolve um problema que ocorre somente no modelo passivo, mas no modelo passivo, ele ocorre com muito mais frequência. No modelo ativo, seu callback pode ser chamado mesmo quando o erro acontece. No modelo passivo é tão problemático, que já vi eventos de erro serem declarados como notificações separadas, que podem ser facilmente ignoradas, característica indesejável quando você quer diminuir e evitar os bugs no seu código.

Graças ao modelo ativo, muito da biblioteca que estou desenvolvendo com o intuito de submeter a Boost foi simplificado.

O modelo passivo, no entanto, é ótimo para prototipações rápidas. Felizmente podemos implementar abstrações que introduzam o modelo passivo em termos do modelo ativo e ter o melhor (ou o pior, se você for um projetista de C#) dos dois mundos.

Referências bônus

Uns links aleatórios que também me ajudaram como referência ou podem servir de leitura aprofundada para quem quer mais:

Comentários bônus

Espero ter introduzido você, estimado leitor que não comenta, as principais práticas utilizadas no universo da programação assíncrona. E agora que você já está armado com uma visão geral da “área”, se jogar nesse mar da internet para buscar mais referências e cavar mais fundo é só o segundo passo.

Esses dois meses que não postei nada e fui perdendo acessos/visitantes… valeram a pena, pois estou bastante satisfeito com esse trabalho e esse blog é sobre minha satisfação pessoal, por isso que eu nem coloco propaganda.

Há alguns outros textos separados em minha pasta de rascunho com temas variando desde ideias originais até assuntos “manjados” que eu pretendo usar quando bater aquela preguiça de investir muito tempo para um post só.

Agora que eu finalmente discuti concorrência sem paralelismo nesse blog, eu devia atualizar meu notebook (ou corrigir meu desktop) para ter um hardware suficientemente bom e começar a estudar OpenCL para fazer uns posts sobre paralelismo massivo.

EDIT (2015/3/11):

  • Ressaltar sugestão de estudar arquitetura de computadores e sistemas operacionais na seção de motivação.
  • Menção sobre “executor implícito” que ocorre em JavaScript.
  • Adicionado exemplo de motivação não relacionado a buffers na discussão de modelo ativo versus modelo passivo.

EDIT (2015/9/21):

No dia 25 de Abril de 2015 aconteceu o FLISOL e, no FLISOL Maceió, eu palestrei sobre esse tema. Os slides da apresentação estão disponíveis, para quem se interessa.

Inkscape, curvas de Bézier, SVG e Python

Nesse texto, pretendo introduzir o leitor ao desenvolvimento de programas que façam uso de curvas de Bézier e, como metodologia de ensino experimental, pretendo usar conhecimentos relacionados (a ferramenta Inkscape e o padrão SVG) como auxílio.

Edição vetorial

Chegamos em um ponto onde os computadores transformaram nossos hábitos diários e estão presentes nas mais variadas partes de nossas vidas. Uma das áreas que foram modernizadas pelos computadores é a área de tratamento de imagens.

A forma mais comum utilizada para representar imagens em computadores é através de matrizes onde cada ponto contém um nível de intensidade das cores vermelho, verde e azul. Os monitores funcionam de forma parecida. O problema com essa abordagem é que se precisarmos modificar a imagem, mudando o tamanho, a posição ou o ângulo da imagem, a imagem passará por uma edição destrutiva, onde há a perda de qualidade na imagem original. Começando pelo exemplo mais simples, veja as imagens abaixo:

Small girl

Small girl

Small girl

Small girl

Note como a imagem, ao aplicarmos um “zoom”, torna-se quadriculada. É possível remover esse aspecto aplicando alguns filtros, mas não importa o filtro que escolhermos, ele irá provocar alguma perda de informação. Filtros especializados são populares, pois costumam conseguir resultados melhores para casos específicos (fotos, pixel art, …). A esse tipo de imagem, utilizamos o termo raster.

A solução que encontramos para o problema de qualidade perfeita independente de resolução foram imagens vetoriais. Imagens vetoriais são imagens que, no lugar de ter uma representação dos pontos da imagem, possuem um conjunto de descrições de curvas, que são utilizadas para gerar a imagem raster em qualquer resolução, mantendo as características da curva. A imagem abaixo ilustra bem a diferença entre imagens raster e imagens vetoriais:

Exemplo da diferença entre imagens raster e imagens vetoriais

Esse tipo de imagem ainda não é dominante, pois é mais difícil de trabalhar com elas para certos tipos de trabalho. Entretanto, para alguns tipos de trabalho (como logotipos), elas são um requisito.

Inkscape

Inkscape é uma ferramenta open source para trabalharmos com imagens vetoriais que vem sendo desenvolvida há bastante tempo.

O Inkscape

O Inkscape

A principal ferramenta que usamos para trabalhar com imagens vetoriais é a curva de Bézier. No Inkscape, você pode pressionar Shift + F6 para selecioná-la.

Inkscape - Curvas de Bézier

Inkscape – Curvas de Bézier

A curva de Bézier (do ponto de vista do Inkscape) é definida por nós de controle e você pode mudar certos atributos de cada nó para fazer a curva se tornar mais aberta, fechada ou cúspide.

Inkscape - edição de nós

Inkscape – edição de nós

O formato que o Inkscape usa para salvar o seu trabalho é o SVG, um padrão aberto da W3C (a mesma entidade que define vários padrões utilizados pela web) baseado em XML, que, em breve, será abordado novamente.

O foco desse artigo não é transformá-lo em um grande designer, então isso é tudo que será comentado sobre o Inkscape.

Python

Python é uma linguagem de programação razoavelmente popular com um incrível poder expressivo que inspira vários programadores. Nesse texto ela será utilizada para a demonstração dos algoritmos e os códigos costumam ser bem legíveis, sem que nenhum treinamento especial seja necessário para o entendimento. Entretanto, utilizo alguns poucos conceitos que talvez sejam incomuns, então resolvi documentá-los aqui, para facilitar na futura compreensão de tais códigos.

For

Em Python, o for atua da forma que, em outras linguagens, atua o for-each. Uma função comumente utilizada quando você precisa manipular elementos através de índices é a função range, que recebe um argumento inteiro N e retorna uma lista contendo os números no intervalo [0;N). A saída de range(10), por exemplo, é [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].

Fatias/slices

Em Python, existe um operador para fatiar listas (e similares). Ele possui a mesma sintaxe do operador de índice. O código a seguir resume bem algumas das operações possíveis com o mesmo:

Listas e tuplas

Em Python, os dois tipos principais para se trabalhar com cadeias são listas e tuplas. Colchetes são utilizados para criar listas, enquanto parênteses são utilizados para criar tuplas. A diferença entre listas e tuplas é que tuplas são imutáveis. Para converter entre um tipo e outro, basta passar o objeto como argumento do construtor do outro tipo.

Sobrecarregando o operador de chamada de função

Em Python, tudo é um objeto (incluindo as funções) e é possível sobrecarregar até o ponto. Para sobrecarregar o operador de chamada de função, basta definir a função-membro __call__ e, daí em diante, você poderá usar o objeto com o operador sobrecarregado como se fosse uma função.

Considerações finais sobre Python

Nesse texto, o código-fonte apresentado utiliza tuplas para representar pontos e, apesar de não haver verificações de segurança, assumimos que um ponto pode ser de qualquer dimensão (na verdade não). Isso implica que sempre utilizamos repetição para preencher os valores dos pontos.

Curvas de Bézier

Há vários modos que podemos utilizar para descrever curvas e cada abordagem reflete uma definição diferente e possui diferentes limitações. Se usarmos funções matemáticas fundamentais, como x^2 ou 2*x para representar curvas, não poderemos representar curvas que passam pela mesma linha em y mais de uma vez, uma limitação inaceitável. Utilizar funções complicadas também não é uma solução viável, pois a instabilidade numérica dos algoritmos utilizados pode estragar o resultado final. Felizmente, foram criadas as curvas de Bézier, que resolvem todos esses problemas.

Função batman

Função batman

A nossa jornada para explorar as curvas de Bézier começa com uma fórmula matemática bem simples, acompanhada de seu algoritmo em Python (por que usar pseudo-algoritmos se temos uma linguagem mais expressiva, fácil e inambígua?):

f1

A função l(t) define uma linha que passa pelos pontos p1 e p2. No código-fonte, foi criada uma classe que permite definir uma família dessas funções, com diferentes pontos p1 e p2. Para obter um ponto presente na linha, você passa um valor t qualquer. É fácil perceber que o valor de l(0) é igual a p1 e o valor de l(1) é igual a p2.

“Curva” definida por dois pontos

Usando dois pontos e a fórmula anterior é possível definir uma linha reta. Podemos adicionar um terceiro ponto e a “mesma fórmula” para definir a curva que vai da linha (a fórmula anterior usava pontos) l1 até a linha l2. Com 3 pontos, a fórmula é transformada no seguinte:

f2

Toda a ideia de curvas de Bézier é uma generalização desse princípio: Dada uma lista de pontos, nós queremos descrever uma curva que viaja do ponto p0 até o ponto pn guiada pelos pontos intermediários. Uma curva de Bézier composta por N pontos de controle possui grau N – 1. O código anterior se traduz na seguinte animação:

Você pode (e deveria) brincar com curvas de Bézier nesse site (que não necessita do Flash Player ou do Java).

Algoritmo de De Casteljau

Agora que sabemos o que é uma curva de Bézier, devemos começar a nos preocupar como podemos renderizá-las e esse é justamente o propósito do algoritmo de De Casteljau, um algoritmo recursivo lento, porém numericamente estável.

A propriedade explorada pelo algoritmo de De Casteljau para renderizar linhas de Bézier é que qualquer curva de Bézier pode ser dividida em duas, conectadas fim-a-fim, produzindo o mesmo resultado (lembre-se que é um algoritmo recursivo).

No código anterior, a função is_flat informa quando uma curva está indistinguível de uma reta (uma forma de heurística) e a função subdivide divide uma curva de Bézier em duas. Esse pequeno código simples é suficiente para entender as diferentes partes do algoritmo. Há uma condição para parar a recursão e duas sub-tarefas (seguindo o princípio de dividir para conquistar).

O problema será quebrado, primeiramente, com a solução para o problema de dividir uma curva de Bézier cúbica em duas. Ao tentar encontrar o ponto onde t=1/2 em uma curva de Bézier, encontramos o ponto que usaremos para “cortá-la” em duas. Na imagem a seguir, esse é o ponto azul ciano:

subdivision

Existe uma afirmação de que a curva formada pelos pontos P0, P1, P2 e P3 é igual a junção da curva formada pelos pontos P0, m0, q0, B(1/2) com a curva formada pelos pontos B(1/2), q1, m2 e P3. A prova dessa afirmação existe, porém não será feita abaixo, pois ela é de pouco interesse (nem tanto…) para o nosso objetivo. Note que os pontos m0, q0, m1, q1, m2 são os pontos médios dos segmentos de retas anteriores e são facilmente computáveis. O código Python a seguir é capaz de computar esses pontos:

Note que o algoritmo serve para curvas de Bézier cúbicas (formadas por 4 pontos). Agora que temos um algoritmo para dividir as curvas de Bézier, a implementação restante para sermos capazes de utilizar o algoritmo de De Casteljau é a “heurística” que informa se a curva já está “reta” o suficiente.

Uma heurística que podemos utilizar e possui boa estabilidade numérica computa a distância, num instante t, entre onde a curva está e onde ela deveria estar caso fosse uma linha reta. Chamemos essa função de d(t), então o valor de t que queremos utilizar para a função heurística é o valor onde d(t) é maior (maximizar). Após cancelar, reduzir e otimizar várias operações, chegamos a seguinte função (que só funciona para pontos bidimensionais):

SVG

A abordagem usada até o momento requer que compartilhemos nossas imagens vetoriais através de longos textos descritivos ou códigos feito em Python, uma abordagem inviável caso queiramos compartilhar e colaborar nossos trabalhos. Para resolver esses problemas, o padrão SVG foi criado.

Como comentado anteriormente, SVG é o padrão aberto mais difundido para se trabalhar com imagens vetoriais. O SVG é baseado em XML (sendo assim, legível para humanos) que contém descrições sobre curvas de Bézier (e outros conceitos que o tornam mais expressivo e poderoso).

O elemento raiz de um SVG é o svg e seu esqueleto básico pode ser visto abaixo:

O exemplo anterior possui um comentário que indica onde colocamos o conteúdo dos nossos trabalhos, em SVG. O elemento que utilizamos para criar linhas de Bézier é o elemento path, que recebe um atributo d, capaz de controlar, de forma análoga, os movimentos que você pode fazer com um lápis. O exemplo abaixo será útil para explicar características importantes:

O documento acima possui dois novos elementos do tipo path. Eles desenham duas linhas diagonais para ajudar você a entender o sistema de coordenadas do SVG e possuem traços de preenchimento com largura e cor diferentes, para diferenciá-los mais facilmente.

Sistema de coordenadas

Sistema de coordenadas

O sistema de coordenadas é parecido com o desenho torto exemplificado acima. Você tem o “centro” no canto superior esquerdo e anotamos as coordenadas na forma x y. É diferente da forma que comumente fazemos em matemática, mas se você é um programador já deve estar acostumado com esse sistema.

Outra característica interessante para se notar é que os documentos são desenhados na ordem que foram definidos. Nesse caso, a “linha azul” irá sobrepor a “linha preta”, pois foi definida por último.

O atributo stroke define a cor de contorno (que até agora é o próprio caminho) e ela pode ser especificada de forma similar a HTML/CSS. Uma alternativa para a cor azul, usada anteriormente como blue, seria “#0000ff“. O atributo stroke-width determina a largura do contorno. A largura de contorno padrão é 1.

Para especificarmos o caminho, usamos uma linguagem que trabalha com uma sequência de instruções. A instrução M recebe uma coordenada como argumento e serve para “movimentar o lápis”. A instrução L também recebe uma coordenada como argumento e serve para fazer uma linha. Assim, essa “máquina” mantém um estado e a ordem em que você poe as instruções influencia no resultado.

Caso você tente fazer duas linhas não colineares com o que aprendeu, vai perceber que um triângulo preenchido será desenhado, pois o padrão é que a cor de preenchimento do objeto seja a cor preta. Para desativar o preenchimento, use o atributo fill com o argumento none (fill=”none”).

Hora de aprender a desenhar curvas de Bézier, o momento pelo qual estávamos esperando! O comando Q faz curvas de Bézier de quadráticas (que precisam de 3 pontos, sendo o primeiro o ponto no qual o “lápis” está), enquanto o comando C faz curvas de Bézier cúbicas (que precisam de 4 pontos). Veja o exemplo abaixo (e não fique só olhando os códigos de exemplo, teste-os):

Se você não sabia que vírgulas podem ser utilizadas como separadores de coordenadas, deve ter percebido com esse último exemplo. Outra coisa legal é que se você, em vez de digitar um comando, simplesmente colocar uma coordenada, a “máquina” entenderá como um comando L, por padrão.

Referências

Esse “trabalho” foi baseado no excelente texto de Jeremy Kun publicado no blog Math ∩ Programming, que é licenciado sob a Creative Commons Attribution-NonCommercial 3.0 Unported. Outra fonte importante foi o SVG Primer, hospedado na W3C.

Imagens vindas da Wikipédia e de outras fontes estão “corretamente” linkadas para a fonte original. Demais imagens são autoria própria (e por isso estão mais “feias”).

Programação 3 – uma oportunidade para aprender uma nova linguagem

Para nos avaliar, o professor de programação 3 da UFAL pediu um sistema que é composto por um servidor web que fornece webservices, uma interface web para o mesmo, e também uma interface gráfica.

Ele exigiu inicialmente para implementar em Java, e no começo da matéria eu estava realmente disposto a dar mais uma chance a Java, mas depois de ver swing e o suporte ridículo a metaprogramação que Java fornece, eu insisti o suficiente para que ele me deixasse fazer em outras linguagens.

Então, como ele foi bonzinho, resolvi optar pelo meio-termo. Ele gosta de Java, eu de C++, então vou fazer em Python =p. Devo conhecer só 10% de Python, mas isso é mais que o suficiente para fazer muito mais do que sou capaz de fazer em muitas linguagens, e combinado com o shell ipython e a ide komodo, espero não encontrar grandes dificuldades na linguagem.

Problema da linguagem resolvido, agora preciso de uma framework web (e toda a infraestrutura como servidores web e forma de integração …) para desenvolvimento. Lendo bastante acabei conhecendo o fcgi e o genshi. Eu já conhecia o CGI, mas sempre achei limitado e complicado, mas o fcgi parece ter removido os maiores problemas. Para o servidor, acho que vou colocar o Lighttpd, pelo simples motivo de que ele suporta o fcgi e parece ser fácil de configurar.

A parte mais difícil foi escolher a framework. Descobri os projetos web2py e Django, e ambos aparentam ser tão legais que eu não consigo me decidir entre um ou outro. Um amigo resolveu fazer o trabalho em Python também e tivemos a ideia de cada um fazer usando uma framework diferente para depois fazer uma comparação profunda. Ele ficou com Django e eu fiquei com web2py. Vou ver se é fácil substituir o sistema de templates próprio do web2py pelo genshi.

Tentarei caminhar o máximo no resto que sobrou de hoje e só retornarei a esse projeto próxima semana.

%d blogueiros gostam disto: