Tudo ao mesmo tempo agora: paralelismo com C#

Há cerca de um mês, tive o prazer de apresentar um webinar da Pricefy, falando sobre programação assíncrona com C# .NET. Na ocasião, apresentei conceitos fundamentais de multithreading, I/O assíncrono, Task-based Asynchronous Programming, mostrei como async/await funciona por baixo dos panos e finalizei com código (é lógico!), mostrando exemplos de mau uso de async/await e como usar isso do jeito certo. O conteúdo está bem didático, tenho certeza que mesmo quem não é da turma do C# pode aprender uma coisa ou duas.

https://www.youtube.com/watch?v=ywpEtLht6So

Uma coisa é uma coisa; outra coisa é outra coisa

Durante o webinar, fiz questão de deixar claro que há uma distinção entre o que são tarefas assíncronas e o que são tarefas paralelas. Muito embora, sejam frequentemente usadas em conversas corriqueiras como sendo a mesma coisa, elas não são a mesma coisa.

Obviamente que uma discussão exaustiva sobre o assunto está fora da agenda deste post. Mas vou fazer uma nano desambiguação aqui, para então seguir com o assunto alvo deste post.

Consideramos paralelismo quando temos uma tarefa que leva um certo tempo para ser concluída e desejamos completá-la em menos tempo. Para isso, o requisito basilar é que a tal tarefa seja passível de ser dividida em múltiplos pedaços iguais (ou bem próximo disso) e que se tenha um número de unidades de trabalho de igual capacidade, para que possam trabalhar ao mesmo tempo e com equivalente desempenho.

Um exemplo cotidiano de paralelismo seria dividir a tarefa de descascar 3 kg de batatas entre três pessoas de igual habilidade (e força de vontade!). Digamos que você sozinho leve 3 horas para concluir a tarefa. Okay, o problema é que o jantar é daqui há 1 hora e meia. O que fazer? Dividir a tarefa com aqueles dois amigos que estão sentados no sofá, sem fazer nada, enquanto você prepara tudo sozinho? Sim, essa é uma ideia. Se os dois tiverem a mesma habilidade de descascar batatas que você tem, em aproximadamente 1 hora a tarefa estará concluída e você poderá partir para a próxima ⎼ assar, cozer, fritar, ou o que quer que seja.

Em termos de software, o princípio é o mesmo. Digamos que você tenha, por exemplo, uma lista com 300 itens e tenha que realizar uma determinada operação em cada um deles. Se você tiver três CPUs em seu computador, você pode dividir a lista em três e processar ⅓ em cada CPU.

“Most modern CPUs are implemented on integrated circuit (IC) microprocessors, with one or more CPUs on a single metal-oxide-semiconductor (MOS) IC chip. Microprocessors chips with multiple CPUs are multi-core processors. The individual physical CPUs, processor cores, can also be multithreaded to create additional virtual or logical CPUs.” — Wikipedia.

Note que eu disse “CPU” e não “thread”. Isso porque o paralelismo “de verdade” é obtido com múltiplas CPUs executando threads ao mesmo tempo e não com múltiplas threads sendo escalonadas por uma única CPU. Com múltiplas threads em uma única CPU, temos o que é conhecido como multitarefas “virtualmente simultâneas”.

Por exemplo, quando eu digito algo no teclado do meu computador e como amendoins “ao mesmo tempo”, no grande esquema das coisas, digamos, em uma janela de 10 minutos, alguém pode dizer que estou comendo amendoins ao mesmo tempo em que digito coisas no computador; mas na real, encurtando essa janela de tempo, é possível ver que eu não faço as duas coisas exatamente ao mesmo tempo, mas sim, intercaladamente, mudando de uma tarefa para a outra a cada certo intervalo.

É basicamente assim que as threads funcionam: elas são como “unidades de processamento virtuais”, que uma CPU executa com exclusividade por 30 milissegundos cada. Humanamente falando, imagino que seja impossível perceber essa mudança de contexto; por isso, tudo parece realmente simultâneo para nós.

Assíncrono vs Paralelo

Já no caso da assincronicidade, consideramos assíncrono aquilo que não vai acontecer do início ao fim exatamente agora, que pode ter um curso intermitente, e não queremos ficar sem fazer nada enquanto esperamos por sua conclusão, que acontecerá em algum momento futuro. Isso é tipicamente comum com operações de I/O.

Operações de I/O não dependem apenas de software, obviamente, mas invariavelmente de dispositivos de I/O (a.k.a. hardware), que de algum modo compõe ou complementam um computador, cada qual com seu modo de funcionar, seu tempo de resposta e outras particularidades quaisqueres. Alguns dos dispositivos de I/O mais comuns são: HD, monitor de vídeo, impressora, USB, webcam e interface de rede. Qualquer programa de computador que valha seu peso em sal executa alguma operação de I/O ⎼ mostrar “hello world” em uma tela, salvar um texto qualquer em um arquivo, iniciar um socket de rede, enviar um e-mail, etc.

Voltando à cozinha para mais um exemplo, digamos que o jantar de hoje seja macarrão à bolonhesa e salada verde com tomates cerejas e cebola. Como poderia acontecer a preparação desse cardápio? Bom, eu poderia fazer uma coisa de cada vez, de modo sequencial. Ou poderia tentar otimizar um pouco meu tempo, minimizando o tempo que fico sem fazer nada, aqui e ali, esperando por algum “output” qualquer.

  1. Eu começo colocando uma panela de água para ferver, onde vou cozer o macarrão. Enquanto ela não ferve, eu corto cebola, alho e bacon para refogar a carne moída;
  2. Terminando, a tarefa de pré-preparo, sequencialmente, enquanto ainda espero a água para o macarrão chegar à fervura, começo então a preparar a carne moída ⎼ refogo, coloco temperos diversos, extrato de tomate e deixo cozer em fogo médio;
  3. Vejo que a água começou a ferver, então, acrescento um tanto de sal e ponho o macarrão para cozer. Okay, agora, enquanto o macarrão cozinha por aproximadamente 8–10 minutos e a carne moída também está cozendo, apurando o sabor, o que eu faço? Sento e espero? Não! Ainda tenho que preparar a sala;
  4. Lavo as folhas verdes, os tomates, corto a cebola, junto tudo em uma saladeira (trriiimmm!!!) ouço o alarme indicando que o macarrão está cozido e é hora de escorrê-lo rapidamente, mesmo que tenha que parar o preparo da salada, momentaneamente, afinal de contas, só falta temperar e isso não é algo tão crítico, pode acontecer daqui um pouco; já o macarrão, precisa ser escorrido agora!
  5. Escorro o macarrão, coloco em uma travessa de macarronada, despejo a carne moída por cima, misturo cuidadosamente e finalizo ralando uma generosa quantidade de queijo parmesão por cima;
  6. Levo a travessa de macarronada para a mesa de jantar, volto à cozinha, tempero a salada rapidamente e levo para mesa também;
  7. Tá na mesa, pessoalll!!!

