C#:uso de ManualResetEventSlim y AutoResetEvent para señalar subprocesos en espera

C#:uso de ManualResetEventSlim y AutoResetEvent para señalar subprocesos en espera

En el desarrollo de software hay muchas formas de resolver un mismo problema. Se trata de saber qué opciones están disponibles y usar el enfoque más simple para el trabajo.

Cuando desea que uno o más subprocesos esperen hasta que sean señalados antes de continuar, ¿cómo lo hace? Hay muchas opciones que puede utilizar para lograr esto.

Uno de los enfoques más simples es usar un ManualResetEventSlim o AutoResetEvent, como este:

static string deviceData = null;
static ManualResetEventSlim gotDataSignal;
static void Main(string[] args)
{
	gotDataSignal = new ManualResetEventSlim();

	while (true)
	{
		Console.WriteLine("Running device simulation loop.");


		Task.Run(DeviceSimulation);

		Console.WriteLine("Thread 1 waiting for gotDataSignal");
		gotDataSignal.Wait();

		Console.WriteLine($"Thread 1 signaled, got data {deviceData}");
		Console.WriteLine("Resetting signal for next simulation");

		gotDataSignal.Reset();

	}
}
static void DeviceSimulation()
{
	Console.WriteLine("Thread 2 - type anything to simulate getting device data");
	deviceData = Console.ReadLine();

	Console.WriteLine("Thread 2 signaling Thread 1 that it got data");
	gotDataSignal.Set();
}
Code language: C# (cs)

Ejecutar esto da como resultado el siguiente resultado:

Running device simulation loop.
Thread 1 waiting for gotDataSignal
Thread 2 - type anything to simulate getting device data
0001 1000
Thread 2 signaling Thread 1 that it got data
Thread 1 signaled, got data 0001 1000
Resetting signal for next simulation
Running device simulation loop.
Thread 1 waiting for gotDataSignal
Thread 2 - type anything to simulate getting device data
f
Thread 2 signaling Thread 1 that it got data
Thread 1 signaled, got data f
Resetting signal for next simulation
Running device simulation loop.
Thread 1 waiting for gotDataSignal
Thread 2 - type anything to simulate getting device dataCode language: plaintext (plaintext)

Es posible que haya notado que esto está llamando a Reset(). Sin llamar a esto, el manejador de espera del evento permanece en un estado señalado y cualquier subproceso que llame a Wait() no se bloqueará. Aquí es donde ManualResetEventSlim o AutoResetEvent obtienen sus extraños nombres, y es la principal diferencia entre ellos. ManualResetEventSlim requiere que llame a Reset(), mientras que AutoResetEvent llama automáticamente a Reset() después de llamar a Set().

En las secciones a continuación, mostraré ejemplos que muestran la diferencia clave entre ManaulResetEventSlim (señala todos los hilos a la vez) y AutoResetEvent (señala un hilo a la vez).

ManualResetEventSlim:señala todos los subprocesos en espera

ManualResetEventSlim es como ondear una bandera a cuadros en una carrera de autos. Todos los autos de carrera (hilos de espera) se alinean en la línea de salida y esperan la bandera a cuadros, y luego todos comienzan.

ManualResetEventSlim es fácil de usar. Créelo, haga que los subprocesos llamen a Wait() y llamen a Set() para permitir que todos los subprocesos pasen a la vez. Como su nombre indica, debe llamar a Reset() para bloquear manualmente todos los subprocesos en espera futuros. Nota:No llamaré a Reset() a continuación, porque el propósito principal de esta sección es mostrar cómo ManualResetEventSlim señala todos los hilos a la vez.

El siguiente código muestra esta analogía de carrera de autos en la práctica.

static void Main(string[] args)
{
	Console.WriteLine("Welcome to the race track.");
	Console.WriteLine("Your job is to wave the checkered flag once all race cars are lined up");
	Console.WriteLine("Press anything + enter to wave the flag");

	using (var checkeredFlag = new ManualResetEventSlim())
	{

		for (int i = 1; i <= 10; i++)
		{
			var raceCarNumber = i; //capture for closure
			Task.Run(() =>
			{
				Console.WriteLine($"Race car {raceCarNumber} is ready");
				checkeredFlag.Wait();

				for(int j = 0; j < 100; j++)
				{
					//simulate laps around the track
				}

				Console.WriteLine($"Race car {raceCarNumber} finished");

			});
		}

		Console.ReadLine();
		Console.WriteLine("Ready");
		Console.WriteLine("Set");
		Console.WriteLine("Go!");

		checkeredFlag.Set();

		Console.ReadLine();
	}
}
Code language: C# (cs)

