Controlling the Execution of Tasks


The primary purpose of the task manager is to enable the host to provide the CLR with the basic primitives it needs to create, execute, abort, and otherwise manage tasks in the process. In scenarios in which a host does not provide an implementation of the task manager, the CLR calls the Microsoft Win32 API CreateThread each time it needs to create a thread, it calls the Win32 SetPriority API to adjust a thread's priority, and so on. However, when a host provides the CLR with a task manager, all such calls are sent to the host instead of directly to the operating system. In this way, the host can deeply integrate the CLR's management of tasks with its own.

The following four interfaces comprise the task manager:

  • IHostTaskManager

  • ICLRTaskManager

  • IHostTask

  • ICLRTask

IHostTaskManager is the primary interface in the task manager. That is, it is the interface the CLR will ask the host for during startup by passing the IID for IHostTaskManager to the GetHostManager method on the host's implementation of IHostControl. Hosts express their desire to provide an implementation of the task manager by returning a valid interface of type IHostTaskManager when asked. Generally speaking, IHostTaskManager provides the CLR with a set of methods it can use to create individual tasks and to notify the host of special situations, including periods of time when the CLR cannot tolerate a thread abort or when calls transition into and out of the CLR when an add-in uses PInvoke or COM interoperability. Table 14-1 describes the methods on IHostTaskManager.

Table 14-1. The Methods on IHostTaskManager

Method

Description

GetCurrentTask

Returns the task that is currently running on the thread from which GetCurrentTask is called.

CreateTask

Enables the CLR to create a new task.

Sleep

Causes the current task to sleep for a given number of milliseconds. The CLR calls this method when an add-in calls System.Threading.Thread.Sleep, for example.

SwitchToTask

Notifies the host that it should switch out the currently running task. In SQL Server fiber mode, calls to SwitchToTask cause the fiber representing the current task to be unscheduled or taken off the thread on which it is currently running.

SetLocale

Notifies the host that the culture has been changed. The CLR calls SetLocale when an add-in sets the System.Threading.Thread.CurrentCulture property, for example. By notifying the host of changes made to the culture, the CLR gives the host a chance to adjust any culture-related state it maintains internally. For example, SQL Server 2005 has its own notion of culture that is used for sorting database tables and so on. Calls to SetLocale enable SQL Server to stay in sync with changes to culture made by the add-ins it is hosting.

SetUILocale

Notifies the host that the UI culture has been changed. The CLR calls SetUILocale when an add-in sets the System.Threading.Thread.CurrentUICulture property. By notifying the host of changes made to the UI culture, the CLR gives the host a chance to adjust any culturerelated state it maintains internally.

LeaveRuntime

In the section "Hooking Calls That Enter and Leave the CLR" later in this chapter, I describe how the CLR calls methods on IHostTaskManager to enable the host to hook calls that transition between managed and unmanaged code. Calls to LeaveRuntime notify the host that a transition from managed to unmanaged code is about to occur. A transition from managed to unmanaged code would occur if an add-in were to use PInvoke to call a method in a Win32 DLL, for example.

EnterRuntime

EnterRuntime is called to notify the host that a previous transition from managed to unmanaged code (as indicated by a previous call to LeaveRuntime) is now returning. See "Hooking Calls That Enter and Leave the CLR" later in this chapter for more details on how the calls to LeaveRuntime and EnterRuntime are related.

ReverseEnterRuntime

Transitions between managed and unmanaged code can be nested. ReverseEnterRuntime notifies the host that a previously identified transition from managed to unmanaged code has initiated a call back into managed code. See "Hooking Calls That Enter and Leave the CLR" later in this chapter for more details about how such transitions can be nested.

ReverseLeaveRuntime

Notifies the host that a nested transition is going back out to unmanaged code from managed code. See "Hooking Calls That Enter and Leave the CLR" for more details.

BeginThreadAffinity

At times, the CLR requires the current task not be moved to a different physical thread. If SQL Server 2005 is running in fiber mode, this affinity requirement specifically means that the current fiber should not be rescheduled on a different thread. The CLR calls BeginThreadAffinity to notify the host that a period of time in which thread affinity is required is beginning. There are various points during the initialization of the CLR when thread affinity is required.