Vê como tudo aconteceu de modo predominantemente assíncrono, porque cada preparo teve seu tempo e sua prioridade? Tudo aconteceu de modo intercalado. Foram 40 minutos intensos, sem ficar um minuto parado sem fazer nada, mas aproveitei muito melhor o meu tempo.

É basicamente assim que funcionam as operações de I/O assíncronas: uma única thread é capaz de despachar milhares de operações de leitura ou escrita para os diversos dispositivos de hardware de um computador, conforme as requisições vão chegando; e enquanto as respostas dos dispositivos não chegam, indicando que as operações foram bem sucedidas ou não, elas vão atendendo a outras requisições; e assim seguem, em um loop semi-infinito.

Por que uma thread ficaria parada, bloqueada, esperando pela resposta de uma escrita em um socket, que pode levar certo tempo, enquanto poderia estar escrevendo algo em um arquivo no disco rígido? Essa é a magia do I/O assíncrono in a nutshell. Depois, assista à minha talk no YouTube, que lá eu me aprofundo mais no assunto; não quero me repetir aqui.

Como você provavelmente já notou, tanto a abordagem paralela, quanto a assíncrona, são maneiras de se implementar concorrência em uma aplicação, cada qual com sua finalidade. A primeira, envolve threads de execução em múltiplas CPUs simultâneas; a segunda se baseia em máquinas de estado, futures e callbacks, executando possivelmente em uma única thread.

Espero que essa introdução tenha sido suficiente para ficarmos todos na mesma página.

Paralelismo está no menu hoje

O assunto da vez hoje é paralelismo com C#. E para continuar se aprofundando no assunto, uma visão de alto-nível da arquitetura de programação paralela oferecida pela plataforma .NET.

Fonte: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/

Eu acho importante começar com essa big picture, porque infelizmente, a plataforma .NET não ajuda muito com aquela distinção de assíncrono vs paralelo, que vimos há pouco. A razão disso é que a classe Task é o ponto de partida da Task Parallel Library (TPL) tanto para algoritmos assíncronos quanto para paralelos.

A documentação sobre Task-based Asynchronous Programming (TAP) define “task parallelism” como “uma ou mais tarefas independentes executando concorrentemente”, o que soa um pouco fora do que vimos há pouco.

Já a documentação sobre Parallel Programming in .NET (fonte do diagrama acima), por sua vez, diz que “muitos computadores pessoais e estações de trabalho têm vários núcleos de CPU que permitem que várias threads sejam executadas simultaneamente”, e então, conclui dizendo que “para aproveitar as vantagens do hardware, você pode paralelizar seu código para distribuir o trabalho entre vários processadores”. Isso faz mais sentido para mim.

Acredito que isso se dê pelo fato de que eles quiseram implementar uma abstração única de tarefas independentes em cima do modelo de threads, que são executadas a partir de um thread pool, que por sua vez, dispara a execução delas em um número de CPUs disponíveis ⎼ por padrão, o número mínimo de threads do ThreadPool é equivalente ao número de CPUs disponíveis no sistema.

O que vale é a intenção

Uma dica de ouro, que para mim ajuda bastante, é olhar para a classe Task sob a ótica daquilo que tenho a intenção de implementar.

  • Os membros .Result.Wait().WaitAll().WaitAny() são bloqueantes (a.k.a. síncronos) e devem ser usados somente quando se tem a intenção de implementar paralelismo. Tarefas paralelas estão relacionadas ao que chamamos de CPU-bound e devem ser criadas usando preferencialmente os métodos .Run().Factory.StartNew();
  • Além da palavra-mágica await, usada para aguardar assíncronamente a conclusão de um método assíncrono (na prática, uma instância de Task ou Task<T>), os métodos .WhenAll().WhenAny(), que não são bloqueantes (a.k.a. assíncronos), devem ser usados quando a intenção for implementar assincronicidade. Tarefas assíncronas estão relacionadas ao que chamamos de I/O-bound.

A propósito, TaskCreationOptions.AttachedToParent pode ser usada na criação de uma tarefa paralela (estratégia “divide and conquer” / “parent & children”), mas não por uma tarefa assíncrona. Tarefas assíncronas, de certo modo, já criam sua própria “hierarquia” via await.

Okay, vamos focar em paralelismo a partir daqui e ver um pouco de código.

Colocando a mão na massa

Como vimos há pouco, tarefas paralelas vão bem para implementar processos que sejam CPU-bound; ou seja, que levam menos tempo para conclusão em função do número de CPUs disponíveis para particionamento/execução do trabalho.

O código fonte dos exemplos está neste repositório aqui.

Podemos dividir esse cenário em duas categorias de processamento:

  • Estático — data parallelism
  • Dinâmico — task parallelism

Processamento paralelo “estático”

Chamamos essa categoria de estática, porque se trata de iterar uma coleção de dados e aplicar um dado algoritmo em cada um de seus elementos. Para isso, a TPL nos oferece três métodos a partir da classe Parallel.

1. For() ⎼ análogo ao for clássico

Repare nos parâmetros passados na invocação do método Parallel.For(), na linha 14. Além dos típicos limites inicial e final, há também uma Action<int, ParallelLoopState>, que provê o índice da iteração atual e um objeto com o estado do loop. É a partir deste objeto de estado que solicitamos um “break” no loop.

private static void ParallelFor()
{
var numberOfIterations = 1000;
var min = 0;
var max = 100_000_000;
var random = new Random();
var breakIndex = random.Next(1, numberOfIterations);
Console.WriteLine($"Generating random numbers from {min} to {max} over {numberOfIterations} iterations");
Console.WriteLine($"Random break index: {breakIndex}");
var result = Parallel.For(1, numberOfIterations, (i, state) =>
{
Console.WriteLine($"– Iteration #{i} > Begin at thread #{Thread.CurrentThread.ManagedThreadId}, task #{Task.CurrentId}");
// Has .Break() been called by another parallel iteration?
if (state.ShouldExitCurrentIteration)
{
// Is this current iteration greater then the one where .Break() was invoked?
if (state.LowestBreakIteration < i)
{
Console.WriteLine($"– Iteration #{i} > Will exit now <———–");
return;
}
}
int num;
// A naive lock for educative purpose only
lock (random)
{
num = random.Next(min, max);
}
// If it got to the break index, invokes .Break() to prevent further iterations
if (i == breakIndex)
{
Console.WriteLine($"– Iteration #{i} > Got to break index <———–");
state.Break();
}
Console.WriteLine($"– Iteration #{i} > End: {num}");
});
if (result.LowestBreakIteration.HasValue)
Console.WriteLine($"Lowest break iteration? {result.LowestBreakIteration}");
}
view raw parallel_for.cs hosted with ❤ by GitHub

Uma outra coisa a se notar é o seguinte: diferente de um loop for clássico, que acontece sequencialmente, este Parallel.For() pode ter sido escalonado para rodar em múltiplos processadores simultaneamente; portanto, um break não é imediato, evitando que a próxima iteração aconteça. Pelo contrário, é bem provável que um número de iterações tenham sido iniciadas em um breve momento anterior ao .Break() ser invocado. É por isso que, na linha 19, precisamos checar se podemos ou não continuar a iteração atual.

