Este artículo proviene de un motor de traducción automática.

El programador políglota

Simultaneidad en canales, dominios y mensajes

Ted Neward

En el último artículo analizamos el lenguaje de programación cobra, un lenguaje orientado a imprecisa derivado de Python que abarca ambos estáticos y dinámicos de escribir, algunos "automatización"conceptos similares a los que se encuentran en Python y Ruby y preparado en pruebas de unidades características, entre otras cosas. Tras la investigación más profunda, vimos que tenía el valor y estábamos felices aprendido un nuevo lenguaje de programación general.

A veces como interesante y eficaz como idiomas de propósito generales, sin embargo, lo que se necesita no es un martillo, Sierra o destornillador, pero un controlador de tuerca de 3/16-pulgadasen otras palabras, tanto como como desarrolladores Valoramos las herramientas que ofrecen capacidad amplio y facilidad, a veces la herramienta adecuada para el trabajo es muy específico. En este fragmento, nos centraremos no en un idioma que vuelve a crear todas las construcciones Turing completo que utilizamos para pero un lenguaje que se centra en un área determinado, búsquedas hacerlo así, en este caso, simultaneidad.

Los lectores de cuidado y exhaustivos de esta revista se tenga en cuenta que este meme concurrencia no es una desconocidas a estas páginas. Los últimos años han visto numerosos artículos en la concurrencia provienen de aquí y MSDN Magazine es apenas único en ese sentido--blogosphere, Twitterverse y portales Web de desarrollador son todo abundan con las discusiones de simultaneidad, incluidos unos naysayers que piensan que este lo concurrencia todo es otro intento de crear un problema de aire. Algunos expertos del sector han incluso pasado hasta que decir (según a rumor en una discusión de panel en una conferencia OOPSLA hace unos años) que "concurrencia es el dragón nos debe slay en los próximos diez años".