EndThreadAffinity

Calls to EndThreadAffinity notify the host that a period requiring thread affinity is ending. The host is now free to reschedule the current task on a different physical thread if desired.

BeginDelayAbort

As you saw in Chapter 11, the ability to abort a thread or unload an application domain is key to the CLR's ability to support hosts that require long process lifetimes. There are periods of time, however, when the CLR cannot tolerate a thread abort. The CLR calls BeginDelayAbort to notify the host when such a period of time begins.

EndDelayAbort

Calls to EndDelayAbort notify the host that the CLR is now able to tolerate thread aborts again. Each call to BeginDelayAbort has a corresponding call to EndThreadAbort.

SetCLRTaskManager

Provides the host with a pointer to the CLR's implementation of ICLRTaskManager.


After the CLR obtains a pointer to the host's implementation of IHostTaskManager by calling IHostControl::GetHostManager, it gives the host a pointer to its implementation of ICLRTaskManager by calling the SetCLRTaskManager method on IHostTaskManager. ICLRTaskManager is the mirror image of IHostTaskManager in that it provides the host with a set of methods for controlling the CLR's basic task-related functions. The methods on ICLRTaskManager are shown in Table 14-2.

Table 14-2. The Methods on ICLRTaskManager

Method

Description

CreateTask

Enables the host to create a new task. As you'll see later in the section entitled "The Life Cycle of a Task," either the CLR or the host can initiate the creation of a new task.

GetCurrentTask

Returns the CLR's notion of the task running on the calling thread.

SetUILocale

Notifies the CLR that the UI culture has been changed by the host. In most cases, a host will call this if the UI culture has been changed by some unmanaged code (either by the host or by an unmanaged addin) running in the host's process. ICLRTaskManager::SetUILocale is the mirror image of IHostTaskManager::SetUILocale.

SetLocale

Notifies the CLR that the culture has been changed. ICLRTaskManager::SetLocale is the mirror image of IHostTaskManager::SetLocale.

GetCurrentTaskType

The CLR creates a few threads itself regardless of whether the host has provided an implementation of the task manager. Examples of these threads include those used for garbage collection and those used to control debugging. Hosts can use GetCurrentTaskType to determine whether the current task is one of the special tasks that the CLR creates directly. See the sidebar "CLR Threads Created Independently of the Hosting API" for more details on the threads the CLR creates itself.


The other two interfaces in the task manager, IHostTask and ICLRTask, represent individual tasks that have been created in the process. As shown in Figure 14-2, each process contains one instance each of IHostTaskManager and ICLRTaskManager and many instances of IHostTask and ICLRTask. Each task created in the process is represented by a matched pair of IHostTask and ICLRTask implementations. As their names suggest, IHostTask is the view of the task as implemented by the host and ICLRTask is implemented by the CLR.

Figure 14-2. A matched pair of IHostTask and ICLRTask interfaces represents each task in the process.


IHostTask and ICLRTask provide operations that the CLR and the host, respectively, can use to manipulate a specific task. Tables 14-3 and 14-4 describe the methods on these two interfaces.

Table 14-3. The Methods on IHostTask

Method

Description

Start

Instructs the host to begin execution of the task.

Alert

Wakes a task up so it can be aborted. The CLR will call Alert when aborting a thread or unloading an application domain.

Join

Blocks the current task either until the task on which Join is called terminates or a specified interval of time passes.

SetPriority

Sets the task's priority. One scenario in which the CLR calls SetPriority is when an add-in adjusts a task's priority using the Priority property on System.Threading.Thread.

GetPriority

Gets the task's priority.

SetCLRTask

When called by the CLR, associates an implementation of ICLRTask with this instance of IHostTask.


Table 14-4. The Methods on ICLRTask

Method

Description

SwitchIn

Notifies the CLR that the host is scheduling this task to run. In fiber mode, calls to SwitchIn indicate that the fiber representing this task is about to begin executing on a physical thread.

SwitchOut