E finalmente, note que na linha 32 há um lock da variável random. Isso é necessário por se tratar de um processo paralelo, que vai potencialmente mutar essa variável concorrentemente ao longo das iterações. Idealmente, você vai evitar esse tipo de cenário, porque onde há lock, há contenção; e onde há contenção, há tempo despendido esperando. Você não quer isso, mas às vezes é preciso.

— A little break here —

Acho que esse é o momento ideal para dizer que paralelismo pode criar situações de concorrência, intencional ou acidentalmente, dependendo do que você está implementando — porque programação paralela é um tipo de multithreading; e multithreading é um tipo de concorrência.

Se você ficou com dúvidas sobre isso, se ficou confuso com a terminologia, se acha que é tudo a mesma coisa, ou algo assim, tudo bem, não se desespere. Eu sei que, como acontece com assincronicidade e paralelismo, concorrência e paralelismo também são confundidos o tempo todo.

O artigo da Wikipedia sobre concorrência (em inglês) traz um ótimo resumo do Rob Pike, que distingue bem uma coisa da outra: “Concurrency is the composition of independently executing computations, and concurrency is not parallelism: concurrency is about dealing with lots of things at once but parallelism is about doing lots of things at once. Concurrency is about structure, parallelism is about execution, concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.”

Se tiver um tempo extra, recomendo que você veja a apresentação do Rob Pike, Concurrency is not Parallelism, para ter uma introdução amigável ao assunto.

2. ForEach() ⎼ análogo ao foreach clássico

Semelhantemente ao que se pode fazer com um loop foreach, aqui, Parallel.ForEach() está recebendo um IEnumerable<int> provido por um generator (a.k.a. yield return). Além deste parâmetro, há ainda outros dois: um com opções de configuração de paralelismo e uma Action<int, ParallelLoopState, long>, que pode ser usada como no exemplo anterior.

O ponto de destaque neste exemplo vai para a linha 29, onde é definido um CancellationToken para o loop. Naturalmente, o objetivo do CancellationToken é sinalizar o cancelamento de um processo que está em curso. E neste caso, o cancelamento ocorre na linha 18, depois de um intervalo randômico ⎼ to spice it up.

private static void ParallelForEach()
{
var numberOfIterations = 1000;
var min = 0;
var max = 100_000_000;
// Will cancel the operation on a random time basis; it might complete sometimes
// depending on the host machine
var timeInMs = 0;
var cts = new CancellationTokenSource();
Task.Run(async () =>
{
timeInMs = new Random().Next(100, 1000);
await Task.Delay(timeInMs);
Console.WriteLine($"– Will cancel after {timeInMs} ms <———–");
cts.Cancel();
});
Console.WriteLine($"Generating random numbers from {min} to {max} over {numberOfIterations} iterations");
try
{
Parallel.ForEach(
GenerateRandomNumbers(numberOfIterations, min, max),
new ParallelOptions
{
CancellationToken = cts.Token,
//MaxDegreeOfParallelism = 100 // <– you can play with this number to get a grasp of how this works;
// // essentially, the max viable degree of parallelism is the number
// // of cores available; try a few numbers and watch it for time to
// // complete the work
},
(num, state, i) =>
{
Console.WriteLine($"– Iteration #{i} > Begin at thread #{Thread.CurrentThread.ManagedThreadId}, task #{Task.CurrentId}");
Console.WriteLine($"– Iteration #{i} > End: {num}");
});
}
catch (OperationCanceledException)
{
Console.WriteLine($"Parallel task was canceled by a CancellationToken after {timeInMs} ms");
}
}

Este é um pattern bastante comum na plataforma .NET, que você já deve estar bem acostumado, pois se vê isso por toda parte da biblioteca padrão.

3. Invoke() ⎼ neste caso, a coleção iterada é de algoritmos

Este caso é um pouco diferente dos dois anteriores. Nos casos anteriores, o problema que você estava querendo resolver era o seguinte: você tinha uma coleção de itens sobre os quais você queria aplicar um dado algoritmo, e para fazer isso o mais rápido possível, você queria tirar proveito do número de CPUs disponíveis e escalonar o trabalho entre elas.

Agora, neste caso, o problema é que você tem um número de algoritmos para executar (independentes uns dos outros, de preferência) e gostaria de fazer isso o mais rápido possível. A solução, no entanto, é essencialmente a mesma.

private static void ParallelInvoke()
{
var numberOfIterations = 1000;
var min = 0;
var max = 100_000_000;
Console.WriteLine($"Generating random numbers from {min} to {max} over {numberOfIterations} iterations");
var nums = GenerateRandomNumbers(numberOfIterations, min, max).Select(num => (long)num).ToArray();
// With Parallel LINQ it is a piece of cake — fast as f*k
var originalSum = nums.AsParallel().Sum();
Parallel.Invoke(
() => ExpensivePlusOne(ref nums, 0, nums.Length / 2),
() => ExpensiveMinusTwo(ref nums, nums.Length / 2, nums.Length)
);
var newSum = nums.AsParallel().Sum();
Console.WriteLine($"Sum of all random generated numbers are {originalSum} (original) and {newSum} (new) [{originalSum newSum} less]");
}

Um bônus no código acima, que não tem exatamente a ver com a questão do .Invoke(), mas que é super interessante e vale a pena comentar, é o método .AsParallel() sendo invocado no long[], na linha 13. Este método é parte da chamada Parallel LINQ (PLINQ), que torna possível a paralelização de queries LINQ.

Como você já deve imaginar, o que a PLINQ faz é particionar a coleção em um número de segmentos, e então, executar a query em worker threads separadas, em paralelo, usando os processadores disponíveis.

A propósito, assim como os dois métodos anteriores, .Invoke() também suporta CancellationToken via ParallelOptions.

Processamento paralelo “dinâmico”

Chamamos essa categoria de dinâmica, porque não se trata de iterar em uma coleção e aplicar um determinado algoritmo; também não se trata de invocar uma lista de métodos em paralelo. Na verdade, trata-se de iniciar uma nova Task (ou mais de uma), que vai executar um processo custoso em uma worker thread, em paralelo, se possível, e poderá iniciar outras Tasks “filhas” a partir dela, criando uma hierarquia, onde a tarefa mãe só será concluída quando suas filhas tiverem concluído.

Inicia, se divide, trabalha, converge e finaliza.

Essa é a uma categoria de paralelismo em que, questões como: quantas tarefas mães, quantas tarefas filhas, que processos cada uma delas realiza, em que circunstâncias, em que ordem, etc, etc, etc, são todas respondidas em runtime, de acordo com as regras xpto de cada caso de uso. Daí referir-se a ela como dinâmica.

