jueves, 16 de julio de 2015

Receta Multithreading en C# No. 2-8: Uso de la Estructura SpinWait - Ahorro de Ciclos de CPU

Índice

0. Introducción
1. Problema
2. Solución
3. Discusión de la Solución
3.1 La estructura SpinWait
4. Conclusiones
5. Glosario
6. Literatura & Enlaces

0. Introducción

Última receta multithreading de la serie Sincronización de Threads: en esta receta exploraremos la estructura SpinWait con la que podemos poner en espera un thread sin recurrir al uso de contrucciones basadas en el modo kernel: el thread pasará a modo bloqueado sin consumir ciclos de procesador. Esto nos llevará a entender que al alternación entre estos dos modos de operación conlleva a un ahorro de tiempo (o ciclos) de la Unidad de Procesamiento Central (CPU).

1. Problema

Requerimos aplicar un modo espera en un thread que optimice el consumo de ciclos de procesador.

2. Solución

En el namespace System.Threading encontramos la estructura SpinWait que optimiza el tiempo de uso de procesador.

3. Discusión de la Solución

3.1 La estructura SpinWait

La estructura SpinWait [2] está diseñada para poner en espera un thread sin ocupar tiempo de procesador en busy waits (~esperas desocupadas). Podemos exponer literalmente el comentario en [2]:
"SpinWait encapsulates common spinning logic. On single-processor machines, yields are always used instead of busy waits, and on computers with Intel processor employing Hyper-Threading technology, it helps to prevent hardware thread starvation."
SpinWait está diseñada como una construcción de sincronización híbrida, lo que quiere decir que la espera se realiza en modo bloqueado y no en modo usuario, de esta manera se evita ocupar tiempo de CPU de innecesaria.

Continuando, SpinWait es dependiente de la velocidad del procesador, por lo tanto un experimento efectuado con esta estructura puede variar significativamente de equipo en equipo dependiendo del número de núcleos lógicos y físicos.

Ahora podemos pasar a escribir un ejemplo de uso basado en el código presentando en [2]:

Ejemplo de uso:

En la línea 15 declaramos la variable valorCentinela para indicar cuando el thraed asociado con la estructura SpinWait debe terminar. Creamos un instancia de Task para ejecutar un thread que itera indefinidamente (líneas 24-34) y donde:
  • Línea 28: Se valida con NextSpinWaillYield si el control pasó al procesador o simple se ha efectuado un simple ~giro (spin). De ser así, incrementamos en una unidad la variable numeroCedes.
  • Línea 33: Invocamos el método SpinOnce [4] para dar un nuevo ~giro (spin).
El ciclo while se interrumpirá asignando true se asigne a la variable valorCentinela.


Casi al final, creamos un nuevo objeto Task para controlar cuando se ha de cambiar el valor de la variable valorCentinela. Cuando esto ocurra el ciclo del primer thread se interrumpirá y se culminará las llamadas a SpinOnce.



Compilación:



  1. csc /target:exe UsoSpinWait.cs


Ejecución assembly:



  1. .\UsoSpinWait.exe


> Prueba de ejecución: http://ideone.com/OsqwDQ



> Prueba de ejecución:
Ejecución assembly UsoSpinWait.exe
Figura 1. Ejecución assembly UsoSpinWait.exe.

En las 5 ocasiones que se ejecutó el assembly UsoSpinWait.exe la mayor parte de operaciones fueron cedidas al procesador; sin embargo, hay que dejar claro que esto ha sido para tiempos de espera corto. También quiero apuntar que la ejecución de este programa se realizó sobre un equipo con un procesador AMD FX 6300 de 3 núcleos físicos y 6 lógicos y puede variar en otro máquina con distinta configuración.

4. Práctica: Código C#

Hagamos una adaptación al ejemplo de SpinWait presentando en [1]. En este ejemplo demostraremos la diferencia entre el modo de espera de usuario y el modo de espera de kernel (o híbrido).

Archivo C# ModoUsuarioVsModoKernel.cs [Enlace alternativo]:

En la línea 10 creamos la variable static finalizado:bool con volatile [4] para indicar que el valor asignado estará presente y puede ser modificado desde cualquier thread sin generar ningún problema de sincronización.


Con el método ModoEsperaUsuario (líneas 37-46) simulamos un tiempo de espera de usuario con la ejecución indefinida del ciclo while: este ciclo no finaliza sino hasta que el valor de la variable finalizado cambie a true.



Por otro lado, con el método ModoEsperaKernel (líneas 46-60) simulamos el tiempo de espera híbrido (o por kernel). La parte más interesante ocurre en esta sección del programa donde se muestra que con SpinWait la espera se hacen en modo usuario (manteniendo un consumo considerable de ciclos de CPU) durante poco tiempo y después el control pasa un estado de bloqueo (reduciendo así el consumo de ciclos de CPU).



En Main (líneas 12-34):

  • Líneas 16-17: Creación de dos threads que referencian los métodos mencionados anteriormente.
  • Línea 20: Se inicia la prueba del modo espera basado en usuario.
  • Línea 22: Suspendemos el tiempo de espera asignando true a la variable finalizado.
  • Línea 26: Asignamos nuevamente true a la variable finalizado para poder iniciar la prueba del modo de espera basado en kernel o híbrido.
  • Línea 29: Iniciamos el thread para simular la espera en modo híbrido.
Compilación:

  1. csc /target:exe ModoUsuarioVsModoKernel.cs

Ejecución assembly:

  1. .\ModoUsuarioVsModoKernel.cs

> Prueba de ejecución: http://ideone.com/p0BT3b


> Prueba de ejecución:
Ejecución del assembly ModoUsuarioVsModoKernel.exe
Animación 1. Ejecución del assembly ModoUsuarioVsModoKernel.exe.
Notemos lo que ocurre en la Animación 1: cuando ejecutamos el modo de espera en modo híbrido: durante los primeros ciclos el control está en modo usuario (false), después el thread (t2) pasa a modo bloqueado (true) y de este modo reduciendo el consumo innecesario de ciclos de procesador.

5. Conclusiones

En esta última receta multithreading aprendimos a distinguir los modos de espera: usuario e híbrido (con SpinWait). La espera basada en usuario puede significar el uso innecesario de recursos, y más en particular, ciclos de procesador.


A partir de la siguiente receta multithreading nos enfocaremos en las técnicas básicas y comunes para la gestión de un pool (grupo) de threads.

6. Glosario

  • Ciclo
  • Core
  • CPU
  • Espera
  • Híbrido
  • Núcleo
  • Procesador
  • Sincronización
  • Thread
  • Tiempo

7. Literatura & Enlaces

[1]: Multithreading in C# 5.0 Cookbook by Eugene Agafonov. Copyright 2013 Eugene Agafonov, 978-1-84969-764-4.
[2]: SpinWait Structure (System.Threading) - https://msdn.microsoft.com/en-us/library/system.threading.spinwait(v=vs.110).aspx
[3]: SpinWait.NextSpinWillYield Property (System.Threading) - https://msdn.microsoft.com/en-us/library/system.threading.spinwait.nextspinwillyield(v=vs.110).aspx
[4]: volatile (C# Reference) - https://msdn.microsoft.com/en-us/library/x13ttww7.aspx


A

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.