Notifies the CLR that this task is being unscheduled. In fiber mode, calls to SwitchOut indicate that the fiber representing this task is being removed from running on a physical thread.

GetMemStats

Returns a structure of type COR_GC_THREAD_STATS that contains statistics about the amount of memory allocated by this task.

Reset

Clears all internal state related to a task that is to be reused. The CLR allows its representation of a task to be reused to prevent the host from paying the cost of re-creating large numbers of tasks. A host calls Reset in preparation for task reuse. One example of state that is cleared is all data stored on the task when an add-in calls System.Threading.Thread.Alloc(Named)DataSlot.

ExitTask

Notifies the CLR that the task is exiting normally.

Abort

Notifies the CLR that the task is being aborted.

RudeAbort

Notifies the CLR that the task is being rudely aborted. Refer to Chapter 11 for details on the differences between an abort and a rude abort.

NeedsPriorityScheduling

Notifies the host that the task must be rescheduled. One of the challenges in integrating the CLR into SQL Server 2005 is to ensure that the SQL Server scheduler doesn't schedule tasks in such a way as to interfere with the CLR's garbage collector. Specifically, if the CLR is attempting to get all tasks to a state in which it is safe to perform a garbage collection, but a given task isn't being scheduled frequently enough to make progress toward a safe state, the garbage collector can end up waiting for an unacceptable amount of time before it can proceed with a collection. The return value from NeedsPriorityRescheduling indicates to the host that the task should be rescheduled as soon as possible. A host can use this information to place the task near the beginning of the scheduling queue instead of at the end, for example.

YieldTask

Causes the CLR to attempt to bring the task to a state in which it will yield. This method is used in an attempt to get long-running code to give up the CPU in scenarios in which the host uses cooperative task scheduling.

LocksHeld

Returns the number of locks (synchronization primitives) currently held by the task.

SetTaskIdentifier

Associates a host-defined identifier with the task. This identifier is used primarily to group related tasks together in debugger output. The CLR simply passes this identifier on to the debugger. The identifier isn't used internally at all.


Now that I've described the basic capabilities offered by the task manager, take a look at how the specific interfaces are used during the typical life cycle of a task.

The Life Cycle of a Task

In general, the life cycle of a task consists of three primary phases:

  1. The task is created.

  2. Tasks are repeatedly switched in and out as they execute.

  3. The task terminates.

Figure 14-3 shows the relationship between these phases over time for a host that is running the CLR on cooperatively scheduled fibers.

Figure 14-3. The typical life cycle of a task


The numbered steps in Figure 14-3 are described in the following points:

1.

Tasks are in the unscheduled, or "not-running," state when they are first created. The creation of a new task is typically initiated by the CLR, but a host can create a new task as well. The CLR creates a new task by calling the CreateTask method on IHostTaskManager. Next, the CLR creates an instance of ICLRTask to associate with the new task the host has just created. The link between the CLR's representation of the task and the host's representation is established when the CLR passes its instance of ICLRTask to the SetCLRTask method of the IHostTask that represents the new task. When all the interfaces are in place, the CLR calls the Start method on IHostTask to move the task to a state in which it can be scheduled.

2.

At some point, the host will schedule the task for execution. The host calls the SwitchIn method on ICLRTask to notify the CLR that the task is about to be scheduled.

3.

Later, the host might decide to switch the task out or remove it from the thread and place it back in a nonrunning state. A host might choose to do this if the running task blocks on a synchronization primitive, is waiting for I/O to complete, and so on. A host notifies the CLR that a task is being switched out by calling ICLRTask::SwitchOut.

4.

The next time the task is scheduled, it's quite possible it can be scheduled to run on a different physical thread than it ran on previously. This is shown in Figure 14-3 when the task that first ran on OS Thread 1 is placed on OS Thread 2 when rescheduled.

However, in some scenarios the CLR requires thread affinity for a given task. Periods of thread affinity are defined when the CLR calls IHostTaskManager::BeginThreadAffinity and IHostTaskManager::EndThreadAffinity. In this particular diagram, when the time comes to reschedule it, the host must schedule the task back on OS Thread 1 if the task requires thread affinity.

5.