Os métodos da classe Parallel e a PLINQ são super amigáveis, convenientes, e você deve tentar usar sempre que possível. Mas quando o problema for um tanto mais flexível, dependente de informações conhecidas somente em runtime, o negócio é partir para Task. Por exemplo, você precisa percorrer uma estrutura de árvore e, dependendo do nó, executar um processo ou outro.

private static void ParallelParentAndChildren()
{
var numberOfIterations = 1000;
var min = 0;
var max = 100_000_000;
Console.WriteLine($"Generating random numbers from {min} to {max} over {numberOfIterations} iterations");
var nums = GenerateRandomNumbers(numberOfIterations, min, max).Select(num => (long)num).ToArray();
var task = Task.Factory.StartNew(
() => ProcessNumbers(nums), // <– let's pretend it is a so freaking CPU-intensive method
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default
);
Console.WriteLine("Spawned parallel process of numbers and will wait until completion");
task.Wait();
Console.WriteLine("Parallel process of numbers has completed");
}
private static void ProcessNumbers(long[] nums)
{
var odds = new ConcurrentBag<long>();
var evens = new ConcurrentBag<long>();
Console.WriteLine("Will divide odds / evens in parallel");
Parallel.ForEach(nums, (num) =>
{
if (num % 2 != 0)
odds.Add(num);
else
evens.Add(num);
});
Task.Factory.StartNew(
() => ExpensiveWorkWithOdds(odds.ToArray()),
CancellationToken.None,
TaskCreationOptions.AttachedToParent, // <– pay attention at here
TaskScheduler.Default
);
Console.WriteLine("Spawned parallel expensive work on odds");
Task.Factory.StartNew(
() => ExpensiveWorkWithEvens(evens.ToArray()),
CancellationToken.None,
TaskCreationOptions.AttachedToParent, // <– pay attention at here
TaskScheduler.Default
);
Console.WriteLine("Spawned parallel expensive work on evens");
}

Note que o exemplo acima usa o método Task.Factory.StartNew(), para criar e disparar a execução de instâncias de Task, e não o famoso Task.Run(), que estamos bem acostumados a usar, quando estruturamos aplicações para lidar com problemas de modo concorrente.

O caso é que, o método .Run(), na real, nada mais é do que uma maneira conveniente de invocar.StartNew() com parâmetros “padrões” (veja o fronte dele aqui). E como você já deve imaginar a essa altura do campeonato, é justamente por conta desses parâmetros “padrões” que ele não atende aos requisitos do exemplo acima. Bem, para ser mais específico, estou me referindo ao parâmetro TaskCreationOptions.DenyChildAttach, que nos impede de ter tarefas filhas. Quer testar isso? Substitua .StartNew() por .Run() na linha 12, ou então, o parâmetro TaskCreationOptions.None por TaskCreationOptions.DenyChildAttach, na linha 15, e veja o que acontece ⎼ repare bem na ordem das saídas de console.

Viu?

A regra então é: sempre que for criar e disparar a execução de uma Task, priorize .Run(), a menos que o caso que você vai implementar peça por parâmetros específicos, diferentes dos padrões. Nestas circunstâncias, use .StartNew() e configure a gosto.

[Para ser mais correto com a informação que estou entregando, eu tenho que dizer que, nem o método .Run(), nem o método .StartNew(), “disparam” a execução de uma Task. O que eles fazem, na verdade, é “colocar a Task na fila de execução do ThreadPool, através de um TaskScheduler”. Veja que a própria classe Task tem uma propriedade Status que ajuda a entender seu o ciclo de vida.]

Dica #1 ⎼ Long Running Tasks

Podemos dizer que uma Task é long-running quando ela permanece rodando por minutos, horas, dias, etc. Ou seja, não é uma tarefa de alguns poucos segundos de duração. Para isso, há a opção TaskCreationOptions.LongRunning, que pode ser passada para o método .StartNew().

O que essa opção faz é sinalizar ao TaskScheduler que se trata de um caso de oversubscription, dando a chance do TaskScheduler se precaver contra um fenômeno conhecido como “thread starvation”. Para tanto, duas ações são possíveis ao TaskScheduler: (1) criar mais threads do que o número de CPUs disponíveis; (2) criar uma thread adicional dedicada à Task, para que assim, ela não engargale o fluxo natural de trabalho das worker threads do ThreadPool.

Até aí, tudo bem. Problema resolvido. Espere. Mesmo?

Tarefas com corpo async, ou seja, que seu delegate await por outras tarefas, por si só, naturalmente, já possuem um fluxo de execução escalonado de acordo com sua máquina de estado. Portanto, elas não consomem uma mesma thread por muito tempo; cada passo da máquina de estado pode ser executado em uma thread diferente.

Se você não sabe do que estou falando, veja a minha talk.

O que fazer então? A menos que seu processo long-running não use await, não aguarde a execução de nenhuma Task, seja lá de que maneira, esqueça a opção TaskCreationOptions.LongRunning. Use .Run() e deixe que o TaskScheduler faça seu trabalho autonomamente ⎼ em raros casos ele realmente precisa de “dicas”.

Dica #2 ⎼ Exceções

Sim, elas existem e uma hora ou outra vão ocorrer. Lide com isso. Quero dizer, literalmente!

Quando você estiver trabalhando com tarefas aninhadas, parent & children, como no exemplo que vimos há pouco, quando ocorrer uma exceção, você vai receber uma AggregateException borbulhando. Não importa se você await ou .Wait() a Task, uma AggregateException é o que você vai ter. Sendo assim, muito do que eu falo sobre o uso de .Wait() e exceções na minha talk não se aplicam a esse tópico de hoje. Mas isso não significa que você deva preferir .Await() em lugar de await ⎼ pelo contrário. Evite bloquear a “thread chamadora” desnecessariamente.

Uma dica para lidar com isso é usar o método .Flatten() da AggregateException. A grosso modo, o que esse método faz é criar uma única exceção com a exceção original incluída na propriedade InnerExceptions. Assim você não tem que ficar iterando por exceções aninhadas.

Outra dica é usar o método .Handle().

Dica #3 ⎼ Cuidado com aplicações web

Programação paralela visa aumentar o uso das CPUs, temporariamente, com intuito de aumentar o throughput da aplicação. Ótimo! Queremos isso. Afinal, muitos desktops ficam com a CPU idle boa parte do tempo. Mas cuidado, porque isso não é sempre verdade quando se trata de servidores.

Servidores rodando aplicações ASP.NET, por exemplo, podem ter menos tempo de CPU idle, dependendo do volume de requisições simultâneas, porque o próprio framework lida com múltiplas requisições em paralelo, visando tirar melhor proveito do ambiente multicore disponível. Portanto, botar código que atende a requisições de clientes para rodar em paralelo, pode acabar sendo um baita de um tiro no pé, quando houver um grande volume de requisições simultâneas.

Além disso, o ASP.NET pode limitar o número de threads usadas até mesmo pela TPL. Sendo assim, até mesmo um Task.Tactory.StartNew() da vida pode ser uma senhora martelada no dedão.

Via de regra, muito cuidado com multithreading em aplicações web.

Concluindo