Ejecutar este código produce el siguiente resultado.

Welcome to the race track.
Your job is to wave the checkered flag once all race cars are lined up
Press anything + enter to wave the flag
Race car 1 is ready
Race car 7 is ready
Race car 5 is ready
Race car 6 is ready
Race car 3 is ready
Race car 4 is ready
Race car 8 is ready
Race car 2 is ready
Race car 9 is ready
Race car 10 is ready
Start race
Ready
Set
Go!
Race car 9 finished
Race car 3 finished
Race car 2 finished
Race car 4 finished
Race car 10 finished
Race car 1 finished
Race car 7 finished
Race car 6 finished
Race car 5 finished
Race car 8 finishedCode language: plaintext (plaintext)

Como puede ver, todos los autos (hilos de espera) fueron señalados al mismo tiempo.

AutoResetEvent:señala un hilo a la vez

AutoResetEvent es como una tienda con un carril de pago. Solo se puede atender a un cliente (hilo en espera) a la vez. El resto de los clientes tienen que seguir esperando.

AutoResetEvent es fácil de usar. Créelo, haga que los subprocesos llamen a WaitOne() y llame a Set() para dejar pasar un subproceso a la vez.

El siguiente código muestra esta analogía del carril de pago en la práctica.

static void Main(string[] args)
{

	Console.WriteLine("Welcome to the store!");
	Console.WriteLine("There's one checkout lane, so customers will have to queue up");
	Console.WriteLine("Type anything to signify the next customer can be checked out");



	using (var checkoutLaneCashier = new AutoResetEvent(initialState: false))
	{
		for (int i = 1; i <= 5; i++)
		{
			var customerNumber = i; //capture for closure
			Task.Run(() =>
			{
				Console.WriteLine($"Customer {customerNumber} is waiting in line");
				checkoutLaneCashier.WaitOne();
				Console.WriteLine($"Customer {customerNumber} is now checking out");

				//simulate check out process
				Thread.Sleep(50);

				Console.WriteLine($"Customer {customerNumber} is done checking out");

			});
		}


		while (true)
		{
			Console.ReadLine();
			Console.WriteLine("Serving next customer");
			checkoutLaneCashier.Set();
		}
	}
}
Code language: C# (cs)

Ejecutar este código produce el siguiente resultado.

Welcome to the store!
There's one checkout lane, so customers will have to queue up
Type anything to signify the next customer can be checked out
Customer 2 is waiting in line
Customer 5 is waiting in line
Customer 4 is waiting in line
Customer 1 is waiting in line
Customer 3 is waiting in line
next
Serving next customer
Customer 2 is now checking out
Customer 2 is done checking out
next
Serving next customer
Customer 5 is now checking out
Customer 5 is done checking out
next
Serving next customer
Customer 4 is now checking out
Customer 4 is done checking out
next
Serving next customer
Customer 1 is now checking out
Customer 1 is done checking out
next
Serving next customer
Customer 3 is now checking out
Customer 3 is done checking out
Code language: plaintext (plaintext)

Compare esto con ManualResetEventSlim. En este caso, tuve que seguir escribiendo algo (escribí "siguiente" cada vez) para que llamara a Set(), permitiendo que un cliente pasara por la línea de pago a la vez.

Esperar con tiempo de espera o token de cancelación

Por lo general, no es una buena idea esperar incondicionalmente. Por lo general, debe especificar un tiempo de espera, pasar un token de cancelación o pasar un token de cancelación con un tiempo de espera.

//wait with a timeout
signal.Wait(TimeSpan.FromSeconds(5));

//wait with a cancel token
signal.Wait(new CancellationTokenSource().Token);

//wait with a cancel token with a timeout
signal.Wait(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
Code language: C# (cs)

La opción que elija dependerá de su escenario específico.

Por ejemplo, supongamos que su software acepta un pago y espera que un cliente interactúe con un dispositivo de pago. Es posible que tenga un hilo que esté esperando los datos de pago. El cliente o el cajero pueden querer cancelar la transacción. En este caso, podría llamar a Cancel() en el token de cancelación para detener el hilo que está esperando los datos del dispositivo.