Eventually, a CLR task ends permanently. This generally happens when the code running on the task reaches its natural end. In this scenario, the host calls ICLRTask::ExitTask to notify the CLR that the task is ending and that the implementation of ICLRTask can be destroyed. Tasks can also end in aborts or rude aborts as indicated, respectively, by calls to the Abort or RudeAbort methods of ICLRTask. It is also possible to reuse an implementation of ICLRTask instead of destroying it when the task it currently represents terminates. To reuse a task, a host calls ICLRTask::Reset to reset the CLR's representation of the task to a clean state. The instance of ICLRTask can then be reused in the future to represent a new task.

CLR Threads Created Independently of the Hosting API

When an add-in or other managed code running in a process requests that a new task be created (by using System.Threading.Thread, for example), that request is mapped by the CLR to the host through the task manager as discussed. However, not all threads the CLR creates in a process are redirected through the host in this way. Regardless of whether it is hosted or not, the CLR always creates a few threads for its internal implementation using the Win32 API. These threads include the following:

  • The threads for performing garbage collections

  • A thread for interacting with debuggers

  • A thread that gates access to the thread pool

  • An internal timer thread

The threads the CLR creates without the aid of the host are represented by the values from the ETaskType enumeration (minus the TT_USER value). Here's its definition from mscoree.idl:

typedef enum ETaskType {    TT_DEBUGGERHELPER = 0x1,    TT_GC = 0x2,    TT_FINALIZER = 0x4,    TT_THREADPOOL_TIMER = 0x8,    TT_THREADPOOL_GATE = 0x10,    TT_THREADPOOL_WORKER = 0x20,    TT_THREADPOOL_IOCOMPLETION = 0x40,    TT_ADUNLOAD = 0x80,    TT_USER = 0x100,    TT_THREADPOOL_WAIT = 0x200,    TT_UNKNOWN = 0x80000000, } ETaskType;


Hooking Calls That Enter and Leave the CLR

In addition to the ability to provide an abstraction over the basic unit of execution and the method of scheduling, the task manager also enables a host to intercept all calls that transition between managed and unmanaged code. Transitions out of managed code occur when an add-in (or your host runtime assembly) accesses a Win32 DLL through either PInvoke or COM interoperability. Transitions into managed code occur when code in a Win32 DLL calls into managed code either through reverse PInvoke[1] or COM interoperability.

[1] By reverse PInvoke, I'm referring to the scenario in which a Win32 DLL calls into managed code through a delegate that had been previously marshaled out to native code as a function pointer.

The ability to hook these transitions is important to hosts that employ cooperative scheduling because once a call leaves the CLR for unmanaged code, the thread on which that call is made is no longer under the CLR's (or the host's) control. As a result, the host can no longer schedule cooperative tasks to run on that thread. Instead, the host must allow the thread to be scheduled preemptively by the operating system until it returns to managed code, at which point the thread can be used again by the host's cooperative scheduler.

The CLR notifies the host of transitions into and out of managed code through a series of calls to the following methods on IHostTaskManager:

  • LeaveRuntime

  • EnterRuntime

  • ReverseEnterRuntime

  • ReverseLeaveRuntime

You can easily guess how LeaveRuntime and EnterRuntime are used: the CLR calls LeaveRuntime each time a call transitions out of managed code and EnterRuntime when that call returns. The use of ReverseEnterRuntime and ReverseLeaveRuntime isn't quite so obvious, however. These calls exist to support the nested transitions between managed and unmanaged code.

Here is an example to give you an idea of how these methods are used when transitions into and out of the CLR are nested. Figure 14-4 shows the sequence of calls that occur when three transitions are nested.

Figure 14-4. Nested calls between managed and unmanaged code


The sequence of calls the CLR makes to the methods on IHostTaskManager to notify the host of the transitions shown in Figure 14-4 is as follows:

1.

The PInvoke call from MethodA to NativeCall1 initiates the sequence of transitions. The CLR calls LeaveRuntime to notify the host that control is transferring from managed code to unmanaged code. Notice that NativeCall1 takes a parameter that is a function pointer. MethodA passes an instance of MethodB as this parameter.

2.