Ao longo deste mais do que longo post, diferenciamos tarefas assíncronas de tarefas paralelas, nos aprofundamos nas paralelas e vimos algum código para ilustrar a discussão. Também vimos que concorrência não significa necessariamente paralelismo. Mas pode vir a ser, eventualmente.

https://github.com/leandrosilva/parallel-programming-article

Se você chegou até aqui e gostou, por favor, compartilhe com seus amigos, deixe seu comentário, suas dúvidas, e vamos nos falando.

Se mesmo tendo visto o meu webinar, você ainda tiver dúvidas sobre async/await, bota suas dúvidas aqui nos comentários. Quem sabe não consigo te ajudar? Não custa tentar.

Ah! E por falar em webinar, talvez eu faça um webinar discutindo o conteúdo deste post. Ou talvez me aprofunde mais no assunto concorrência, algo nessa linha. Seria a última peça do quebra-cabeças. Vou pensar a respeito.

Até a próxima!

BTW, estamos contratando 🙂

Se você ainda não conhece a nossa stack e quer saber como você poderia nos ajudar a construir a Pricefy, que foi recentemente adquirida pela Selbetti, dá uma checada nesses bate-papos:

  • ProdOps ⎼ Engenharia e Produto com Leandro Silva (link 1 e link 2);
  • ElvenWorks ⎼ Conhecendo a tecnologia por trás de uma solução muito inteligente de Precificação (link).

Se tiver interesse: leandro.silva@pricefy.com.br. A gente segue de lá.

Colocando o papo em dia com C#

Lá pelo idos de 2000, quando tive meu primeiro contato com C#, eu era um jovem “programador Java”, com alguns poucos anos de experiência em Delphi, sonhando com um futuro onde Java seria ubíquo e nada mais seria importante. Mas C#, em especial, capturou minha atenção por estar sendo desenvolvida por Anders Hejlsberg, o mesmo cara que já havia criado o Turbo Pascal e o Delphi, que eu era muito fã ⎼ e ainda tenho certa nostalgia, vez ou outra, confesso.

Na época, achei que não passava de um Microsoft copycat do Java e segui a vida.

O reencontro

Fast forward uns 9 anos e lá estava eu, na Locaweb, assumindo a gestão de um time que era quase 100% baseado em C#, depois de quase uma década sem dar qualquer 5 minutos de atenção à linguagem ou à plataforma .NET em si.

A primeira surpresa que tive foi que .NET ainda não tinha ferramentas robustas de build, gerenciamento de artefatos, dependências, etc e tal, como tínhamos Maven (a.k.a. The Official Internet Download Tool), Ant, Ivy, Hudson (a.k.a. Jenkins) e Artifactory na comunidade Java. Isso, obviamente, dava espaço para que diversos problemas emergissem do desenvolvimento de aplicações distribuídas, como era o nosso caso. Problemas estes, que a comunidade Java já tinha passado há anos e coçado a sua própria coceira.

Uma das minhas primeiras iniciativas no time, então, foi dar apoio aos ótimos engenheiros de software que tínhamos, para que eles resolvessem esse problema. (Alguns de nós éramos programadores Java experientes, by the way.) Além de liberar certo tempo nas sprints, para que pudessem se dedicar a isso, também contribui com algum código. E foi dessa iniciativa que nasceu o projeto IronHammer ⎼ curiosamente, escrito em Ruby, não C#. Assunto para um outro momento.

Fonte: https://github.com/leandrosilva/Iron-Hammer

A segunda surpresa que tive foi o quanto C# tinha evoluído e se tornado, na minha opinião, um Java melhor (a linguagem em si, não a plataforma), o que contrastava com a questão do ferramental precário, que me chamou atenção inicialmente. O próprio Visual Studio era super precário; sem ReSharper, ele não era muito mais do que um Notepad glorificado.

Propriedades auto-implementadas, inferência de tipos (var), expressões lambda, LINQ (que é uma das coisas mais fantásticas do .NET), extension methods, tipos anônimos, etc, eram features que C# já possuía há pelo menos 2 anos quando tomei conhecimento. E passados alguns poucos meses, quando eu mal tinha emergido da minha imersão em C# 3, veio o C# 4, trazendo dynamic e parâmetros opcionais, entre outras coisas.

C# 4.0 Breaking News! ⎼ Tech talk que fiz com um colega à época

Como eu queria ter tido essas features nos meus últimos anos de Java, quando eu estava completamente vidrado por linguagens dinâmicas e programação funcional (Ruby e Erlang).

É bom que se reconheça uma coisa: se a linguagem Java foi a grande influência da criação e dos primeiros anos de vida do C#, muito de sua evolução posterior se deu por influência de F#, que apesar de mais jovem, tem em seu DNA a família ML de linguagens, descendendo diretamente de OCaml. Uma bagagem e tanto.

Ela segue em frente

Fast forward mais uma década e aqui estou eu, construindo a Pricefy há 5 anos, mantendo uma base de código majoritariamente C#, e mais uma vez impressionado com o quanto a linguagem evoluiu desde a última vez que parei para refletir a respeito. Do async/await do C# 5 aos avanços de pattern matching e top-level statements do C# 9, o que fica muito claro para mim é que a evolução da linguagem continua a todo vapor, mantendo-a jovial e moderna. Até quando? Não sei. É verdade que tem gente que já está reclamando que a linguagem está ficando complicada demais e inchada. Aliás, essa não é uma reclamação nova, sejamos francos; já tem certo tempo que essa questão tem sido levantada. Há até quem diga que está se transformando em C++.

Mas a verdade é que não temos que usar todas as features e capacidades de uma linguagem, sejam elas novas ou de berço, simplesmente porque elas estão lá, disponíveis. Há tempo e utilidade para cada funcionalidade ⎼ “à moda antiga” não é automaticamente errado.

No caso específico de C#, que estamos discutindo, a esmagadora maioria das novidades que chegaram ao longo dos anos foram para simplificar o código que se escreve e não para deixá-lo mais complicado. O mesmo vale para C++, diga-se de passagem.

Como as coisas foram ficando mais simples ao longo dos anos.

Complicado é tentar guardar na cabeça cada uma das zilhares de funcionalidades da linguagem ou as trocentas maneiras de se fazer a mesma coisa.

Honestamente? Eu não sei todas as features da linguagem. Não sei. Eu provavelmente nem devo saber, de bate pronto, assim, pá pum, todas as maneiras de se declarar e atribuir uma variável qualquer para salvar a minha própria vida. Isso porque, é quase impossível bem difícil você “modernizar” sua base de código de negócio pari passu com a linguagem de programação (ou mesmo framework) que você usa. Primeiro, porque com o passar dos anos a base de código vai ficando cada vez maior e essa “modernização” nem sempre vai trazer um retorno substancial para o problema de negócio, que o código se propõe a resolver, em função do esforço necessário para a tal “modernização”. Segundo que, vintage é cool. Não, é brincadeira. (Mas é verdade.)

O meu ponto é: manter uniformidade na base de código é mais importante e traz mais valor real ao negócio do que manter a base de código atualizada com as últimas novidades da linguagem. Um código uniforme, que qualquer dev do time navega bem e se sente em casa para implementar novas funcionalidades, modificar funcionalidades existentes e corrigir bugs, na minha cartilha, é mil vezes mais importante. Ou bem próximo disso.

Prefira a homogeneização

O que eu advogo é ir incorporando à base de código novidades da linguagem, especialmente, syntactic sugars, de maneira homogênea, para evitar criar uma colcha de retalhos. Sabe aquela base de código, que se você abrir três arquivos diferentes, parece que foram três pessoas de três lugares remotos do planeta que escreveram? Pois é. Em um arquivo, todas as variáveis são declaradas com anotação de tipo (a.k.a. tipo à esquerda); no outro, todas as variáveis são declaradas com inferência de tipo (var); e no último, há uma mistura dos dois estilos de sintaxe. E você, precisando editar o arquivo fica: e agora, que estilo devo seguir?

Eu sou do tipo que, em geral, segue o padrão do arquivo e mantém o estilo dele, seja este qual for. Exceto quando o dito arquivo não tem padrão nenhum ou não segue as convenções gerais do projeto; aí eu mudo, para enquadrá-lo no estilo majoritário do projeto.

Uma boa abordagem para solucionar este exemplo é ir homogeneizando em favor de var tanto quanto possível. Abriu um arquivo para editar, viu um trecho de código que está fora do “padrão”, refatora, padroniza. Um padrão ruim ainda é melhor do que a ausência de um padrão, porque se no futuro o time decidir mudar o padrão, vai ser muito mais fácil do que se não houvesse padrão algum.

Que tal um outro exemplo? Você precisa implementar uma nova funcionalidade na aplicação e, no processo, se depara com um método que possui uma cadeia de ifs relativamente complexa e tudo mais. Aí, analisando, você vê que o problema não é tão complexo quanto a implementação da solução. Você vê que a implementação poderia ser drasticamente simplificada usando pattern matching, que você andou estudando, mas em nenhum outro lugar da aplicação há uso de pattern matching ainda, muito embora o runtime da aplicação dê suporte a isso. O que você faz? Refatora. Porque vai haver ganho para o negócio. Quero dizer, a próxima pessoa que precisar dar manutenção neste código vai fazer isso de maneira mais confiante e rápida.

Fonte: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns

Um último exemplo que gostaria de dar aqui é o caso de interpolação de strings, que é super mundano. C# faz interpolação de strings de maneira elegante desde a versão 6, se não estou enganado, mas ainda é comum ver código por aí que não se beneficia disso, o que é uma pena. Porque além do código ficar mais limpo, quando você faz interpolação de strings, o compilador, gratuitamente, gera para você o código equivalente usando string.Format, que no final das contas, vai usar um StringBuilder. De graça e elegantemente. Quem não quer isso?

As minhas favoritas

Se você acompanhou meu rant até aqui, provavelmente, já teve uma ideia das minhas features favoritas do C# “moderno”, se é que posso chamar assim. Além delas, vou citar mais algumas.

Inferência de tipos

De longe, eu acho que o que eu mais gosto da linguagem C# é a inferência de tipos que ela proporciona o tempo todo (uma salva de palmas para a dupla “linguagem & compilador”), tanto na declaração de variáveis (var / dynamic), quanto na invocação de métodos que possuem type parameters (a.k.a. generics).

Out variables

Eu odiava quando tinha que declarar uma variável antes de usá-la em um método que tem parâmetros out.

Declaração prévia.

Mas aí, finalmente, no C# 7, alguém teve a brilhante ideia de resolver isso, permitindo que se declarasse variáveis out direto da invocação de métodos com parâmetros out.

Declaração concomitante ao uso.

Named Tuples, Discarts e Deconstruction

Tuplas estão entre minhas construções favoritas em qualquer linguagem de programação. Meu primeiro contato com elas foi em Scala, depois em Erlang. Para expressar retorno de métodos e aplicar pattern matching, por exemplo, tuplas são extremamente poderosas. Junte a isso a capacidade de fazer desconstrução de objetos e descartar valores que não importam no momento e você tem uma oportunidade fantástica de escrever código limpo e expressivo. C# 7 nos deu isso.

Fonte: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7#tuples-and-discards

O snippet que apresentei na seção anterior, falando de pattern matching, é outro exemplo prático e elegante do uso de tuplas.

Throw Expressions

Essa feature, também do C# 7, tornou bastante prático, quando é preciso checar se um determinado identificador contém um valor e, se sim, atribuí-lo a um outro identificador; ou senão, lançar uma exceção.

Fonte: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/throw#the-throw-expression

Anteriormente, esse código obrigaria um tipo de if-else.

Null-coalescing Assignment

C# 6 havia introduzido Null-conditional Operator (.? e []?) na linguagem com bastante sucesso, eliminando uma série de expressões check-before-access. C# 8, por sua vez, trouxe um novo syntactic sugar para ir um pouco além (????=), super útil para fazer check-then-assign. Trocando em miúdos e exemplificando com o código abaixo, o que isso significa é que, usando o operador ??=, o valor da direita só será atribuído à variável da esquerda, se esta variável for nula.

That sugar, baby!

Record Types

Para fechar, uma feature mais recente, do C# 9, que eu sinceramente ainda não apliquei em códigos de produção. Diferente das structs, que são value types que todos nós conhecemos de longa data, os records são, na verdade, syntactic sugar para classes que implementam uma série de funcionalidades que favorecem o emprego de “imutabilidade” de dados, como é o caso do operador with, que faz cópia com modificação (a.k.a. Nondestructive Mutation). É isso. Embora, eles também possam ser mutáveis, seu objetivo principal é ser immutable & data-centric types.

Declaração em uma linha, via positional syntax, checagem de igualdade de valor e nondestructive mutation, usando with.
Records são imutáveis por padrão, quando declarados via positional syntax.

De início, quando comecei a ouvir falar dessa feature, achei que fosse só hype e pensei: já temos struct e class, para que mais um tipo? Mas quando fui me informar melhor, estudar a proposta, enxerguei benefícios práticos.

Essa é, portanto, uma feature que vejo como forte candidata a ser introduzida em bases de código mais datadas. E isso, porque ela diminui a quantidade de código que você tem que escrever para ter certas garantias e comportamentos por vezes desejados (imutabilidade, igualdade de valor, etc). C# 9 faz isso de graça, sem que você tenha que escrever um tanto de código boilerplate, que teria que dar manutenção depois.

Conclusão

C# evoluiu demais nos últimos anos. Especialmente, na última década.

A linguagem tem ficado mais complexa? Tem. Mas não é o tipo de complexidade que você e eu somos obrigados a lidar no nosso dia a dia. É complexidade que os desenvolvedores da linguagem precisam lidar. Você e eu podemos, ou não, usar as novas features que ela oferece; que na maioria dos casos, insisto, só facilitam o nosso trabalho.

