Para ver el artículo en inglés, active la casilla Inglés. También puede ver el texto en inglés en una ventana emergente si pasa el puntero del mouse por el texto.
Traducción
Inglés

The Managed Thread Pool

 

La clase ThreadPool proporciona a la aplicación un grupo de subprocesos de trabajo administrados por el sistema, que le permite concentrarse en las tareas de la aplicación en lugar de en la administración de los subprocesos. Si tiene tareas cortas que requieran procesamiento en segundo plano, el grupo de subprocesos administrados permite aprovechar fácilmente las ventajas de varios subprocesos. Por ejemplo, a partir de .NET Framework 4, puede crear objetos Task y Task<TResult>, que realizan tareas asincrónicas en los subprocesos del grupo de subprocesos.

System_CAPS_noteNota

A partir de .NET Framework 2.0 Service Pack 1, mejora considerablemente el rendimiento del grupo de subprocesos en tres áreas clave que se identificaron como cuellos de botella en las versiones anteriores de .NET Framework: poner tareas en cola, enviar subprocesos del grupo de subprocesos y enviar subprocesos de finalización de E/S. Para utilizar esta funcionalidad, la aplicación debe utilizar .NET Framework 3.5 o posterior como destino.

Para las tareas en segundo plano que interactúan con la interfaz de usuario, la versión 2.0 de .NET Framework también proporciona la clase BackgroundWorker, que se comunica mediante eventos generados en el subproceso de la interfaz de usuario.

.NET Framework utiliza los subprocesos del grupo de subprocesos para muchos fines, incluida la finalización de E/S asincrónica, las devoluciones de llamada del temporizador, las operaciones de espera registradas, las llamadas de métodos asincrónicos mediante delegados y las conexiones de socket System.Net.

Hay varios escenarios en los que es adecuado crear y administrar sus propios subprocesos en lugar de utilizar subprocesos del grupo de subprocesos:

  • Necesita un subproceso en primer plano.

  • Necesita que un subproceso tenga una prioridad determinada.

  • Tiene tareas que hacen que el subproceso se bloquee durante largos períodos de tiempo. El grupo de subprocesos tiene un número máximo de subprocesos, por lo que si hay un gran número de subprocesos del grupo de subprocesos bloqueados, esto puede impedir que se inicien las tareas.

  • Debe colocar los subprocesos en un contenedor uniproceso. Todos los subprocesos ThreadPool están en el contenedor multiproceso.

  • Debe tener una identidad estable asociada al subproceso o dedicar un subproceso a una tarea.

Los subprocesos del grupo de subprocesos son subprocesos en segundo plano. Consulte Foreground and Background Threads- Cada subproceso utiliza el tamaño de pila predeterminado, se ejecuta con la prioridad predeterminada y está en el contenedor multiproceso.

Hay solo un grupo de subprocesos por cada proceso.

Las excepciones no controladas en los subprocesos del grupo de subprocesos finalizan el proceso. Hay tres excepciones de esta regla:

  • Se genera un ThreadAbortException en un subproceso del grupo de subprocesos, porque se llamó a Abort.

  • Se genera un AppDomainUnloadedException en un subproceso del grupo de subprocesos, porque se está descargando el dominio de aplicación.

  • Common Language Runtime o un proceso de host finaliza el subproceso.

Para más información, consulte Exceptions in Managed Threads.

System_CAPS_noteNota

En las versiones 1.0 y 1.1 de .NET Framework, Common Language Runtime intercepta silenciosamente las excepciones no controladas en los subprocesos del grupo de subprocesos. Esto puede dañar el estado de la aplicación y puede acabar haciendo que las aplicaciones no respondan, lo cual puede ser muy difícil de depurar.

El número de operaciones que pueden ponerse en la cola del grupo de subprocesos está limitado solamente por la memoria disponible. Sin embargo, el grupo de subprocesos limita el número de subprocesos que pueden estar activos en el proceso de forma simultánea. A partir de .NET Framework 4, el tamaño predeterminado del grupo de subprocesos de un proceso depende de varios factores, como el tamaño del espacio de direcciones virtuales. Un proceso puede llamar al método GetMaxThreads para determinar el número de subprocesos.

El número máximo de subprocesos se puede controlar con los métodos GetMaxThreads y SetMaxThreads.

System_CAPS_noteNota

En las versiones 1.0 y 1.1 de .NET Framework, el tamaño del grupo de subprocesos no se puede establecer desde el código administrado. El código que hospeda Common Language Runtime puede establecer el tamaño con CorSetMaxThreads, definido en mscoree.h.

El grupo de subprocesos ofrece nuevos subprocesos de trabajo o subprocesos de finalización de E/S a petición hasta que llega a un mínimo especificado para cada categoría. Puede utilizar el método GetMinThreads para obtener estos valores mínimos.

System_CAPS_noteNota