The implementation of NativeCall1 calls the function identified by the function pointer it is passed. In this case, the function pointer is to a managed method (MethodB), so calling through the function pointer causes a call from unmanaged code to managed code. This transition is nested because the call the CLR previously identified through a call to LeaveRuntime (the call from MethodA to NativeCall1) has not completed. The CLR calls ReverseEnterRuntime to notify the host of this nested transition back into managed code.

3.

MethodB uses PInvoke to transition back out to unmanaged code using a call to NativeCall2. Because the call to NativeCall2 initiates a new series of transitions from managed to unmanaged code, the CLR notifies the host of this transition by calling LeaveRuntime.

4.

NativeCall2 returns to the CLR with no further nesting. EnterRuntime is called to indicate that the previous call to LeaveRuntime is now returning.

5.

The executing of MethodB is now complete. Upon return from MethodB, control will transfer back out to unmanaged code. Because the call to MethodB occurred within a nested transition, its transition from managed code to unmanaged code is communicated by a call to ReverseLeaveRuntime. The call to ReverseLeaveRuntime completes the transition that started in step 2 with a call to ReverseEnterRuntime.

6.

The call from managed code to unmanaged code that initiated this whole sequence is now complete. The CLR notifies the host of the return from NativeCall1 back into managed code by calling EnterRuntime.

As you can see, nested transitions such as the sequence shown in Figure 14-4 can result in multiple interleaved calls to LeaveRuntime, EnterRuntime, ReverseLeaveRuntime, and ReverseEnterRuntime. Figure 14-5 shows how the sequence of calls used to notify the host of the transitions that occur in Figure 14-4 would be represented on the call stack. Notice that calls to LeaveRuntime and EnterRuntime are always paired, as are calls to ReverseEnterRuntime and ReverseLeaveRuntime.

Figure 14-5. A stack representing nested transitions between managed and unmanaged code


The Synchronization Manager

In the last few sections, I've shown how SQL Server 2005 implements a task manager to integrate the execution of managed code with SQL Server's custom scheduler. Although the capabilities offered by the task manager provide many of the basic features that are needed, the task manager alone doesn't provide all that's required to achieve a high-performance integration between the two products. Specifically, the use of synchronization primitives, either by the CLR or by the managed add-ins it executes, has a direct impact on scalability.

For example, say the CLR needs to synchronize access to a resource from multiple threads. The traditional way to accomplish such synchronization is to use a Win32 API, such as CreateEvent, to obtain an object that can be used for synchronization. However, if such a lock were taken on a thread without informing SQL Server, the thread that is currently waiting on the synchronization primitive would be blocked, and therefore would be unavailable for SQL Server to schedule. As a result, the overall efficiency of the system is reduced.

What's needed instead is a way for the host to supply the CLR with the synchronization primitives it typically gets from the operating system. In this way, if the CLR were to block waiting for a synchronization primitive to be signaled, SQL Server could unschedule that blocked task, thereby making the thread available for other tasks to run. At some later point in time, SQL Server would reschedule the blocked task to give it a chance to obtain the lock and become unblocked. The ability to supply the CLR with synchronization primitives in this way is exactly the role of the synchronization manager in the CLR hosting API. By providing the CLR with the basic primitives it needs to synchronize access to resources, the host is not only able to detect cases of contention, but also to consider the locks taken by managed code when determining how to resolve a deadlock. The synchronization manager enables the CLR to create the following types of synchronization primitives through the host:

  • Critical sections

  • Semaphores

  • Monitors

  • Reader/writer locks

  • Auto-reset events

  • Manual-reset events