Você não precisa guardar toda a sintaxe da linguagem na sua cabeça. Há coisas melhores para ocupar a nossa memória. Apenas lembre-se de sempre checar se não há uma maneira melhor ou mais simples de fazer a tarefa da vez.

Tecnologia, Produtos, Pricefy e afins

Andei batendo um papo muito bacana sobre a construção da Pricefy, engenharia de produtos digitais e a vida real em produção, com Christiano Milfont e Bruno Pereira, dois veteramos da tecnologia, que muita gente conhece desde os bons tempos de GUJ.

Para quem ainda não conhece o canal deles no YouTube, recomendo bastante dar um checada. Tem muita entrevista legal lá, com outros veteranos da área. Vale a pena conferir.

gRPC no Browser

Nos últimos 4 anos e pouco, a maior parte do código que lido no meu dia a dia profissional na Pricefy é C# .NET. É verdade que tem um tanto de JavaScript no front-end e Node.js em pontos específicos do back-end, mas a maioria esmagadora é .NET mesmo. Por isso não escrevo muito sobre .NET e procuro fazer meus projetos pessoais em outras tantas linguagens que também me interessam. Não porque não gosto de C#, mas porque não gosto só de C#.

Eu gosto de exercitar minha “mente de programador” em paradigmas diferentes e tentar caminhos alternativos para chegar ao mesmo lugar; às vezes mais rápidos, às vezes mais eficientes e outras vezes até mais elegantes. Com sorte, tudo isso junto.

Eu gosto disso.

Hoje, então, é um desses raros episódios em que C# aparece no blog.

O que é gRPC afinal?

Parafraseando longamente o próprio site oficial, gRPC é um framework de RPC open source, moderno, de alto desempenho, que pode ser executado em qualquer ambiente. Ele pode conectar serviços de forma eficiente dentro e entre data centers, com suporte plugável para balanceamento de carga, tracing, verificação de integridade e autenticação. Também é aplicável na última milha da computação distribuída para conectar dispositivos, aplicativos móveis e navegadores a serviços de back-end.

Inicialmente, foi desenvolvido pelo Google, lá pelos idos de 2015, juntando duas peças fundamentais já existentes: HTTP/2, para fazer o transporte, e Protocol Buffers, como IDL e método eficiente de serialização/deserialização de dados. Protobuf, para os mais chegados, também foi desenvolvido no Google.

Muitas empresas usam massivamente gRPC para otimizar a comunicação de microsserviços. Netflix e Spotify são algumas delas.

Também é muito empregado no segmento de Blockchain ⎼ eu mesmo, quando escrevi meu I.N.A.C.A.B.A.D.O. blockchain de brinquedo, há uns 3 anos, também lancei mão de gRPC em Go.

Complexidade de uso

No back-end e dispositivos móveis, seu uso é trivial. Você escreve um arquivo .proto que define o serviço (IDL), usa uma ferramenta para gerar código para uma determinada linguagem que vai implementar o servidor do serviço e para outras N linguagens que vão implementar o cliente do serviço. Voilà.

Concept Diagram
Fonte: Introduction to gRPC (link)

Já em navegadores a coisa não é tão simples assim.

Por se tratar de um protocolo que faz uso avançado de HTTP/2, seu uso em navegadores é mais “complicado” em servidores, para dizer o mínimo.

Basicamente, o que você precisa é ter um proxy na frente, fazendo traduções do cliente rodando no browser para o serviço no servidor. Não é o fim do mundo, mas com certeza passa longe do trivial. Mais sobre isso adiante.

Para tentar facilitar seu uso em navegadores, surgiu a iniciativa gRPC-Web.

O protocolo gRPC-Web

Sem muito rodeio, indo direto ao que importa, o que o protocolo gRPC-Web faz é prover uma API Javascript para navegadores baseada na mesma API do Node.js, mas por baixo dos panos, implementando um protocolo diferente do gRPC padrão, que facilita o trabalho do proxy na tradução entre o protocolo cliente e o protocolo servidor.

O proxy default dessa implementação é o Envoy. Mas há planos para que os próprios frameworks web das diversas linguagens implementem in-process proxy, eliminando a necessidade de um proxy terceiro.

Entra o ASP.NET Core

O que o time do ASP.NET Core fez foi exatamente isso: escreveram um middleware que faz o papel de proxy, eliminando a necessidade de se ter um proxy terceiro na jogada. Thanks, Sir Newtonsoft.

Seu uso ficou muito, muito prático. Uma vez que você tenha adicionado ao projeto referência ao pacote Grpc.AspNetCore.Web, basta duas linhas no Startup.cs:

Fonte: Configure gRPC-Web in ASP.NET Core (link | source)

E no cliente JavaScript, nada muito diferente do que já estamos acostumados a fazer em Node.js.

Fonte: gRPC for .NET / Examples / Browser (source)

Okay. Neste momento, se você lida com aplicações web requisitando serviços em servidores diferentes, provavelmente já pensou em quatro letras: CORS. O pessoal do ASP.NET Core também.

Fonte: gRPC-Web and CORS (link)

Será que vale a pena?

Difícil dizer se vale a pena ou não para seu projeto, para sua equipe, porque cada caso é um caso. Como quase tudo na vida, a resposta mais provável é DEPENDE.

Não é porque é novo e fácil, que é bom para tudo. Vamos pensar, então, em cenários de uso.

Um dos potenciais cenários de uso é quando você precisa lidar com grandes volumes de dados, trafegando pra lá e pra cá. Há algumas maneiras de se lidar com esse tipo de problema. Uma delas é usar um protocolo binário, pois o tamanho das mensagens é muito, muito menor, do que o de um protocolo texto, como JSON, e a serialização/deserialização é absurdamente mais rápida.

Size
Speed
Fonte: How to boost application performance by choosing the right serialization (link)

Pensando neste caso de uso e possível solução, a equipe do ASP.NET Core em parceria com a equipe do Blazor, escreveram uma aplicação web (Blazor WebAssembly) para demonstrar o uso do gRPC-Web vs o bom e velho JSON.

Abaixo, um printscreen que acabei de tirar, comparando uma requisição obtendo 10 mil registros em formato JSON vs 10 mil registros com gRPC-Web.

Fonte: Weather forecast – fetch data demo (link)

Conclusão

gRPC-Web traz para o ambiente de aplicações web muitas vantagens já consolidadas pelo gRPC tradicional, como por exemplo, o uso de mensagens binárias bem menores que o bom o velho JSON, serialização/deserialização consequentemente mais rápida, APIs de streaming e a adoção de contratos de API formalizados por IDL.

Para quem quiser saber mais sobre o assunto, recomendo What is gRPC? Protocol Buffers, Streaming, and Architecture Explained, Introduction to gRPC on .NET e gRPC services with ASP.NET Core para começar.

Cheers!

.NET 2015: Ótima notícia para usuários de Mac e Linux