Cuando la demanda es baja, el número real de subprocesos del grupo de subprocesos puede descender por debajo de los valores mínimos.

Cuando se alcanza el mínimo, el grupo de subprocesos puede crear subprocesos adicionales o esperar hasta que se completen algunas tareas. A partir de .NET Framework 4, el grupo de subprocesos crea y destruye subprocesos de trabajo para optimizar el rendimiento, definido como el número de tareas que se completan por unidad de tiempo. Si hay demasiados pocos subprocesos, puede que los recursos disponibles no se usen de manera óptima, mientras que si hay demasiados subprocesos, puede aumentar la contención de recursos.

System_CAPS_cautionPrecaución

Puede utilizar el método SetMinThreads para aumentar el número mínimo de subprocesos inactivos. Sin embargo, aumentar innecesariamente estos valores puede causar problemas de rendimiento. Si se inician demasiadas tareas al mismo tiempo, puede que todas ellas parezcan funcionar con lentitud. En la mayoría de los casos, el grupo de subprocesos funciona mejor con su propio algoritmo de asignación de subprocesos.

El grupo de subprocesos también proporciona los métodos ThreadPool.UnsafeQueueUserWorkItem y ThreadPool.UnsafeRegisterWaitForSingleObject. Utilice estos métodos solamente cuando tenga la seguridad de que la pila del llamador es irrelevante para las comprobaciones de seguridad que se realizan durante la ejecución de la tarea en cola. QueueUserWorkItemy RegisterWaitForSingleObject capturan la pila del llamador, que se combina en la pila del subproceso del grupo de subprocesos cuando el subproceso empieza a ejecutar una tarea. Si es necesaria una comprobación de seguridad, debe comprobarse toda la pila. La comprobación proporciona seguridad, pero también supone un coste para el rendimiento.

A partir de .NET Framework 4, la manera más fácil de usar el grupo de subprocesos es utilizar la Task Parallel Library (TPL). De forma predeterminada, los tipos de biblioteca de procesamiento paralelo, como Task y Task<TResult>, utilizan subprocesos del grupo de subprocesos para ejecutar tareas. También puede utilizar el grupo de subprocesos llamando a ThreadPool.QueueUserWorkItem desde código administrado (o CorQueueUserWorkItem desde código no administrado) y pasando un delegado WaitCallback que represente al método que realiza la tarea. Otra forma de usar el grupo de subprocesos es poner en cola los elementos de trabajo que están relacionados con una operación de espera mediante el método ThreadPool.RegisterWaitForSingleObject y pasar un WaitHandle que, cuando se señala o cuando se agota el tiempo de espera, llame al método representado por el delegado WaitOrTimerCallback. Los subprocesos del grupo de subprocesos se usan para invocar métodos de devolución de llamada.

En el siguiente ejemplo, se muestra cómo crear y usar un objeto Task para llamar al método TaskFactory.StartNew. Para ver un ejemplo que utiliza la clase Task<TResult> con el fin de devolver un valor de una tarea asincrónica, consulte How to: Return a Value from a Task.

using System;
using System.Threading;
using System.Threading.Tasks;

class Example
{
    static void Main()
    {
        Action<object> action = (object obj) =>
                                {
                                   Console.WriteLine("Task={0}, obj={1}, Thread={2}",
                                   Task.CurrentId, obj,
                                   Thread.CurrentThread.ManagedThreadId);
                                };

        // Create a task but do not start it.
        Task t1 = new Task(action, "alpha");

        // Construct a started task
        Task t2 = Task.Factory.StartNew(action, "beta");
        // Block the main thread to demonstate that t2 is executing
        t2.Wait();

        // Launch t1 
        t1.Start();
        Console.WriteLine("t1 has been launched. (Main Thread={0})",
                          Thread.CurrentThread.ManagedThreadId);
        // Wait for the task to finish.
        t1.Wait();

        // Construct a started task using Task.Run.
        String taskData = "delta";
        Task t3 = Task.Run( () => {Console.WriteLine("Task={0}, obj={1}, Thread={2}",
                                                     Task.CurrentId, taskData,
                                                      Thread.CurrentThread.ManagedThreadId);
                                   });
        // Wait for the task to finish.
        t3.Wait();

        // Construct an unstarted task
        Task t4 = new Task(action, "gamma");
        // Run it synchronously
        t4.RunSynchronously();
        // Although the task was run synchronously, it is a good practice
        // to wait for it in the event exceptions were thrown by the task.
        t4.Wait();
    }
}
// The example displays output like the following:
//       Task=1, obj=beta, Thread=3
//       t1 has been launched. (Main Thread=1)
//       Task=2, obj=alpha, Thread=4
//       Task=3, obj=delta, Thread=3
//       Task=4, obj=gamma, Thread=1

En el ejemplo siguiente, se pone en cola una tarea muy sencilla, representada por el método ThreadProc, con el método QueueUserWorkItem.