Support for these primitives is provided by the six interfaces that comprise the synchronization manager. In addition to interfaces that represent specific types of synchronization primitives, the synchronization manager also includes two interfaces that enable the creation and exchange of ownership data for individual primitives. The following points provide an overview of the interfaces that make up the synchronization manager.

  • IHostSyncManager As the primary interface in the synchronization manager, IHostSyncManager is the interface the CLR asks the host for at startup through a call to IHostControl::GetHostManager. In addition to its role as the primary interface, IHostSyncManager contains the methods the CLR uses to create individual synchronization primitives. These methods include CreateSemaphore, CreateMonitorEvent, CreateAutoEvent, and so on.

  • ICLRSyncManager After the CLR obtains a pointer of type IHostSyncManager from the host, it provides the host with an implementation of ICLRSyncManager by calling IHostSyncManager::SetCLRSyncManager. Hosts use ICLRSyncManager to determine which task owns a particular synchronization primitive.

  • IHostSemaphore Semaphores are represented in the CLR hosting API by the IHostSemaphore interface. The CLR uses methods on IHostSemaphore to wait for a particular semaphore to become available and to release the semaphore when it is no longer needed.

  • IHostManualEvent Manual reset events are represented by the IHostManualEvent interface. In addition to providing the CLR with manual reset events, IHostManualEvent is also used in the implementation of reader/writer locks. IHostManualEvent has methods corresponding to the operations that are typically performed on events created using the Win32 API. For example, IHostManualEvent enables the CLR to wait on the event and to set or reset the event.

  • IHostAutoEvent Auto reset events are represented by the IHostAutoEvent interface. As with IHostManualEvent, IHostAutoEvent contains methods for waiting on the event and for setting the event. In addition to their use as stand-alone synchronization primitives, auto-reset events are also used in the implementation of monitors and reader/writer locks.

  • IHostCrst The CLR manages critical sections created by the host using the IHostCrst interface. IHostCrst has methods that enable the CLR to enter and leave the critical section. In addition, IHostCrst also contains a spincount used to implement lightweight locks when the critical section isn't contested.

Replacing the CLR's Thread Pool

The final threading-related hosting manager to cover is the thread pool manager. As discussed in Chapter 5, and as you are probably aware from your own experience with .NET Framework programming, the CLR provides a process-wide thread pool that makes it easier to write scalable multithreaded applications. By providing an implementation of the thread pool manager, you are able to replace the CLR's thread pool with your own implementation.

The requirement to be able to supply a custom thread pool comes from the integration of the CLR into SQL Server 2005, as many of the other threading-related requirements have. As you've seen throughout this chapter, SQL Server closely manages how threads are used in its process, including the number of threads that are created. If the CLR would continue to implement its own pool of threads while running in SQL Server, several threads wouldn't be under the control of SQL Server. Given requirements for scalability in SQL Server, any threads running in the process that are not integrated with the custom scheduling mechanism of SQL Server can throw off the internal optimizations of SQL Server, and performance can be hampered.

The thread pool manager consists of a single interface called IHostThreadpoolManager. The CLR asks the host if it wants to provide a custom thread pool implementation by calling the host's implementation of IHostControl::GetHostManager, passing the IID for IHostThreadpoolManager. If an implementation of IHostThreadpoolManager is provided, the CLR directs all requests to queue items to a thread pool to the host. In addition to the method used to queue work items to the host's thread pool, IHostThreadpoolManager also contains methods that enable the CLR to set and query various values related to the size of the thread pool, as shown in Table 14-5.

Table 14-5. The Methods On IHostThreadpoolManager

Method

Description

QueueUserWorkItem

Queues a work item to the thread pool. QueueUserWorkItem has the same signature as the Win32 API with the same name.

SetMaxThreads

Sets the maximum number of threads that can be created in the thread pool. Hosts are not required to honor the CLR's request to set the size of the thread pool. If a host wishes to keep the size of the thread pool under its own control, it should return the E_NOTIMPL HRESULT from SetMaxThreads.

GetMaxThreads

Returns the maximum number of threads that will be created in the thread pool.

GetAvailableThreads

Returns the number of threads that are currently available to service requests.

SetMinThreads

Sets the minimum number of threads that will be created in the thread pool. As with SetMaxThreads, hosts are not required to honor the CLR's request to alter any aspect of the thread pool's size.

GetMinThreads

Returns the minimum number of threads that will be created in the thread pool.




    Customizing the Microsoft  .NET Framework Common Language Runtime
    Customizing the Microsoft .NET Framework Common Language Runtime
    ISBN: 735619883
    EAN: N/A
    Year: 2005
    Pages: 119

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net