Sou muito fã de .NET – curto muito C#, F# e ASP.NET MVC –, mas ter que bootar um Windão no meu Mac para poder mexer com isso nunca foi, absolutamente, algo que me agradou. Já tentei usar Mono algumas vezes, mas hmmmm acabou não rolando muito pra mim, então a solução do Windão no Parallels acabou sendo a menos inconveniente pra mim até hoje.

Mas agora há pouco li uma notícia que promete mudar isso de uma vez por todas:

Announcing .NET 2015 – .NET as Open Source, .NET on Mac and Linux, and Visual Studio Community

Pelo que diz o post, os caras estão construindo uma .NET Core CLR para Windows, Mac e Linux, totalmente open source e suportada pela Microsoft. E além disso, também, um web server chamado Kestrel, baseado em libuv, para rodar ASP.NET 5 no Mac e no Linux.

No que diz respeito a ferramentas de desenvolvimento, a galera que desenvolve o ASP.NET e web tools está trabalhando num projeto chamado OmniSharp, que prove “real intellisense & stuff” para SublimeText, Vim, Emacs, etc. Ou seja, vai melhorar bastante a experiência de desenvolvimento .NET em Mac e Linux.

So sweet.

Página nova e contratações

Virei a página outra vez….

Ontem assumi como diretor de tecnologia da Urban Summer, uma agência digital bem bacana, que tem feito trabalhos incríveis para uma porção de clientes legais. E como de costume, cá estou eu procurando por gente boa para trabalhar no meu time.

Tenho algumas vagas em aberto, que ainda vou postar aqui com mais detalhes, mas de cara, urgente, urgentíssimo, estou procurando por dois desenvolvedores client-side, como uns dois anos de experiência mais ou menos, com bastante habilidade em:

  • Recortar imagem – você pega um PSD e faz a mágica para transformar em página web
  • HTML
  • CSS
  • JavaScript

Se souber programar “razoavelmente bem” em Python, PHP, C#.NET, Objective-C, Java ou Ruby, que beleza, isso conta ponto a seu favor. Mas se não souber, tudo bem, não tem problema; esse não é um requisito obrigatório.

Se você não tiver interesse, mas souber de um camarada que tem, indique! Eu ficarei grato. Ele, certamente, também ficará – talvez ele até lhe pague um café.

Aguardo contato em leandro@urbansummer.com.br.

Valeu!

Composição e Integração de Sistemas em 2013

Hoje estive falando no TDC 2013, na trilha Arquitetura .NET, sobre a estratégia de compor e integrar sistemas a partir de micro-serviços, que é um tema que tenho bastante interesse e pretendo ainda falar um pouco mais a respeito, em eventos e aqui no meu blog.

Esta foi a segunda vez que falei nesse evento e, assim como no ano passado, a sala estava lotada, o pessoal bem interessado e participativo; e no final, uma galerinha veio trocar uma idéia comigo, fazer perguntas, etc. Foi bem bacana mesmo.

Parabéns aos organizadores e ao pessoal que participou!

A Locaweb está contratando!

A Locaweb está procurando por desenvolvedores experiêntes e talentosos para integrar nossos times de SaaS, Cloud Computing, Hospedagem, Q&A e Sistemas Centrais.

Se você tem experiência em Ruby, Python, Java, .NET, PHP, Perl, bancos de dados relacionais e não-relacionais, arquiteturas distribuídas, sistemas de agendamento de tarefas, filas assíncronas, desenvolvimento web, e umas coisinhas mais, pode ser que tenhamos um lugar aqui para você. Que tal?

O perfil completo pode ser visto na integra aqui, no OndeTrabalhar.com.

O quanto realmente importa a escolha de uma linguagem?

Há alguns meses perguntaram ao Rich Hickey:

“How much does a choice of language really matter? Are there good reasons to choose one language over another or does it all come down to taste?”

E sua resposta foi:

“I think it matters quite a bit. A good language is opinionated, and strives to make a particular style of programming easy and idiomatic. It only seems a matter of taste when you are comparing languages that are more similar than they are different, like Java/C# or Python/Ruby. Try something really different like Clojure, Haskell, or Erlang and the fact that it matters becomes readily apparent.”

Eu acho que concordo bastante com sua opinião. Discutir se Java é melhor do que C#, por exemplo, é inútil, porque as duas linguagens são muito semelhantes. Nesse caso, o que acaba pesando mais na hora da escolha é o ecosistema no qual cada linguagem está inserida, que pode agradar mais a um ou a outro programador. É puro gosto.

O mesmo vale para Ruby e Python, como ele mesmo cita.

Mundos diferentes

Mas e se a comparação for entre Java e Ruby, por exemplo, como é que fica? Na minha humilde opinião, fica no sense. Porque Java e Ruby não são liguagens de mesma proposta; e mesmo sendo ambas de propósito geral, ambas tem objetivos claramente diferentes.

Comparação entre mundos diferentes

Agora vou, propositalmente, contradizer um pouco o que eu disse à cima: faz sentido, sim, você comparar Ruby com Java; Erlang com Python; F# com PHP. Sim, faz sentido.

Faz sentido quando você está escolhendo a linguagem que oferece a melhor solução para um dado domínio de problema.

Super CRUD com Erlang? Não, acho que não.

Precisar de concorrência massiva, tolerância a falhas, processos distribuídos, downtime mínimo? Humm, não sei não, mas será que não é de Erlang que você precisa?

Entende? É nessa hora que a escolha de uma linguagem começa a pesar de verdade. Isso realmente importa.

Mundos diferentes se complementam

Tempos atrás, escrevendo programas Erlang/OTP, senti falta de uma ferramenta que me ajudasse a criar rapidamente a estrutura inicial do projeto e umas coisas mais. O que fiz? Criei uma ferramenta que faz isso: otp_kickoff. Em Erlang? Não, em Ruby. Fiz isso em poucas horas, usando Thor.

Pouco tempo depois, senti falta de uma ferramenta de build amigável. Novamente, o que fiz? Criei o ebuilder, usando Ruby/Thor.

Um outro exemplo de mundos diferentes que se complementam é o JSparrow, um cliente de JMS bem fluente, que fiz usando JRuby.

Essa é a ideia de tirar o melhor de cada linguagem!

Mesmo porque, dificilmente, você constroi um sistema de verdade — que não seja um super CRUD — com apenas uma única linguagem de programação. Na minha equipe mesmo, há sistemas desenvolvidos em C# .NET, que são buildados, testados e deployados com Ruby/Rake/Cucumber e usam Java/Ivy como repositório de assemblies. Isso sem falar em JavaScript, que também tem de monte.

Esse é o mundo real dos sistemas de verdade.

Moral da história

Isso me faz pensar que brigas de fanboys de linguagens — em frenéticas buscas por prosélitos — são uma verdadeira piada.

Quer saber onde será seu próximo trabalho?

Então não deixe de visitar…

O site é recém-nascido, mas já tem uns recursos bem legais, como sistema de pesquisa por palavras-chave, núvem de tags e entre outros. Vale a pena conferir e acompanhar sua evolução — e oportunidades de bons trabalhos.

Mais uma iniciativa bem interessante da Caelum. 🙂