Este artículo proviene de un motor de traducción automática.
Cobertura de sincronización
Cobertura de código para simultaneidad
Chris Dern y Roy Patrick Tan
Descargar el código de ejemplo
Estamos en crossroads otro en el sector, como procesadores y más cargado de transistor petición código multiproceso tener en cuenta todo su potencial. Mientras los equipos de escritorios netbooks no deporte menos procesadores duales en realidad, tallos de ganas transistores permanecer inactivo--crying para las aplicaciones multiproceso devour. Para solucionar la oncoming ola de aplicaciones simultáneas, compañías como Intel y Microsoft son carreras al mercado con generadores de perfiles, marcos, los depuradores y bibliotecas. Como las aplicaciones multiproceso proliferando, lo demasiado discusiones de interbloqueos, live bloqueos y carreras de datos será cada vez más común a través de la industria del software. Los profesionales de desarrollo de software que adoptan nuevas herramientas, técnicas y métricas que pueden tratar con software multiproceso.
Cobertura de código y concurrencia
Métrica de cobertura de código tradicional, como instrucción, bloque y bifurcación, no direcciones prueba adecuación preocupaciones introducidas por la simultaneidad. Echemos instrucción cobertura como ejemplo. Aunque una métrica imperfecta, cobertura de la instrucción es popular y es útil en un grado en averiguar cómo exhaustivamente haya probado el código. Como un actualizador, cobertura de instrucción mide cuántas instrucciones del código de la aplicación se ejecutaron. Es una métrica simple que es utilizada por muchas herramientas para pruebas y muchos equipos de desarrollo de software incluyen cobertura de instrucción alto como una barra de calidad del código. Por desgracia, cobertura de instrucción no le otorga cualquier visibilidad a cómo la simultaneidad en el código se está probando.
Como ejemplo, la figura 1 muestra una implementación sencilla de un tipo de cola de subprocesos escrito en C# (en este artículo, utilizamos C# y .NET para nuestros ejemplos, pero la idea de cobertura de sincronización es aplicable a código nativo así).
Nuestro ejemplo tiene sólo dos métodos: Enqueue Dequeue y ajusta una cola de .NET < T >por que rodean cada enqueue y dequeue operación en un bloqueo. ¿Cómo podría probamos esta implementación de la cola? En el código siguiente se muestra una prueba unitaria simple para nuestro cola de subprocesos:
void SimpleQueueTest() {
ThreadSafeQueue<int> q = new ThreadSafeQueue<int>();
q.Enqueue(10);
Assert.AreEqual(10, q.Dequeue());
}
Observe que esta prueba simple nos da cobertura de la instrucción de 100 por ciento, como ésta una prueba ejercita cada instrucción en nuestra implementación de la cola.
Pero, espere, tenemos un problema, se supone que la cola de subprocesos, pero hasta ahora hemos probado con un único subproceso! Nos no probar el comportamiento fundamental que fue la razón por qué hemos implementado este tipo ThreadSafeQueue en primer lugar. Cobertura de instrucción indica que tenemos cobertura de 100 por cien cuando usamos un subproceso en un tipo diseñado para tener acceso simultáneamente. Claramente, cobertura de la instrucción es inadecuado para decirnos qué parte de los aspectos de concurrencia del código que hemos probado. ¿Qué nos falta?
Figura 1 Implementación de una cola de seguros para subprocesos en C#
public class ThreadSafeQueue<T> {
object m_lock = new object();
Queue<T> m_queue = new Queue<T>();
public void Enqueue(T value) {
lock (m_lock) {
m_queue.Enqueue(value);
}
}
public T Dequeue() {
lock (m_lock) {
return m_queue.Dequeue();
}
}
}
Lo faltan es una rama ocultada dentro de la instrucción de bloqueo. Imagine que nos reemplazar el bloqueo de un personalizado bloqueo primitivo, como la implementación simplificada de un método de bloqueo de bucle como sigue:
void Acquire() {
while (!TryAcquire ()) {
// idle for a bit
Spin();
}
}
El método Enter llama a TryAcquire y si se produce un error adquirir el bloqueo (como TryAcquire devuelve false), se gira un poco y se intenta de nuevo. ¿Si este código de bloqueo personalizado se combina con nuestro método Enqueue, cómo que afectaría instrucción cobertura? En el código siguiente se muestra cómo se puede reescribir la puesta en cola con bloqueo personalizado:
public void Enqueue(T value) {
while (!TryAcquire ()) {
//idle for a bit
Spin();
}
m_queue.Enqueue(value);
Release();
}
Ahora, si se ejecuta nuestra prueba, de repente vemos falta de cobertura de la instrucción. Cualquier prueba de subproceso único perderá la instrucción de número, porque TryAcquire sólo se devuelven false si se ha producido otro subproceso que ya contiene el bloqueo. La única forma que se gire el método es si algún otro subproceso ha entrado la sección crítica. Es decir, se puede cubrir sólo la instrucción de número si hay contención. Esta rama implícita dentro de la instrucción de bloqueo es el origen de nuestro orificio de cobertura.
Cobertura de sincronización
Porque don’t esperamos cualquiera para reemplazar instrucciones lock con sus propias primitivas de exclusión mutua (ni pendientes se promuevan hacerlo), se necesita buscar una manera de exponer esta rama oculta, por lo que se puede medir la cantidad de conflictos producidos durante nuestras pruebas. Investigadores en IBM surgió con un modelo de cobertura de código denominado cobertura de sincronización puede hacerlo sólo.
Puede leer el documento hace referencia en la sección información adicional siguiente, pero la idea básica es sencilla. En primer lugar, tomamos una sincronización primitiva--por ejemplo el tipo de Monitor de .NET (que se utiliza la palabra clave lock en C# y la palabra clave SyncLock en Visual Basic). A continuación, en cada punto en el código donde se un bloqueo adquirido, registro si se cualquiera bloqueado (en el caso de, digamos, Monitor.Enter) o en caso contrario Error al adquirir el bloqueo (por ejemplo con Monitor.TryEnter). Cualquier resultado significa que se ha producido contención. Por lo tanto, quiere para decir que tenemos cobertura a través de un bloqueo, no es suficiente han ejecutado una instrucción lock;También se debe ejecutar la instrucción lock mientras mantiene el bloqueo de algún otro subproceso.
Mientras que esta técnica se aplica a aplicaciones nativas y administradas, el resto de este artículo tratará sobre nuestra solución administrada--una herramienta de cobertura de sincronización de prototipo para .NET denominado cubierta de sincronización.
Vamos a unos instantes a clarificar los conceptos usados por nuestra herramienta de cobertura. Un punto de sincronización es un sitio de llamada específica léxico, que invoca un método de sincronización. Tomar una rápida mirar vuelve nuestra cola de subprocesos de ejemplo (figura 1), con nuestro sombrero de compilador de C#, vemos dos dichos lugares ocultos en las instrucciones de bloqueo de puesta en cola y Dequeue. Después de cualquier ejecución única estamos interesados en dos tipos de cobertura: Cobertura de instrucción, donde el método no experimentaron cualquier contención;y cobertura de bloqueo, donde el método fue obligado para bloquear o espere a que otro subproceso liberarlo.
El resto de este artículo nos centraremos específicamente en System.Threading.Monitor, el caballo de trabajo de la sincronización .NET estable. Sin embargo, es útil Nota Este enfoque funciona igualmente bien con otros tipos primitivos, como System.Threading.Interlocked.CompareExchange.
Redondeo tripping con IL
Nuestro objetivo de esta herramienta de cobertura de sincronización es transparente instrumentar ensamblados de .NET existentes, que se puede interceptar todas las llamadas Monitor.Enter en dicho ensamblado y determinar si sincronizar punto encontró contención. Como la mayoría de las soluciones de cobertura de código en la actualidad, nuestra herramienta aplica técnicas que reescribir IL (el .NET lenguaje intermedio) para producir versiones instrumentadas de los ensamblados de destino. Aunque dedicaremos la mayoría de nuestro tiempo en C# por tratar directamente con el IL, en teoría esta herramienta debe funciona en todos los lenguajes .NET que se compilan en IL.
Mientras hay un pocas tecnologías posibles para habilitar la reescritura de código, incluida la infraestructura de compilador comunes de Microsoft Research (MSR), publicado recientemente para simplificar elegimos retroceder al confianza combinado de compilador y descompilador de IL ILASM y ILDASM. Trabajar directamente con el IL en archivos de texto sin formato demostrado para ser una gran ayuda durante las fases de descubrimiento, diseño y desarrollo del proyecto, habilitar nos explorar las opciones de diseño de un "sueldo-para-play"enfoque con nuestra inversión de tiempo. Se demostró la viabilidad y pragmatismo de nuestra solución al primero a través de un proceso manual completamente, consta de archivos por lotes y el Bloc de notas. Aunque esto puede no ser la mejor elección para una herramienta de calidad de producción, se recomienda este enfoque para cualquier prototipo similar rescribir la aplicación.
Con esta completa de decisión de diseño global, vamos a un paseo rápido de nuestra cadena de la herramienta, como se muestra en la figura 2.
Utilizando ILDASM, nos descompilar el ensamblado de destino en un archivo de texto con las instrucciones IL. Después se, cargar el código fuente a la memoria subyacente una fachada basada en objetos para. A continuación, aplique las modificaciones necesarias y las inserciones de código en el código, emitir el origen modificado al disco. A continuación, es sólo una simple cuestión de volver a compilar el ensamblado cubierto con ILASM y comprobación con la instrucción PeVerify para detectar cualquier tonta transformaciones no válidas se que haya aplicado. Una vez tenemos nuestros ensamblados de cobertura, nos ejecutar nuestro pase de prueba como normal y recopilar el archivo de cobertura de tiempo de ejecución para el análisis posterior.
Nuestro conjunto de herramientas se compone de dos partes principales: Cubierta de sincronización, que automatiza este proceso de instrumentación y proporciona un IL redondo plataforma de ida y vuelta en el que creamos nuestro instrumentación;y Visor de portada, una aplicación de Windows Presentation Foundation (WPF) que proporciona una forma intuitiva y familiar de explorar los archivos de cobertura.
Bloqueo de captura
Por ahora, debe se pregunte cómo podemos observar y registrar la contención de experiencias de un punto de sincronización. Mirando la "oculto-rama" volvermodelo mental de cómo podría implementarse Monitor.Enter nos proporciona una pista, si se aplica un casi literalmente en nuestro transformación. Vemos en la anterior implementación simplificada de un código de bloqueo de bucle que el método adquirir captura esta información exacta en el valor devuelto. Lo debemos hacer es verla.
Como queremos reescribir el IL de otras personas, insertar ramas arbitrarias promete abrir un cuadro de Pandora de problemas en su lugar se podría evitar. Recuperar el segundo problema de grabación y vincular los datos de punto de sincronización sugiere que se vaya a nuestro cuadro de herramientas a los programadores y alcanzar algunas direccionamiento indirecto.
Nos solucionar ambos problemas introduciendo una clase contenedora--una fachada preparadas para cobertura de sincronización a través de Monitor cuyos métodos toman un argumento syncId. Este syncId es un entero generado de único que identifica un punto de sincronización; es decir, damos cada punto de sincronización un identificador único y cuando se alcanza un punto de sincronización, pasamos ese identificador a nuestra clase de contenedor. Nuestra implementación de cobertura comienza por llamar a Monitor.TryEnter, registrar el resultado y, a continuación, si es necesario, delegar a la original Monitor.Enter de bloqueo. El punto de sincronización indica que se administran mediante una clase base de datos en memoria simple llamamos el CoverageTracker. Reunir todas las piezas, nuestra versión de cobertura de Monitor.Enter este aspecto, como se muestra en la figura 3.
Figura 3 Versión de cobertura de Monitor.Enter
namespace System.Threading.SyncCover {
public static class CoverMonitor {
public static void Enter(object obj, int syncId) {
if (Monitor.TryEnter(obj)) {
coverageTracker.MarkPoint(syncId, false); // No Contention.
} else {
Monitor.Enter(obj);
coverageTracker.MarkPoint(syncId, true); // Contention
}
}
}
}
Tenga en cuenta que el código en figura 3 está pensado para admitir la versión 3.5 de .NET de Monitor.Enter. El tipo de Monitor de 4 .NET próximas incluye cambios que realizar flexibles contra las excepciones asincrónicas, consulte blogs.msdn.com/ericlippert/archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx. Agregar compatibilidad para el 4 de .NET sobrecargas es sólo cuestión de sobrecarga de los contenedores en una similar fashion.Once tenemos nuestro método en la mano, el proceso de instrumentación será bastante sencillo. Veamos el IL generado por el compilador de .NET 3.5 C# de la instrucción de bloqueo, como sigue:
IL_0001: ldfld object class ThreadSafeQueue'1<!T>::m_lock
IL_0002: call void [mscorlib]System.Threading.Monitor::Enter(object)
Sólo necesitamos buscar las llamadas a Monitor.Enter y reemplazarlos con llamadas a nuestro CoverMonitor.Enter mientras no olvidarse de inyectar un argumento syncId adicionales antes de la invocación del método. En el código siguiente se ilustra esta transformación:
IL_0001: ldfld object class ThreadSafeQueue'1<!T>::m_lock
I_ADD01: ldc.i4 1 // sync id
IL_0002: call void System.Threading.Coverage.Monitor::Enter(object, int32)
Como parte final de este proceso, debería hablar un poco informes y qué tipo de información es útil. Hasta ahora, sabemos acerca de los puntos de sincronización que se identifican mediante un syncId, que se insertan directamente en el código fuente de destino. Sería mucho más útil que podíamos obtener los archivos de origen y números de línea de cada punto de sincronización. Encontramos que ILDASM con la opción /LINENUM nos proporciona la información que necesitamos extrayendo ubicaciones de archivo de origen y los números de línea de base de datos de programa (PDB).
Cuando se encuentra un nuevo punto de sincronización durante el proceso de instrumentación, se genera la siguiente syncId y capturar esta información contextual en un mapa. Esta asignación, ver en el siguiente código, a continuación, se emite como un archivo al final de la instrumentación:
T|ThreadSafeQueue.exe
SP|
0|D:\ThreadSafeQueue\ThreadSafeQueue.cs|9|ThreadSafeQueue`1<T>|Enqueue
1|D:\ThreadSafeQueue\ThreadSafeQueue.cs|15|ThreadSafeQueue`1<T>|Dequeue
Figura 4 Ejemplo de ejecución de cobertura
~|B3|~
A|ThreadSafeQueue.exe
C|ThreadSafeQueue.exe
D|20090610'091754
T|demo
M|DWEEZIL
O|Microsoft Windows NT 6.1.7100.0
P|4
SP|
0|1|1
1|1|0
~|E|~
Mostrar los datos!
Es el momento de mostrar. Una vez que tenemos nuestros binarios cubiertos, es una simple cuestión de ejecutar el programa tantas veces como desee. Esto produce un archivo de tiempo de ejecución que contiene la métrica de cobertura recopilada durante la ejecución, como se muestra en la figura 4. Toma de inspiración desde otras herramientas de cobertura en el mercado, ofrecemos un visor simple basado en WPF para visualizar los resultados de cobertura.
Pero vamos a paso nuevo para un momento. Sabemos que nuestro caso de prueba de un único subproceso ofrecerá nos cero cobertura de sincronización de porcentaje. ¿Cómo puede se corrija la prueba para que se puede producir contención? La figura 5 muestra una mejora ligeramente la prueba que debería causar contención en el método de puesta en cola. Después de ejecutar la prueba, se pueden ver los resultados, como se muestra en la figura 6.
Aquí vemos tres casos de cobertura posibles para un punto determinado de sincronización. En este ejemplo, vemos que Enqueue experimentado al menos un recuento de contención y por lo que aparece en verde. Dequeue, ejecutada por otro lado, pero no compiten, tal como se muestra en amarillo. También hemos agregado una nueva propiedad Count, que nunca se llamó a, y muestra como rojo.
Figura 5 Una prueba que contención en Enqueue
void ThreadedQueueTest()
{
ThreadSafeQueue<int> q = new ThreadSafeQueue<int>();
Thread t1 = new Thread(
() =>
{
for (int i = 0; i < 10000; i++)
{
q.Enqueue(i);
}
});
Thread t2 = new Thread(
() =>
{
for (int i = 0; i < 10000; i++)
{
q.Enqueue(i);
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Assert.AreEqual(0, q.Dequeue());
}
Mediante la cobertura de sincronización
¿Por lo tanto, al deberíamos utilizar cobertura de sincronización? Como cualquier métrica de cobertura de código, cobertura de sincronización intenta medir cuánto haya probado el código. Cualquier tipo de bloqueo en la aplicación significa que el código fue diseñado para tener acceso simultáneamente y serio debe curiosidad si el conjunto de pruebas ejercita realmente los bloqueos. Su equipo debe objetivo para cobertura de sincronización de 100 por ciento, especialmente si la aplicación espera ejecutar en paralelo un lote.
Intentando practicar lo preach, hemos utilizado esta herramienta de cobertura de sincronización en el transcurso de las pruebas de las extensiones de paralelo para .NET Framework. Ha ayudado a nosotros encontrar errores y agujeros de pruebas durante este ciclo de desarrollo y vamos a continuar usando la métrica adelante. Dos situaciones donde cobertura de la sincronización ha ayudado a nosotros son especialmente interesantes:
Pruebas multiproceso no simultánea
Cobertura de sincronización encontró que algunas pruebas que pensamos que se estaban ejecutando simultáneamente realmente no eran. La figura 7 es una simple prueba multiproceso similar a la figura 5, pero esta vez cada subproceso pone en cola sólo 10 elementos a la cola.
Figura 7 Un test simultáneo que puede no estar simultánea después All
void ThreadedQueueTest() {
ThreadSafeQueue<int> q = new ThreadSafeQueue<int>();
Thread t1 = new Thread(
() => {
for (int i = 0; i < 10; i++) {
q.Enqueue(10);
}
});
Thread t2 = new Thread(
() => {
for (int i = 0; i < 10; i++) {
q.Enqueue(12);
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
Simplemente porque se inicia dos subprocesos, sin embargo, no significa que realmente se están ejecutando en paralelo. Hemos encontrado fue que estos tipos de pruebas con muy breve tiempos de ejecución por subproceso más a menudo que no tendrá un subproceso completamente ejecutar después el otro subproceso. Deseamos una prueba constantemente experiencias contención. Una solución es que cada subproceso enqueue más elementos para que sea más probable que causen contención. Una solución mejor es utilizar una herramienta como CHESS, que obliga a cada intercalación entre los dos subprocesos que se produzca.
Bloqueos innecesarios
Es posible que no se se tratan algunos puntos de sincronización porque no puede cubrir. En el siguiente ejemplo de código, si el bloqueo de b es siempre cuando adquirió el bloqueo en una se mantiene, a continuación, es imposible cubrir el bloqueo en b:
lock(a) {
lock(b) {
//... stuff ...
}
}
Sorprendentemente, cobertura de sincronización realmente ha ayudado a nosotros encontrar bloqueos innecesarios como estos. Resulta que a veces un recurso protegido por un bloqueo que ya no necesitan que. Por ejemplo, un subproceso compite sobre el recurso no se puede ya necesarios y quitado el código, pero no se quitó el bloqueo en el subproceso restante. Aunque el bloqueo adicional es inofensivo en el sentido de comportamiento, todavía podría perjudicar el rendimiento. (Tenga en cuenta, aunque el hecho de nunca se tratar un bloqueo para no significa no es necesario;dicha información sólo proporciona un punto de partida de una investigación para determinar si realmente necesita.)
Limitaciones y el trabajo futuro
Como cualquier métrica de cobertura de código, cobertura de sincronización no es perfecta: tiene sus limitaciones. Limitación de director que recursos compartidos de cobertura de sincronización con otras métricas de cobertura es que no se puede medir lo que no está. Cobertura de sincronización no puede determinar que se desea bloquear a través de algún recurso, sólo que los recursos que han bloqueado se han tratar a través. Por lo tanto, incluso de cobertura de sincronización de 100 por ciento no significa que se realiza el esfuerzo de pruebas, sólo que ha conseguido cierto nivel de minuciosidad las pruebas.
Otro problema es que instrumentar el código de cobertura de sincronización podría modificar la programación de los subprocesos en las pruebas, que se podría cobertura en la ejecución de prueba instrumentadas, pero no en la ejecución uninstrumented (aunque en nuestra experiencia, hemos encontrado que esto normalmente no es un problema). De nuevo, una herramienta como CHESS puede ayudar. Nuestra herramienta prototipo permite medir contención a través de operaciones de monitor y Interlocked. En el futuro, pensamos sobre cómo agregar características que nos medir a otras primitivas de sincronización como semáforo y ManualResetEvent permitirá. Creemos que una métrica de cobertura de código, como cobertura de sincronización, podría ser tan útil y generalizado para las aplicaciones simultáneas como cobertura de la instrucción es para aplicaciones de subproceso único.
Medir un debe
No se puede mejorar lo que no se puede medir. Por lo tanto, a medida que desarrolla aplicaciones de software multiproceso y más, se debe medir minuciosamente cómo hemos probado los aspectos de simultaneidad de nuestro software. Cobertura de sincronización es una forma simple y práctica para ello. Esperamos que, como desplazarse este nuevo mundo de la informática multinúcleo, puede tomar las ideas en este artículo para mejorar el proceso de calidad.
Más información
- Paralelo de informática de Microsoft:
MSDN.Microsoft.com/Concurrency - El papel de cobertura de sincronización:
Bron, r., Farchi, e., Magid, s., Nir, s. y UR, S. 2005. Aplicaciones de cobertura de sincronización. En procedimientos del décimo simposio ACM SIGPLAN en principios y prácticas de programación paralela (Chicago, Ill., EE.UU., 15-17 de junio de 2005). PPoPP 05 '. ACM, Nueva York, N.Y. 212 206. - La herramienta CHESS:
Research.Microsoft.com/en-us/Projects/Chess/default.aspx
Musuvathi, M. y Qadeer, S. 2007. Contexto iterativa de límite de pruebas sistemático de programas multiproceso. En procedimientos de la conferencia ACM SIGPLAN 2007 en la programación de lenguaje diseño e implementación (San Diego, Calif., EE.UU., 10-13 de junio de 2007). PLDI ' 07. ACM, New York, N.Y., 446-455. - Infraestructura de compilador comunes (CCI):
ccimetadata.codeplex.com
Roy tan es ingeniero de desarrollo de software de prueba con el grupo de Parallel Computing Platform de Microsoft. Recibido su Dr. en informática en Virginia Tech en 2007.
Chris Dern es un ingeniero de desarrollo de software de concurrencia en prueba para el equipo Parallel Computing Platform de Microsoft--donde lo único que mejor que escribir software de concurrencia es probarlo.