Algunos desarrolladores se esté preguntando ¿qué es realmente el problema: después de todo, .NET tiene la capacidad de subprocesamiento múltiple durante años, mediante la expresión de invocación de delegado asincrónico (BeginInvoke y EndInvoke) y proporciona mecanismos de seguridad de subprocesos mediante la construcción de monitor (la palabra clave lock de C#, por ejemplo) y otro concurrencia explícita construcciones de objeto (Mutex, sucesos y así sucesivamente, de System.Threading). Por lo tanto, todos los elementos innecesarios de esta nueva parecen realizar una montaña fuera de un molehill. Los programadores sería absolutamente derecho, suponiendo que escriben código que es (1) 100 subprocesos seguros y (2) aprovechar cualquier oportunidad para paralelismo de tareas o los datos en su código girando subprocesos o prestar subprocesos de grupos de subprocesos para maximizar el uso de todos los núcleos proporcionada por el hardware subyacente. (Si está seguro en ambos recuentos, por favor, continuar con el siguiente artículo en la revista. Mejor aún, escribir una y cuéntenos sus secretos.)

Numerosos propuestas para resolver o mitigar al menos, este problema se ha flotando mediante el universo de Microsoft, incluidos la TPL (Task Parallel Library, biblioteca de tareas de Parallel FX), extensiones de Parallel FX (PFX), flujos de F # asincrónicos trabajo, simultaneidad y Control Runtime (CCR) y así sucesivamente. En cada uno de estos casos, la solución es ampliar un lenguaje de host (normalmente C#) de maneras para proporcionar el paralelismo adicional o para proporcionar una biblioteca que se puede llamar desde un lenguaje de propósito general de .NET. El inconveniente de la mayoría de estos enfoques, sin embargo, es porque dependen de la semántica del lenguaje host, actuando correctamente, desde una perspectiva de concurrencia, una elección optativo, algo tienen los desarrolladores para comprar en explícitamente y código correctamente. Esto Desgraciadamente significa que puede ser la oportunidad de actuando incorrecto, cualquier, por lo tanto crea un agujero de concurrencia que código puede finalmente se dividen en y crear un error de espera de detectarse y se trata de algo malo. Herramientas de desarrollador no deberían, permiten en el ideal a los desarrolladores lo incorrecto--ésta es la razón por la plataforma .NET movido a un enfoque de recolección, por ejemplo. En su lugar, herramientas de desarrollo deben hacer a los desarrolladores, como Rico Mariani (arquitecto de Microsoft Visual Studio y arquitecto de rendimiento de CLR anterior) pone, "explícito en el foso del éxito"--Qué buscar a los desarrolladores el más fácil de hacer debe ser lo correcto y qué los programadores encontrar difíciles realizar o buscar imposible realizar debe ser lo incorrecto.

Con ese fin, Microsoft ha publicado como un proyecto de incubación un nuevo lenguaje Encuadrando dirigido al dominio de simultaneidad, denominado "Axum." En el espíritu de habilitar a los desarrolladores "explícito en el foso de éxito"Axum no es un lenguaje de propósito general como C# o Visual Basic, pero uno dirigido Encuadrando el problema de simultaneidad, diseñado desde el principio para ser parte de un conjunto de idiomas que colectivamente cooperan para solucionar un problema empresarial. En esencia, Axum es un buen ejemplo de un idioma específico del dominio, un lenguaje diseñado específicamente para resolver sólo un aspecto vertical de un problema.

De esta escritura, Axum está etiquetada como versión 0,2 y los detalles de idioma y la operación del Axum están completamente sujetos a cambios, como se indica por la renuncia al frente en el artículo. Pero entre "Mientras escribo esto"y "como leer"los conceptos básicos deben permanecer bastante estables. De hecho, estos conceptos no son exclusivos de Axum y se encuentran en un número de otros lenguajes y bibliotecas (incluido varios de los proyectos mencionados anteriormente, como F # y la CCR), lo aunque Axum propio no convertirlo en uso generalizado, las ideas aquí son un valor incalculable para pensar de concurrencia. Lo que es más, la idea general--un idioma específico del dominio problema--está creciendo rápidamente en una "cosa grande"y se merece la pena examinar en su propio derecha, aunque en una parte posterior.

Principio

Los bits Axum para su descarga actualmente activo en el sitio Web de prácticas de Microsoft en msdn.microsoft.com/en-us/devlabs/dd795202.aspx; asegúrese de recoger mínimo tanto el archivo .msi (ya sea para Visual Studio 2008 o de Beta 1 de Visual Studio 2010) como el manual del programador. Una vez instalarlos, iniciar Visual Studio y cree un nuevo proyecto de aplicación de consola Axum, como se muestra en la figura 1.


Figura 1 Proyecto de aplicación de consola de Axum

Reemplácelo por el código que se muestra en la figura 2. Éste es el Axum "Hola a todos",realiza en el que sirve para el mismo propósito que otra aplicación Hola a todos: Para comprobar que la instalación ha funcionado y para hacerse una idea básica de la sintaxis. Suponiendo que todo lo compila y ejecuta, estamos conveniente ir.

Figura 2 Axum "Hello World"

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Hello
{
public domain Program
{
agent MainAgent : channel Microsoft.Axum.Application
{
public MainAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
// TODO: Add work of the agent here.
Console.WriteLine("Hello, Axum!");
PrimaryChannel::ExitCode <-- 0;
}
}
}
}

Como puede ver la sintaxis de Axum es muy que recuerda a de C#, colocar Axum imprecisa en la categoría de idiomas que se conoce como la familia de lenguajes C: utiliza las llaves para indicar el ámbito, puntos y comas para finalizar las instrucciones y así sucesivamente. Sin embargo, con la dice que hay obviamente unas nuevas palabras clave a conocer (dominio, agente, canal, recibir, sólo para los principiantes) más unos nuevos operadores. Formalmente, Axum no es realmente un derivado de C#, pero un subconjunto selectivo con agregado nuevas características. Axum incluye todos los tipos de instrucción de C# 3.0 (tales como lambdas y inferidas variables locales) y expresiones (como cálculos algebraicas y yield return y yield break), métodos, las declaraciones de campo, delegados y tipos de enumeración, pero deja clases, interfaces o definiciones de tipo struct y valor, las declaraciones de operadores, propiedades, campos const o variables locales y métodos de los campos estáticos.

¿Oír que whooshing sonido? Que es el viento corriente más allá de sus orejas, y si piensa que una ligera fluttering en su estómago, está bien. Saltar fuera cliffs es bueno.

Conceptos y sintaxis

La sintaxis Axum directamente refleja los conceptos básicos de Axum, justo como lo hace para C#. Donde C# tiene clases, sin embargo, Axum tiene agentes. En Axum, un agente es una entidad de código, pero a diferencia de un objeto tradicional, agentes nunca interactúan directamente; en su lugar, pasan mensajes entre sí de forma asincrónica a través de canales, o para ser más precisos, entre los puertos definidos en el canal. Enviar un mensaje en un puerto es una operación asincrónica--devuelve inmediatamente el envío--y recibir un mensaje en un puerto se bloquea hasta que haya un mensaje está disponible. De esta forma, subprocesos nunca interactúan en los mismos elementos de datos al mismo tiempo, y un origen principal de interbloqueo y incoherencia eficazmente se eleva inmediatamente.

Para ver esto en acción, considere el código de saludo. El agente MainAgent implementa un canal especial, denominado Application, que es un canal que el motor en tiempo de ejecución de Axum comprende directamente--se procesar los argumentos de línea de comandos entrantes y pasarlos a través de PrimaryChannel canal la aplicación, en el puerto CommandLines. Cuando se construye el motor en tiempo de ejecución, el MainAgent utiliza la operación de recepción (una primitiva Axum) para bloquear ese puerto hasta que los argumentos están disponibles, momento en que la ejecución en el constructor MainAgent continúa. El tiempo de ejecución, necesidad de realizar su trabajo, bloques en el puerto ExitCode de PrimaryChannel, que es la señal que la aplicación ha terminado. Mientras tanto, los rollos de MainAgent a través de la matriz, imprimir cada uno y una vez terminado, envía el literal 0 de entero al puerto ExitCode. Esto es recibido por el tiempo de ejecución, que a continuación, finaliza el proceso.

Observe cómo Axum, desde el principio, realiza esta idea de ejecución simultánea seriamente--no sólo son los desarrolladores reservados desde la creación y manipulación de subprocesos o objetos de bloqueo y simultaneidad, pero el lenguaje Axum supone un enfoque simultáneo incluso para algo tan simple como Hello. Para algo tan simple como Hello, posiblemente, se trata de una pérdida de tiempo; sin embargo, la mayoría de nosotros no escribe aplicaciones tan sencilla como saludo muy a menudo y aquellos que lo hace, puede siempre retroceder al aplicación tradicional de único de un subproceso de forma predeterminada orientada lenguaje (C# o Visual Basic o cualquier otra cosa que produce la fantasía).

Figura 3 Aplicación de proceso

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Process
{
public domain Program
{
agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
for (var i = 0; i < args.Length; i++)
ProcessArgument(args[i]);
PrimaryChannel::ExitCode <-- 0;
}
function void ProcessArgument(String s)
{
Console.WriteLine("Got argument {0}", s);
}
}
}
}

