Necesito comprender el uso de SemaphoreSlim

90

Aquí está el código que tengo pero no entiendo qué SemaphoreSlimestá haciendo.

async Task WorkerMainAsync()
{
    SemaphoreSlim ss = new SemaphoreSlim(10);
    List<Task> trackedTasks = new List<Task>();
    while (DoMore())
    {
        await ss.WaitAsync();
        trackedTasks.Add(Task.Run(() =>
        {
            DoPollingThenWorkAsync();
            ss.Release();
        }));
    }
    await Task.WhenAll(trackedTasks);
}

void DoPollingThenWorkAsync()
{
    var msg = Poll();
    if (msg != null)
    {
        Thread.Sleep(2000); // process the long running CPU-bound job
    }
}

¿Qué espera ss.WaitAsync();y ss.Release();qué hace?

Supongo que si ejecuto 50 subprocesos a la vez, luego escribo código como, SemaphoreSlim ss = new SemaphoreSlim(10);entonces se verá obligado a ejecutar 10 subprocesos activos a la vez.

Cuando se completa uno de los 10 subprocesos, comenzará otro subproceso. Si no estoy en lo cierto, ayúdame a comprender la situación de la muestra.

¿Por qué se awaitnecesita junto con ss.WaitAsync();? ¿Qué hace ss.WaitAsync();?

Mou
fuente
3
Una cosa a tener en cuenta es que realmente debería ajustar ese "DoPollingThenWorkAsync ();" en un "intento {DoPollingThenWorkAsync ();} finalmente {ss.Release ();}", de lo contrario, las excepciones dejarán sin efecto ese semáforo de forma permanente.
Austin Salgat
Me siento un poco extraño que adquiera y liberemos el semáforo afuera / adentro de la tarea respectivamente. ¿Mover "await ss.WaitAsync ()" dentro de la tarea hará alguna diferencia?
Shane Lu

Respuestas:

73

supongo que si ejecuto 50 subprocesos a la vez, codifique como SemaphoreSlim ss = new SemaphoreSlim (10); forzará a ejecutar 10 subprocesos activos a la vez

Eso es correcto; el uso del semáforo asegura que no habrá más de 10 trabajadores haciendo este trabajo al mismo tiempo.

Llamar WaitAsyncal semáforo produce una tarea que se completará cuando ese hilo tenga "acceso" a ese token. awaitHacer esa tarea permite que el programa continúe la ejecución cuando está "permitido" hacerlo. Tener una versión asincrónica, en lugar de llamar Wait, es importante tanto para garantizar que el método permanezca asincrónico, en lugar de ser síncrono, como para lidiar con el hecho de que un asyncmétodo puede estar ejecutando código en varios subprocesos, debido a las devoluciones de llamada, etc. la afinidad del hilo natural con los semáforos puede ser un problema.

Una nota al margen: DoPollingThenWorkAsyncno debería tener el Asyncsufijo porque en realidad no es asincrónico, es sincrónico. Solo llámalo DoPollingThenWork. Reducirá la confusión de los lectores.

Servy
fuente
gracias, pero por favor, dígame qué sucede cuando especificamos el número de subprocesos para ejecutar, digamos 10, cuando uno de los 10 subprocesos termina y, de nuevo, ese subproceso salta para terminar otros trabajos o vuelve al grupo. esto no está muy claro para .... así que por favor explique lo que sucede detrás de escena.
Mou
@Mou ¿Qué no está claro al respecto? El código espera hasta que haya menos de 10 tareas en ejecución; cuando los hay, agrega otro. Cuando una tarea termina, indica que ha terminado. Eso es.
Servicio
cuál es la ventaja de especificar no de hilo para ejecutar. si demasiados hilos pueden obstaculizar el rendimiento? Si es así, ¿por qué obstaculizar ... si ejecuto 50 subprocesos en lugar de 10 subprocesos, entonces por qué el rendimiento importará ... puede explicarme? gracias
Thomas
4
@Thomas Si tiene demasiados subprocesos simultáneos, los subprocesos pasan más tiempo cambiando de contexto que haciendo un trabajo productivo. El rendimiento disminuye a medida que aumentan los subprocesos a medida que pasa más y más tiempo administrando subprocesos en lugar de trabajar, al menos, una vez que el recuento de subprocesos supera con creces la cantidad de núcleos en la máquina.
Servicio
3
@Servy Eso es parte del trabajo del programador de tareas. Tareas! = Hilos. El Thread.Sleepen el código original devastaría el programador de tareas. Si no es asincrónico con el núcleo, no es asincrónico.
Joseph Lennox
54

En el jardín de infancia a la vuelta de la esquina, usan un SemaphoreSlim para controlar cuántos niños pueden jugar en la sala de educación física.

Pintaron en el suelo, fuera de la habitación, 5 pares de huellas.

Cuando llegan los niños, dejan sus zapatos en un par de huellas libres y entran a la habitación.

Una vez que terminan de jugar, salen, recogen sus zapatos y "liberan" un espacio para otro niño.

Si llega un niño y no quedan huellas, van a jugar a otro lugar o simplemente se quedan un rato y revisan de vez en cuando (es decir, no hay prioridades FIFO).

Cuando una maestra está cerca, ella "suelta" una fila adicional de 5 huellas en el otro lado del pasillo para que 5 niños más puedan jugar en la habitación al mismo tiempo.

También tiene las mismas "trampas" de SemaphoreSlim ...

Si un niño termina de jugar y sale de la habitación sin recoger los zapatos (no activa la "liberación"), la ranura permanece bloqueada, aunque teóricamente haya una ranura vacía. Sin embargo, el niño suele ser regañado.

A veces, uno o dos niños astutos esconden sus zapatos en otro lugar y entran en la habitación, incluso si ya se han tomado todas las huellas (es decir, el SemaphoreSlim no controla "realmente" cuántos niños hay en la habitación).

Esto no suele terminar bien, ya que el hacinamiento de la sala tiende a terminar en niños llorando y el maestro cerrando completamente la sala.

dandiez
fuente
3
Este tipo de respuestas son mis favoritas.
My Stack Overfloweth
¡Dios mío, esto es informativo y divertido como diablos!
Zonus
7

Aunque acepto que esta pregunta realmente se relaciona con un escenario de bloqueo de cuenta regresiva, pensé que valía la pena compartir este enlace que descubrí para aquellos que deseen usar un SemaphoreSlim como un simple bloqueo asincrónico. Le permite usar la declaración using que podría hacer que la codificación sea más ordenada y segura.

http://www.tomdupont.net/2016/03/how-to-release-semaphore-with-using.html

Yo cambié _isDisposed=truey_semaphore.Release() en su Dispose en caso de que de alguna manera se llamara varias veces.

También es importante tener en cuenta que SemaphoreSlim no es un bloqueo reentrante, lo que significa que si el mismo hilo llama a WaitAsync varias veces, el recuento que tiene el semáforo se reduce cada vez. En resumen, SemaphoreSlim no es consciente de los subprocesos.

Con respecto a las preguntas sobre la calidad del código, es mejor poner la versión en un último intento para asegurarse de que siempre se publique.

Andrew Paté
fuente
6
No es aconsejable publicar respuestas de solo enlaces, ya que los enlaces tienden a morir con el tiempo, lo que hace que la respuesta no tenga valor. Si puede, es mejor resumir los puntos clave o el bloque de código clave en su respuesta.
John