C# Language
Enhebrado
Buscar..
Observaciones
Un hilo es una parte de un programa que puede ejecutarse independientemente de otras partes. Puede realizar tareas simultáneamente con otros hilos. El subprocesamiento múltiple es una función que permite a los programas realizar un procesamiento simultáneo para que se pueda realizar más de una operación a la vez.
Por ejemplo, puede utilizar subprocesos para actualizar un temporizador o contador en segundo plano mientras realiza simultáneamente otras tareas en primer plano.
Las aplicaciones de subprocesos múltiples responden mejor a las aportaciones de los usuarios y también son fácilmente escalables, porque el desarrollador puede agregar subprocesos a medida que aumenta la carga de trabajo.
Por defecto, un programa C # tiene un hilo: el hilo principal del programa. Sin embargo, los subprocesos secundarios se pueden crear y utilizar para ejecutar código en paralelo con el subproceso principal. Tales hilos se llaman hilos de trabajo.
Para controlar el funcionamiento de un subproceso, el CLR delega una función al sistema operativo conocido como Thread Scheduler. Un programador de hilos asegura que a todos los hilos se les asigna el tiempo de ejecución adecuado. También comprueba que los subprocesos que están bloqueados o bloqueados no consumen mucho tiempo de CPU.
El .NET Framework System.Threading
espacio de nombres facilita el uso de subprocesos. System.Threading permite el subprocesamiento múltiple al proporcionar una serie de clases e interfaces. Además de proporcionar tipos y clases para un subproceso en particular, también define tipos para contener una colección de subprocesos, una clase de temporizador, etc. También proporciona su soporte al permitir el acceso sincronizado a los datos compartidos.
Thread
es la clase principal en el System.Threading
nombres System.Threading
. Otras clases incluyen AutoResetEvent
, Interlocked
, Monitor
, Mutex
y ThreadPool
.
Algunos de los delegados que están presentes en el System.Threading
nombres de System.Threading
incluyen ThreadStart
, TimerCallback
y WaitCallback
.
Las enumeraciones en el System.Threading
nombres de System.Threading
incluyen ThreadPriority
, ThreadState
y EventResetMode
.
En .NET Framework 4 y versiones posteriores, la programación multihilo se hace más fácil y sencilla a través de los System.Threading.Tasks.Parallel
y System.Threading.Tasks.Task
nuevas clases de colección concurrentes clases, Parallel LINQ (PLINQ), en los System.Collections.Concurrent
nombres System.Collections.Concurrent
y un nuevo modelo de programación basado en tareas.
Demostración de subprocesos completa simple
class Program
{
static void Main(string[] args)
{
// Create 2 thread objects. We're using delegates because we need to pass
// parameters to the threads.
var thread1 = new Thread(new ThreadStart(() => PerformAction(1)));
var thread2 = new Thread(new ThreadStart(() => PerformAction(2)));
// Start the threads running
thread1.Start();
// NB: as soon as the above line kicks off the thread, the next line starts;
// even if thread1 is still processing.
thread2.Start();
// Wait for thread1 to complete before continuing
thread1.Join();
// Wait for thread2 to complete before continuing
thread2.Join();
Console.WriteLine("Done");
Console.ReadKey();
}
// Simple method to help demonstrate the threads running in parallel.
static void PerformAction(int id)
{
var rnd = new Random(id);
for (int i = 0; i < 100; i++)
{
Console.WriteLine("Thread: {0}: {1}", id, i);
Thread.Sleep(rnd.Next(0, 1000));
}
}
}
Demostración de subprocesos completa simple usando tareas
class Program
{
static void Main(string[] args)
{
// Run 2 Tasks.
var task1 = Task.Run(() => PerformAction(1)));
var task2 = Task.Run(() => PerformAction(2)));
// Wait (i.e. block this thread) until both Tasks are complete.
Task.WaitAll(new [] { task1, task2 });
Console.WriteLine("Done");
Console.ReadKey();
}
// Simple method to help demonstrate the threads running in parallel.
static void PerformAction(int id)
{
var rnd = new Random(id);
for (int i = 0; i < 100; i++)
{
Console.WriteLine("Task: {0}: {1}", id, i);
Thread.Sleep(rnd.Next(0, 1000));
}
}
}
Paralismo explícito de tareas
private static void explicitTaskParallism()
{
Thread.CurrentThread.Name = "Main";
// Create a task and supply a user delegate by using a lambda expression.
Task taskA = new Task(() => Console.WriteLine($"Hello from task {nameof(taskA)}."));
Task taskB = new Task(() => Console.WriteLine($"Hello from task {nameof(taskB)}."));
// Start the task.
taskA.Start();
taskB.Start();
// Output a message from the calling thread.
Console.WriteLine("Hello from thread '{0}'.",
Thread.CurrentThread.Name);
taskA.Wait();
taskB.Wait();
Console.Read();
}
Paralelismo implícito de tareas
private static void Main(string[] args)
{
var a = new A();
var b = new B();
//implicit task parallelism
Parallel.Invoke(
() => a.DoSomeWork(),
() => b.DoSomeOtherWork()
);
}
Creando e iniciando un segundo hilo
Si está realizando varios cálculos largos, puede ejecutarlos al mismo tiempo en diferentes subprocesos en su computadora. Para hacer esto, hacemos un nuevo hilo y lo apuntamos a un método diferente.
using System.Threading;
class MainClass {
static void Main() {
var thread = new Thread(Secondary);
thread.Start();
}
static void Secondary() {
System.Console.WriteLine("Hello World!");
}
}
Comenzando un hilo con parámetros
utilizando System.Threading;
class MainClass {
static void Main() {
var thread = new Thread(Secondary);
thread.Start("SecondThread");
}
static void Secondary(object threadName) {
System.Console.WriteLine("Hello World from thread: " + threadName);
}
}
Creando un hilo por procesador
Environment.ProcessorCount
Obtiene el número de procesadores lógicos en la máquina actual.
El CLR luego programará cada subproceso a un procesador lógico, esto teóricamente podría significar cada subproceso en un procesador lógico diferente, todos los subprocesos en un solo procesador lógico o alguna otra combinación.
using System;
using System.Threading;
class MainClass {
static void Main() {
for (int i = 0; i < Environment.ProcessorCount; i++) {
var thread = new Thread(Secondary);
thread.Start(i);
}
}
static void Secondary(object threadNumber) {
System.Console.WriteLine("Hello World from thread: " + threadNumber);
}
}
Evitar leer y escribir datos simultáneamente
A veces, quieres que tus hilos compartan datos simultáneamente. Cuando esto sucede, es importante conocer el código y bloquear cualquier parte que pueda salir mal. Un ejemplo simple de dos hilos contando se muestra a continuación.
Aquí hay un código peligroso (incorrecto):
using System.Threading;
class MainClass
{
static int count { get; set; }
static void Main()
{
for (int i = 1; i <= 2; i++)
{
var thread = new Thread(ThreadMethod);
thread.Start(i);
Thread.Sleep(500);
}
}
static void ThreadMethod(object threadNumber)
{
while (true)
{
var temp = count;
System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
Thread.Sleep(1000);
count = temp + 1;
System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
Thread.Sleep(1000);
}
}
}
Notarás, en lugar de contar 1,2,3,4,5 ... contamos 1,1,2,2,3 ...
Para solucionar este problema, debemos bloquear el valor de conteo, de modo que varios subprocesos diferentes no puedan leer y escribir en él al mismo tiempo. Con la adición de un candado y una llave, podemos evitar que los hilos accedan a los datos simultáneamente.
using System.Threading;
class MainClass
{
static int count { get; set; }
static readonly object key = new object();
static void Main()
{
for (int i = 1; i <= 2; i++)
{
var thread = new Thread(ThreadMethod);
thread.Start(i);
Thread.Sleep(500);
}
}
static void ThreadMethod(object threadNumber)
{
while (true)
{
lock (key)
{
var temp = count;
System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
Thread.Sleep(1000);
count = temp + 1;
System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
}
Thread.Sleep(1000);
}
}
}
Parallel.ForEach Loop
Si tiene un bucle foreach que desea acelerar y no le importa en qué orden está la salida, puede convertirlo en un bucle foreach paralelo haciendo lo siguiente:
using System;
using System.Threading;
using System.Threading.Tasks;
public class MainClass {
public static void Main() {
int[] Numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Single-threaded
Console.WriteLine("Normal foreach loop: ");
foreach (var number in Numbers) {
Console.WriteLine(longCalculation(number));
}
// This is the Parallel (Multi-threaded solution)
Console.WriteLine("Parallel foreach loop: ");
Parallel.ForEach(Numbers, number => {
Console.WriteLine(longCalculation(number));
});
}
private static int longCalculation(int number) {
Thread.Sleep(1000); // Sleep to simulate a long calculation
return number * number;
}
}
Puntos muertos (dos hilos esperando el uno al otro)
Un interbloqueo es lo que ocurre cuando dos o más subprocesos están esperando que se completen o liberen un recurso de tal manera que esperen para siempre.
Un escenario típico de dos subprocesos que esperan que se completen entre sí es cuando un subproceso de la GUI de Windows Forms espera un subproceso de trabajo y el subproceso de trabajo intenta invocar un objeto administrado por el subproceso de la GUI. Observe que con este ejemplo de código, hacer clic en el botón 1 hará que el programa se bloquee.
private void button1_Click(object sender, EventArgs e)
{
Thread workerthread= new Thread(dowork);
workerthread.Start();
workerthread.Join();
// Do something after
}
private void dowork()
{
// Do something before
textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
// Do something after
}
workerthread.Join()
es una llamada que bloquea el subproceso de llamada hasta que workerthread se complete. textBox1.Invoke(invoke_delegate)
es una llamada que bloquea el subproceso de llamada hasta que el subproceso de la GUI haya procesado invoke_delegate, pero esta llamada provoca interbloqueos si el subproceso de la GUI ya está esperando a que se complete el subproceso de la llamada.
Para solucionar esto, uno puede usar una forma no bloqueante de invocar el cuadro de texto en su lugar:
private void dowork()
{
// Do work
textBox1.BeginInvoke(new Action(() => textBox1.Text = "Some Text"));
// Do work that is not dependent on textBox1 being updated first
}
Sin embargo, esto causará problemas si necesita ejecutar un código que depende de que el cuadro de texto se actualice primero. En ese caso, ejecute eso como parte de la invocación, pero tenga en cuenta que esto hará que se ejecute en el hilo de la GUI.
private void dowork()
{
// Do work
textBox1.BeginInvoke(new Action(() => {
textBox1.Text = "Some Text";
// Do work dependent on textBox1 being updated first,
// start another worker thread or raise an event
}));
// Do work that is not dependent on textBox1 being updated first
}
Alternativamente, inicie un hilo completamente nuevo y deje que ese haga la espera en el hilo de la GUI, para que ese hilo de trabajo pueda completarse.
private void dowork()
{
// Do work
Thread workerthread2 = new Thread(() =>
{
textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
// Do work dependent on textBox1 being updated first,
// start another worker thread or raise an event
});
workerthread2.Start();
// Do work that is not dependent on textBox1 being updated first
}
Para minimizar el riesgo de encontrarse con un interbloqueo de espera mutua, siempre evite referencias circulares entre hilos cuando sea posible. Una jerarquía de subprocesos en la que los subprocesos de menor rango solo dejan mensajes para los subprocesos de mayor rango y nunca los espera, no se encontrará con este tipo de problema. Sin embargo, aún sería vulnerable a puntos muertos en función del bloqueo de recursos.
Puntos muertos (mantener el recurso y esperar)
Un interbloqueo es lo que ocurre cuando dos o más subprocesos están esperando que se completen o liberen un recurso de tal manera que esperen para siempre.
Si thread1 mantiene un bloqueo en el recurso A y está esperando que se libere el recurso B, mientras que thread2 mantiene el recurso B y está esperando que se libere el recurso A, están bloqueados.
Al hacer clic en el botón 1 para el siguiente código de ejemplo, su aplicación entrará en el estado de interbloqueo mencionado anteriormente y se bloqueará
private void button_Click(object sender, EventArgs e)
{
DeadlockWorkers workers = new DeadlockWorkers();
workers.StartThreads();
textBox.Text = workers.GetResult();
}
private class DeadlockWorkers
{
Thread thread1, thread2;
object resourceA = new object();
object resourceB = new object();
string output;
public void StartThreads()
{
thread1 = new Thread(Thread1DoWork);
thread2 = new Thread(Thread2DoWork);
thread1.Start();
thread2.Start();
}
public string GetResult()
{
thread1.Join();
thread2.Join();
return output;
}
public void Thread1DoWork()
{
Thread.Sleep(100);
lock (resourceA)
{
Thread.Sleep(100);
lock (resourceB)
{
output += "T1#";
}
}
}
public void Thread2DoWork()
{
Thread.Sleep(100);
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
}
Para evitar estar bloqueado de esta manera, se puede usar Monitor.TryEnter (lock_object, timeout_in_milliseconds) para verificar si ya hay un bloqueo en un objeto. Si Monitor.TryEnter no logra adquirir un bloqueo en lock_object antes de timeout_in_milliseconds, devuelve falso, lo que le da al hilo la oportunidad de liberar otros recursos retenidos y el rendimiento, dando así a otros hilos la oportunidad de completar, como en esta versión ligeramente modificada del anterior :
private void button_Click(object sender, EventArgs e)
{
MonitorWorkers workers = new MonitorWorkers();
workers.StartThreads();
textBox.Text = workers.GetResult();
}
private class MonitorWorkers
{
Thread thread1, thread2;
object resourceA = new object();
object resourceB = new object();
string output;
public void StartThreads()
{
thread1 = new Thread(Thread1DoWork);
thread2 = new Thread(Thread2DoWork);
thread1.Start();
thread2.Start();
}
public string GetResult()
{
thread1.Join();
thread2.Join();
return output;
}
public void Thread1DoWork()
{
bool mustDoWork = true;
Thread.Sleep(100);
while (mustDoWork)
{
lock (resourceA)
{
Thread.Sleep(100);
if (Monitor.TryEnter(resourceB, 0))
{
output += "T1#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
public void Thread2DoWork()
{
Thread.Sleep(100);
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
}
Tenga en cuenta que esta solución temporal se basa en que el subproceso2 es obstinado en cuanto a sus bloqueos y el subproceso1 está dispuesto a ceder, de modo que el subproceso2 siempre tiene prioridad. También tenga en cuenta que el subproceso1 debe rehacer el trabajo que realizó después de bloquear el recurso A, cuando cede. Por lo tanto, tenga cuidado al implementar este enfoque con más de un subproceso de rendimiento, ya que correrá el riesgo de ingresar a un llamado Livelock, un estado que se produciría si dos subprocesos siguieran haciendo el primer bit de su trabajo y luego se rindieran mutuamente. , volviendo a empezar repetidamente.