Es decir Hello para Wimps

Un ejemplo ligeramente más interesante es la aplicación de proceso, como muestra en la figura 3. En esta versión, vemos un nuevo concepto Axum, la función. Las funciones son diferentes de los métodos tradicionales en que el compilador Axum garantiza (por medio de error del compilador) que el método nunca modifica el estado interno del agente;en otras palabras, las funciones no tiene efectos secundarios. Evitar efectos agrega a la utilidad del Axum como un lenguaje compatible con la concurrencia--sin efectos secundarios, resulta extremadamente difícil accidentalmente escribir código en un subproceso modifica el estado compartido (y, por lo tanto, presenta la incoherencia o resultados incorrectos).

Pero todavía no concluimos aquí. No sólo Axum desea facilitar escribir código seguro para subprocesos, también desea facilitar escribir código compatible con el subproceso. Con equipos de dos y cuatro núcleos cada vez más estándar y equipos de 8 y 16 núcleos visibles en el horizonte, es importante Busque oportunidades para realizar operaciones en paralelo. Axum facilita esta, vuelva a levantar la conversación lejos de subprocesos y proporcionando algunas construcciones de nivel superiores para trabajar con. La figura 4 muestra una versión modificada de la ProcessAgent.

Aquí suceden dos cosas nuevas: uno, hemos creado un punto de interacción, que puede servir como un origen o un destinatario de mensajes (en este caso, será ambos);y dos, hemos utilizado el operador de reenvío para crear una canalización de mensajes desde el punto de interacción a la función ProcessArgument. De este modo, cualquier mensaje enviado a "los argumentos"punto de interacción se reenviarán ProcessArgument, bueno, procesamiento. A partir de ahí, todo lo que queda es recorrer en iteración los argumentos de línea de comandos y enviar cada uno como un mensaje (mediante el operador de envío, el <--operación dentro del bucle) a la interacción de argumentos comienza punto y el trabajo.

(Axum lo define formalmente como una red de flujo de datos y hay mucho que puede hacer además para crear una canalización de 1 a 1 simple, pero eso más allá del alcance de esta introducción. La Guía del programador tiene más detalles para quienes estén interesados.)

Figura 4 Versión modificada de ProcessAgent

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Process
{
public domain Program
{
agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
// Second variation
//
var arguments = new OrderedInteractionPoint<String>();
arguments ==> ProcessArgument;
for (var i = 0; i < args.Length; i++)
arguments <-- args[i];
PrimaryChannel::ExitCode <-- 0;
}
function void ProcessArgument(String s)
{
Console.WriteLine(“Got argument {0}”, s);
}
}
}
}

Por ahora poner los datos a través de la canalización, Axum puede decidir cómo y cuándo en número de subprocesos para realizar el procesamiento. En este sencillo ejemplo, no puede ser útil o eficaz para ello, pero para escenarios más sofisticados, lo hará con bastante frecuencia. Puesto que el punto de interacción de argumentos es un punto de interacción ordenado, los mensajes enviados hacia abajo de la canalización--y, más importante, los resultados devueltos--conservará su lugar en el orden, incluso si cada mensaje se procesa en un subproceso independiente.

Lo que es más, este concepto de canalización de mensajes desde un punto de interacción a otro es un concepto eficaz que Axum toma prestado de lenguajes funcionales como F #. La figura 5 muestra lo fácil es agregar algún procesamiento adicional en la canalización.

Observe que, de nuevo, la función UpperCaseIt no está modificando el estado interno de ProcessAgent pero funciona sólo en el objeto pasa. Además, la canalización se modifica para incluir UpperCaseIt antes de la función ProcessArgument y ocurre lo intuitiva: los resultados de UpperCaseIt se pasan a la función ProcessArgument.

Como un ejercicio mental, considere cuántas líneas de código de C# se necesitaría para ello, teniendo en cuenta que todo esto se también se realiza a través de varios subprocesos, no sólo un único subproceso de ejecución. Ahora imagine depurar el código para asegurarse de que se ejecuta correctamente. (En realidad, imaginándose no es necesario--utilizar una herramienta como ILDasm o Reflector para examinar el IL generado. Es definitivamente no trivial.)

Por cierto, tengo una confesión pequeño que--tal y como está escrito, mi ejemplo no se ejecuta correctamente. Cuando se ejecuta el código anterior, la aplicación devuelve sin mostrar nada. Esto no es un error en los bits Axum;Esto es como el comportamiento esperado y resalta cómo programación en una mentalidad simultánea requiere un cambio mental.

Cuando el punto de interacción de argumentos está recibiendo los argumentos de línea de comandos mediante el operador de envío (<--), los envía se realizan asincrónicamente. El ProcessAgent no es bloqueo cuando envía los mensajes y, por lo tanto, si la canalización es lo suficientemente compleja, que envían los argumentos son todos, el bucle finaliza y se envía el ExitCode, finalizando la aplicación, todo antes de nada puede lleguen a la consola.

Para solucionar este problema, debe bloquear hasta que la canalización ha finalizado la operación; ProcessAgentdebe contener el subproceso activo con algo como un Console.ReadLine (). (Esto resulta ser complicado en la práctica, consulte el blog del equipo Axum para los detalles.) O es necesario cambiar el funcionamiento ProcessAgent y es este último curso que voy a tomar, principalmente para demostrar unas cuantas más de las características del Axum.

En lugar de trabajo dentro de sí mismo, ProcessAgent se aplazar el trabajo para un nuevo agente. Pero ahora, sólo para hacer cosas un poco más interesante, este agente nuevo se también desea saber si el argumento debe ser en mayúsculas o minúsculas. Para ello, el nuevo agente tendrá que definir un mensaje más complejo, que toma no sólo la cadena para funcionar en, pero también un valor booleano (true para mayúsculas). Para ello, Axum requiere que se defina un esquema.

Figura 5 Mensajes de canalización

using System;
using Microsoft.Axum;
using System.Concurrency.Messaging;
namespace Process
{
public domain Program
{
agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
// Third variation
//
var arguments = new OrderedInteractionPoint<String>();
arguments ==> UpperCaseIt ==> ProcessArgument;
for (var i = 0; i < args.Length; i++)
arguments <-- args[i];
PrimaryChannel::ExitCode <-- 0;
}
function String UpperCaseIt(String it)
{
return it.ToUpper();
}
function void ProcessArgument(String s)
{
Console.WriteLine("Got argument {0}", s);
}
}
}
}

Procesar mi argumento

Implícitamente hasta este punto, uno de los objetivos de Axum ha aislar diferentes entidades ejecuta (agentes) fuera de uno a otro y ha hecho copias de los mensajes y entregarlos que el agente, en lugar pasar objetos directamente. Ningún estado compartido--incluso a través de parámetros--no significa accidentales posibilidades de conflicto de subproceso.

Mientras que funciona bien para tipos definidos en la biblioteca de clases de base de .NET, puede fácilmente ser rechaza si los programadores de .NET no lo derecho en tipos definidos por el usuario. Para combatir este, Axum requiere que se definido un nuevo tipo de mensaje como esquema tipo--esencialmente un objeto sin los métodos y campos requeridos y opcionales, mediante la palabra clave de esquema:

schema Argument
{
required String arg;
optional bool upper;
}

Esto define un nuevo tipo de datos, argumentos, que consta de un campo obligatorio, arg, que contiene la cadena que se realizarán mayúsculas o minúsculas, y un campo opcional, superior, que indica si debe realizarse superior o inferior. Ahora, se puede definir un nuevo canal con un puerto de solicitud-respuesta que toma argumentos mensajes y proporciona mensajes de cadena nuevo:

channel Operator
{
input Argument Arg : String; // Argument in, String out
}

Tener definido este canal, resulta relativamente trivial para escribir al agente que implementa el canal, como se muestra en la figura 6. Observe que este agente técnicamente nunca termina su constructor--gira simplemente en un bucle, llamada recibir en el puerto de argumentos, bloqueo hasta que un argumento de instancia se envía a ella, y a continuación, enviar el resultado de la ToUpper o ToLower encenderlo el mismo puerto (según el valor de superior, por supuesto).

Figura 6 Canal de implementación de agente

agent OperatingAgent : channel Operator
{
public OperatingAgent()
{
while (true)
{
var result = receive(PrimaryChannel::Arg);
if (result.RequestValue.upper)
result <-- result.RequestValue.arg.ToUpper();
else
result <-- result.RequestValue.arg.ToLower();
}
}
}

Desde los llamadoresperspectiva (usuarios), utilizando el OperatingAgent es no diferente la ProcessAgent propio: creamos una instancia de utilizando el método integrado CreateInNewDomain y iniciar registrar instancias de argumento en el puerto de argumentos. Cuando lo hagamos, sin embargo, se devuelve un objeto que ofrecerá finalmente la respuesta desde el agente, que es la única manera de recopilar los resultados (mediante la operación de receive() otro), como se muestra en la figura 7.

Cuando ejecuta, lo esperado--basadas en el milisegundo actual en el momento del argumento enviado, la cadena de la línea de comandos será mayúsculas o minúsculas. Y aún, todo sin ninguna interacción directa del subproceso en parte del desarrollador.

Tan pequeña, pero hasta

Axum claramente representa otra forma de pensar acerca de programación y a pesar de sus diferencias radicalmente de C#, implementa un número significativo de las características de lenguajes de programación más generales. Su finalidad principal, sin embargo, centra alrededor de concurrencia y código descriptivas de subproceso y como tal, funciona mejor con problemas que requieren (o beneficiarán) simultaneidad.

Afortunadamente, el equipo de generación Axum no intenta reinventar el lenguaje C#. Puesto que Axum se compila en ensamblados .NET como haría en otros lenguajes de. NET, es relativamente trivial generar las partes pesado simultáneas de la aplicación en Axum (como un proyecto de biblioteca de clases, en lugar de una aplicación de consola) y, a continuación, llamar a en él desde C# o Visual Basic. La distribución Axum se distribuye con un ejemplo que hace esto (cenas filósofos ejemplo, que ilustra el problema de simultaneidad clásico en una aplicación de WinForms), y calidad tiempo con Reflector a través de las bibliotecas compiladas Axum revelará mucho acerca de ese espacio de interoperabilidad. Utilizar Axum para generar la biblioteca bibliotecas puede llamar desde otro lenguaje (C# o Visual Basic), puede ir un largo camino deque gran simultaneidad utilizar mucho más accesibles para las otras tecnologías de .NET, como Web o aplicaciones de escritorio.

Figura 7 Utilización del método CreateInNewDomain

 

agent ProcessAgent : channel Microsoft.Axum.Application
{
public ProcessAgent()
{
String [] args = receive(PrimaryChannel::CommandLine);
var opAgent = OperatingAgent.CreateInNewDomain();
var correlators = new IInteractionSource<String>[args.Length];
for (int i=0; i<args.Length; i++)
correlators[i] = opAgent::Arg <-- new Argument {
arg = args[i],
upper = ((System.DateTime.Now.Millisecond % 2) == 0) };
for (int i=0; i<correlators.Length; i++)
Console.WriteLine("Got {0} back for {1}",
receive(correlators[i]), args[i]);
PrimaryChannel::ExitCode <-- 0;
}
}

De hecho, Axum utiliza un experimental compilador C# que proporciona algunas características interesantes y diferentes, un superconjunto de C# 3.0 (métodos 3.0 + asincrónicos), ninguno de los cuales está implícita para C# 4.0, a la forma. Sin embargo, lo que esto lo permite, es la capacidad de código de C# y Axum de mezcla y coincidencia en el mismo proyecto, algo exclusivo de Axum hasta ahora. Ver la que guía del programador de Axum para details.Axum tiene características adicionales que no se describe aquí, tratan algunos detalladamente en el Axum manual del programador, incluyendo una relación estrecha con WCF para facilitar la escribir servicios de gran escala, pero esta introducción debería ser suficiente para comenzar la creación de aplicaciones, bibliotecas o servicios en Axum. Recuerde que éste es un proyecto de investigación y los usuarios pueden ofrecer comentarios a través de los foros de Axum en el sitio Web de DevLabs.

¿Qué ocurre si falla Axum para convertir en un producto de envío o lectores desee utilizarla antes? Para empezar, del Axum éxito o fracaso depende significativamente en parte reacción del usuario y por lo que se live o desaparecer por comentarios de calidad--enviar el equipo Axum sus pensamientos y agitate para su versión de forma pública! Pero incluso si Axum no se puede ascender, recuerde, los conceptos de Axum no son únicos. En el phraseology académica, Axum incorpora el modelo de actores de concurrencia y otros modelos de actores están disponibles para el desarrollador de .NET que desea algo para producción hoy en día. El proyecto de código abierto "Retlang"incluye varios de estos conceptos, como el idioma de F # (vistazo tipo de MailboxProcessor del F #) o la concurrencia de Microsoft y Runtime Control (CCR), con lo que ambos son estado cerca de envío, si no hay ya.

Al final, recuerde que el objetivo es no para crear proyectos que utilizan todos los lenguajes basados en CLR en existencia, pero para buscar los idiomas que pueden solucionar problemas concretos y utilizarlas cuando las demandas de la situación.

Y recuerde, Quot linguas se llama, establece homines vales.

Ted Neward es el principal con Neward and Associates, a través del cual habla, escribe y coaches a crear sistemas empresariales confiables. Ha escrito numerosos libros, impartidos y habla en conferencias todo el mundo, es un INETA y recibió el premio MVP en tres áreas diferentes de experiencia. Puede ponerse en contacto con él en ted@tedneward.com o a través de su blog en blogs.tedneward.com.