using System;
using System.Threading;

public class Example
{
    public static void Main()
    {
        // Queue the task.
        ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));

        Console.WriteLine("Main thread does some work, then sleeps.");
        // If you comment out the Sleep, the main thread exits before
        // the thread pool task runs.  The thread pool uses background
        // threads, which do not keep the application running.  (This
        // is a simple example of a race condition.)
        Thread.Sleep(1000);

        Console.WriteLine("Main thread exits.");
    }

    // This thread procedure performs the task.
    static void ThreadProc(Object stateInfo)
    {
        // No state object was passed to QueueUserWorkItem, so
        // stateInfo is null.
        Console.WriteLine("Hello from the thread pool.");
    }
}

El siguiente ejemplo de código utiliza el método QueueUserWorkItem para poner en cola una tarea y proporcionar los datos de la tarea.

using System;
using System.Threading;

// TaskInfo holds state information for a task that will be
// executed by a ThreadPool thread.
public class TaskInfo
{
    // State information for the task.  These members
    // can be implemented as read-only properties, read/write
    // properties with validation, and so on, as required.
    public string Boilerplate;
    public int Value;

    // Public constructor provides an easy way to supply all
    // the information needed for the task.
    public TaskInfo(string text, int number)
    {
        Boilerplate = text;
        Value = number;
    }
}

public class Example
{
    public static void Main()
    {
        // Create an object containing the information needed
        // for the task.
        TaskInfo ti = new TaskInfo("This report displays the number {0}.", 42);

        // Queue the task and data.
        if (ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc), ti))
        {
            Console.WriteLine("Main thread does some work, then sleeps.");

            // If you comment out the Sleep, the main thread exits before
            // the ThreadPool task has a chance to run.  ThreadPool uses
            // background threads, which do not keep the application
            // running.  (This is a simple example of a race condition.)
            Thread.Sleep(1000);

            Console.WriteLine("Main thread exits.");
        }
        else
        {
            Console.WriteLine("Unable to queue ThreadPool request.");
        }
    }

    // The thread procedure performs the independent task, in this case
    // formatting and printing a very simple report.
    //
    static void ThreadProc(Object stateInfo)
    {
        TaskInfo ti = (TaskInfo) stateInfo;
        Console.WriteLine(ti.Boilerplate, ti.Value);
    }
}

En el ejemplo siguiente se muestran diversas características de subprocesamiento:

using System;
using System.Threading;

// TaskInfo contains data that will be passed to the callback
// method.
public class TaskInfo
{
    public RegisteredWaitHandle Handle = null;
    public string OtherInfo = "default";
}

public class Example
{
    public static void Main(string[] args)
    {
        // The main thread uses AutoResetEvent to signal the
        // registered wait handle, which executes the callback
        // method.
        AutoResetEvent ev = new AutoResetEvent(false);

        TaskInfo ti = new TaskInfo();
        ti.OtherInfo = "First task";
        // The TaskInfo for the task includes the registered wait
        // handle returned by RegisterWaitForSingleObject.  This
        // allows the wait to be terminated when the object has
        // been signaled once (see WaitProc).
        ti.Handle = ThreadPool.RegisterWaitForSingleObject(
            ev,
            new WaitOrTimerCallback(WaitProc),
            ti,
            1000,
            false );

        // The main thread waits three seconds, to demonstrate the
        // time-outs on the queued thread, and then signals.
        Thread.Sleep(3100);
        Console.WriteLine("Main thread signals.");
        ev.Set();

        // The main thread sleeps, which should give the callback
        // method time to execute.  If you comment out this line, the
        // program usually ends before the ThreadPool thread can execute.
        Thread.Sleep(1000);
        // If you start a thread yourself, you can wait for it to end
        // by calling Thread.Join.  This option is not available with
        // thread pool threads.
    }

    // The callback method executes when the registered wait times out,
    // or when the WaitHandle (in this case AutoResetEvent) is signaled.
    // WaitProc unregisters the WaitHandle the first time the event is
    // signaled.
    public static void WaitProc(object state, bool timedOut)
    {
        // The state object must be cast to the correct type, because the
        // signature of the WaitOrTimerCallback delegate specifies type
        // Object.
        TaskInfo ti = (TaskInfo) state;

        string cause = "TIMED OUT";
        if (!timedOut)
        {
            cause = "SIGNALED";
            // If the callback method executes because the WaitHandle is
            // signaled, stop future execution of the callback method
            // by unregistering the WaitHandle.
            if (ti.Handle != null)
                ti.Handle.Unregister(null);
        }

        Console.WriteLine("WaitProc( {0} ) executes on thread {1}; cause = {2}.",
            ti.OtherInfo,
            Thread.CurrentThread.GetHashCode().ToString(),
            cause
        );
    }
}
Mostrar: