CLR Threading Overview (i)

Source: Internet
Author: User

Hosting vs. native threading

Managed code executes on a "managed thread" (managed thread) that differs from the native thread that the operating system provides. A native thread is a sequence of native code that executes on a physical machine, while a managed thread is a virtual thread that executes on a CLR virtual machine.

Just as the JIT interpreter maps the "virtual" intermediate (IL) directives to the OST instructions on the physical machine, the CLR threading infrastructure maps the "virtual" managed threads to the native threads of the operating system.

At any one time, a managed thread may or may not be assigned to a native thread to execute. For example, a managed thread that has been created (via "New System.Threading.Thread") but not started (via "System.Threading.Thread.Start") will not be assigned to the native thread to execute. Similarly, although the CLR does not actually do this, a managed thread can be switched to execute on multiple native threads when it executes.

The thread interface exposed in managed code is used to hide the details of its underlying native thread:

    • Managed threads do not need to be bound to a native thread (or even on a native thread at all).
    • The native threads of different operating systems are not the same.
    • In principle, managed threads are "virtual".

The CLR provides and implements an abstraction of managed threads. For example, although it does not expose the thread local storage (TLS) mechanism of the operating system, it provides a managed "thread static" variable. Similarly, although it does not provide the "thread id" of the native thread, it provides a "managed thread ID" unrelated to the operating system. However, in order to diagnose the problem, some details of the underlying native thread can be obtained through the types in the System.Diagnostics namespace.

Managed threads also provide functionality that is typically not used by native threads. First, managed threads use GC references on the stack so that the CLR must be able to enumerate (and possibly modify) These GC references at GC time. For this purpose, the CLR must "pause" each managed thread (that is, stop execution so that all GC references can be discovered). Second, when the AppDomain unloads, the CLR must ensure that no threads are executing code in the AppDomain. This requires the CLR to force threads out of the AppDomain and the CLR to inject ThreadAbortException through the thread to achieve this.

Data

Each managed thread is associated with a thread object, which is defined in Threads.h. This object keeps track of everything the CLR needs to know about managed objects. Includes such essentials as the current GC mode of the thread and the stack frame chain, and also includes many elements created for performance reasons (such as some quick Arena-style allocators).

All the thread objects are saved in ThreadStore (also defined in threads.h), when a list of all known threads is in. To traverse all managed threads, you need to get threadstorelock first, and then use Threadstore::getallthreadlist to enumerate all the thread objects. This list also contains managed threads that are not assigned a native thread (such as a thread that is not started, or a native thread already exists).

The native thread can obtain a managed thread bound to the native thread through a native thread local storage (TLS) slot. This allows code running on the native thread to get the corresponding thread object through GetThread ().

In addition, many managed threads have a managed thread Object (System.Threading.Thread) that differs from the native thread object. Managed thread objects provide a way for managed code to interact with threads, most of which are encapsulated by the functionality of the native thread object. The current managed thread object can be obtained by Thread.CurrentThread (in managed code).

In the debugger, "! Threads "This SOS extension command can be used to enumerate all the thread objects in the ThreadStore.

The life cycle of a thread

A managed thread is created in the following scenarios:

    1. Managed code explicitly requires the CLR to create a new thread through System.Threading.Thread.
    2. Managed threads created by the CLR itself (see the "Special Threads" section).
    3. The native code calls managed code on the native thread, and this managed code is not associated with the managed line threads (through "reverse p/invoke" or COM interaction).
    4. A managed process is started (its main function is called on the main thread of the process).

In these scenarios, the CLR is responsible for creating native threads that support managed threads. This will only happen if the thread is actually started . In these cases, the CLR is "responsible" for the native thread; The CLR is responsible for the life cycle of the native thread, and because the CLR creates it, it knows the thread exists.

In the case of # # and # #, the native thread existed before the managed thread and was responsible for code outside the CLR. The CLR is not responsible for the life cycle of this native thread. The CLR is only aware of its existence when it first invokes managed code.

When a native thread ends, the CLR gets notified through its DllMain function. This happens in the "load lock" of the operating system, so you can only do very little (secure) things when handling this notification. Instead of destroying the data structure associated with the managed thread, the thread is simply identified as a "dead" state and the finalizer thread is started. The finalizer thread traverses all the threads in the threadstore that are dead and the managed code is no longer in use.

Time out

The CLR must be able to find all references to managed objects to perform GC. Managed code has been constantly accessing the GC heap, the Operation Stack, and the reference on the register. The CLR must ensure that all threads are parked in a safe and secure location (so that they do not modify the GC heap) in order to find all managed objects. It will only stop at the Security Point , at which time all available references can be checked on registers and stacks.

Another approach is that the GC heap, each thread's stack and register state are called "shared states" and can be accessed by multiple threads. As with most shared states, some "locks" are required to protect them. Managed code must acquire a lock before accessing the heap, and release the lock when it is safe.

The CLR calls this "lock" the "GC mode" of the thread. When a thread acquires a lock, it is in "cooperative mode", which must be "co-cooperative" with the GC (by releasing the lock) to allow garbage collection. When a thread does not acquire a lock, it is in "priority mode"-The GC can "take precedence" for garbage collection because it knows that the thread does not have access to the GC heap (preemptive).

GC can only be garbage collected if all threads are in "priority" mode (that is, no lock is acquired). The process of moving all threads to priority mode is referred to as "GC hover (GC suspension)" or "Pause execution engine".

An immature scheme to implement a "lock" is to require each managed thread to actually acquire and release the locks that protect it when it accesses the GC heap. The GC then attempts to acquire the lock for each thread, and once it acquires the lock on all threads, it can be safely garbage collected.

However, the above scenario is inadequate for two reasons. First, this would require managed code to spend a lot of time acquiring and releasing locks (or at least checking whether the GC is trying to acquire a lock-that is, "GC polling GC poll-that is, polling the GC continuously"). Second, it requires the JIT interpreter to generate a large number of "GC information codes" that describe the layout and register state of the stack after each line of JIT-generated code, which consumes a lot of memory.

Our improved approach to these approaches is to divide the JIT managed code area into "partially interruptible" and "All Interruptible" code. In partially interruptible code, the place where other functions are called is the only safe point, and the JIT generates an explicit "GC polling" point to check for a waiting GC. (JIT) only need to generate GC information in these places. In all interruptible code, each instruction is a safe point and JIT generates GC information for each instruction-but it does not generate a "GC" polling code. All interruptible code goes to the "break" state by hijacking the thread, which is explained later in this procedure. JIT is based on code quality, the size of GC information, and the time delay of GC hover to determine whether to produce all or part of the interruptible code.

Based on the above information, three basic operations are defined: Enter the cooperative mode, leave the cooperative mode, and pause the execution engine.

Enter the cooperative mode

A thread enters the cooperative mode by calling thread::D ISABLEPREEMPTIVEGC. It acquires a "lock" for the current thread:

    1. If a GC is executing (the GC owns the lock), wait for the GC to complete.
    2. Identifies that the thread will enter the cooperative mode and the GC cannot be triggered until the thread enters "priority mode".

The two steps are actually atomic operations.

Enter priority mode

A thread enters the priority mode (release lock) by calling THREAD::ENABLEPREEMPTIVEGC. It is done by identifying that the thread is no longer in cooperative mode and notifies the GC thread that it can start execution.

Interrupt execution engine

When the GC starts running, the first step is to break the execution engine. The Gcheap::suspendee function is used to do this:

    1. Set a global variable (g_ftrapreturningthreads) to flag that the GC is executing, and any thread that wants to enter cooperative mode will be blocked until the GC has finished running.
    2. Find all the threads in the cooperative mode, try to hijack the thread and force it to leave the cooperative mode for each such thread.
    3. Repeat the previous steps until no threads are in cooperative mode.
Hijacked

The hijacking operation for GC hover is done through the THREAD::SYSSUSPENDFORGC function. This function leaves the cooperative mode at "safe point" by forcing all managed threads running in cooperative mode. It does this by enumerating all managed threads (by traversing ThreadStore) for each managed thread running in cooperative mode:

    1. The underlying native thread is paused by Win32 's SuspendThread API. This API forces threads to stop from running in any location (not necessarily a security point).
    2. Gets the context of the thread through GetThreadContext. This is the concept of an operating system; The context holds the current register state of the thread. This allows us to monitor its instruction register and to know what type of instruction is running.
    3. Check again if the thread is in co-operation mode, because it may have left the cooperative mode before it was paused. If this is the case, then the thread is in a dangerous lot: the thread may be running arbitrary native code and must immediately resume execution to circumvent the deadlock.
    4. Check if the thread is running managed code. It is possible to run the native code of the virtual machine (VM) itself in cooperative mode (see the synchronization section below), and it will need to resume execution as soon as the previous step.
    5. Then the thread is currently paused on managed code. Depending on whether the code is full or partially interruptible, take one of the following steps:
      • If all is interruptible, then the GC is safe in any position, because the thread is defined in all its interruptible terms at the security point. It is theoretically possible for threads to be parked in this position (because it is safe), but several historical operating system bugs hinder this because the thread context previously fetched might be corrupted. The (CLR) then overwrites the thread's instruction register, and the boot thread jumps to a block of code to get a more complete context, leaving the cooperative mode, waiting for the GC to complete, re-entering the cooperative mode, and restoring the thread's register.
      • If it is partially interruptible, then the thread is not defined by a security point. However, its callers are in a safe spot (switching between functions). Based on this knowledge, the CLR "hijacked" the return address (that is, modifying the stack) on the stack frame, and the boot thread jumps to a block of code that is similar to "all interruptible." When the function returns, it does not return the original calling function there, but rather this block of code (this function may also be JIT-injected before the GC poll, causing the thread to leave the cooperative mode and revoke the hijacking operation).
Threadabort/appdomain-unload

In order to unload an application domain (AppDomain), the CLR needs to ensure that no threads are running in this application domain. To achieve this, all managed threads are enumerated, and any thread on the stack that has frames belonging to the unloaded application domain is "interrupted." A ThreadAbortException exception is injected into the running thread and causes the thread to expand (running the removal code) until there is no stack frame running in the application domain. The ThreadAbortException is also converted into a appdomainunloaded anomaly.

ThreadAbortException is a very special anomaly. It may be captured by user code, but the CLR ensures that it is thrown again after the user's exception-handling code. So ThreadAbortException is sometimes called "unable to be captured", although not strictly.

ThreadAbortException is usually thrown by setting a flag bit on a managed thread that flags its "terminating". The CLR checks this flag bit in many places (especially, every time it returns from P/invoke) and often has the purpose of setting this flag bit in order for the thread to terminate in a timely manner.

However, for example, if a thread is running a long managed loop, it may not check this flag bit at all. In order for such a thread to terminate quickly, the thread is "hijacked" and the ThreadAbortException exception is forced to be thrown. The hijacking process is similar to a GC hover, except that the thread jumps past the block of code and throws the ThreadAbortException instead of waiting for the GC to finish running.

This hijacking means that the ThreadAbortException may occur at any location. This makes it difficult for managed code to properly handle ThreadAbortException exceptions. So in addition to using this mechanism when uninstalling an application domain-ensuring that the state corrupted by ThreadAbort is cleaned up with the application domain, it is not a wise choice to use it anywhere else.

CLR Threading Overview (i)

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.