|
El tema de sincronizar varios hilos de forma óptima es bastante complicado. El sistema operativo nos ofrece varias posibilidades de sincronismo que garantizan un correcto uso de recursos compartidos, permitiendo que varios núcleos accedan a la misma zona de memoria sin pisarse. El problema es que este sincronismo suele ser especialmente lento si se usa de forma intensiva, porque nuestro programa tiene que ejecutar código que está en "espacio de kernel", o sea, que tiene que cambiar de tarea, implicar al kernel del sistema (invalidando caches), y despues volver a activar nuestro programa. Fácilmente unos miles de ciclos. Si estos bloqueos son en sistios conocidos del sistema, y se usan de forma racional, no hay problema. Pero al programar gráficos y cosas de "alto rendimiento", esto puede reventar el rendimiento de la aplicación. Un ejemplo es el insertar información en estructuras de aceleración en paralelo, etc. Una solución muy usada son los "spin locks ". Exactamente qué significa esto? Pues es tan sencillo como hacer un búcle que compruebe una variable continuamente. Cuando un hilo entra, la pone a "falso" y el resto se quedan en un bucle esperando hasta que sea verdadero. Esto que parece sencillo, tiene 2 problemas. El primero es el evidente gasto de CPU. Si el código protegido por el spin lock es grande, entonces los otros hilos perderán muchos ciclos en un absurdo bucle... Pero si el código es pequeño (como un simple cambio en una variable, una insercción en una lista, etc) y además, el tiempo de espera preveemos que sea pequeño, entonces compensará ejecutar este bucle, antes de perderse en un bloqueo de sistema. El otro problema es el pensar en cómo escribimos el código. En ensamblador, una instrucción de C tan sencillo como "a=b+1" tiene varios pasos: - calcular la dirección de b - cargar b en un registro - incrementar el registro en 1 - mover el valor del registro en a Qué pasa si hay muchos hilos? pues cuando movemos el valor al registro, el otro thread no se entera, y puede operar en paralelo con ese dato. Así, mientras movemos b a un registro, e incrementamos 1, otro hilo puede haber modificado b, con lo que el resultado final será incorrecto. Además, los compiladores pueden meter más instrucciones entre el principio y el final del incremento, para aprovechar ciclos o ir adelantando tiempo antes de la escritura en b, por lo que las probabilidades de cambio aumentan.No hablemos de temas de cache, ya que podemos escribir b en una memoria intermedia, y otro hilo no darse cuenta de todo esto hasta pasados unos ciclos. Incrementar una variable es algo que ocurre mucho en entornos multihilo, por ejemplo, un contador de referencias. Seria bastante fuerte tener que hacer un bloqueo de sistema en cada uso de un "smart pointer". La solución es hacer ese incremento "atómico". O sea, que en 1 sola instrucción podamos resolver el incremento, evitando que el valor de la variable incrementada pueda ser modificado en otro hilo. Para ello, existen una familia de instrucciones que permiten operar de forma atómica en las variables. Este es el enlace a la lista de operaciones de microsoft . Un spinlock aprovecha esto para comprobar que el bucle realmente bloquea el acceso. El código seria algo como: // inicialmente, lock es el puntero a una variable que contiene 0 (desbloqueado) // esperamos a que lock valga 0 while (::InterlockedExchange( (LONG *) lock, 1) != 0) { // puedo llamar a Switch thread aqui para que el procesador se ocupe de otras cosas más importantes antes que yo. } // hago lo que tenga que hacer // restauro el valor de lock, poniendolo a 0 ::InterlockedExchange( (volatile LONG *) lock, 0);
|