Click or drag to resize

Multithreading

DME Component Libraries is designed for multithreading. Many of the operations it performs, such as propagating and computing Access, are automatically performed in parallel using multiple threads. This enables the library to take full advantage of modern multi-core processors.

Note Note

Insight3D has a different threading policy than the rest of DME Component Libraries. See the Insight3D section below.

Threading in DME Component Libraries

You can control how DME Component Libraries uses threads by changing the ThreadingPolicy. The threading policy is configuration information, associated with each thread, that controls how parallelizable operations started by that thread make use of threads. By default, the policy specifies that parallelizable operations create one thread per logical processor on the system. For example, if your system has four cores, and each core is HyperThreaded (meaning that it supports two hardware threads), DME Component Libraries will automatically create and utilize eight threads to compute Access, Coverage, etc.

You can specify the number of threads to use by setting the NumberOfThreads property. For example, the following code specifies that parallelizable operations created by the current thread should use exactly five threads to perform their work.

C#
ThreadingPolicy.NumberOfThreads = 5;

You can also disable multithreading entirely by setting NumberOfThreads to one, or by calling the ConfigureToUseCurrentThreadOnly method. When the threading policy is configured to use only one thread, parallelizable operations are executed using the calling thread and no additional threads are created.

C#
ThreadingPolicy.ConfigureToUseCurrentThreadOnly();

New threads are either recruited from the thread pool or are created explicitly for use by the parallelizable operation, depending on the value of the ThreadSource property. By default, threads are recruited from the thread pool. The following code changes the property so that new threads are created instead:

C#
ThreadingPolicy.ThreadSource = ThreadSource.NewThread;

Recruiting threads from the thread pool is usually much faster than creating them directly. However, it is sometimes useful to avoid using the thread pool so that the pool is not monopolized by a long-running operation.

Thread Safety

This kind of library-wide multithreading requires careful attention to thread safety throughout the library. It is important to be aware when a type is safe for use by multiple threads simultaneously and when it is not.

In general, all types in DME Component Libraries (with the major exception of Insight3D - see below) are thread-safe for read-only access. In other words, it is safe to call methods and access properties on instances simultaneously from multiple threads as long as the methods and properties do not modify the state of the object. However, if one thread modifies the object while another thread is reading properties or executing methods, the second thread may experience an inconsistent state of the object or the object may throw exceptions.

In addition, static methods and properties are safe to use from multiple threads simultaneously. Also, it is safe to use two different instances of the same type from multiple threads simultaneously. And a single instance can be freely used from any thread (or even multiple threads) as long as you ensure that only one thread at a time is accessing it. Exceptions to any of these rules, in the rare cases where they occur, will be clearly stated in the reference documentation.

Some types in DME Component Libraries, most notably Evaluators, implement the IThreadAware interface. Types that implement this interface might not even be thread-safe for read-only access as described above. If the IsThreadSafe property on this interface returns true, the instance can be assumed to be just as thread-safe as most objects in DME Component Libraries; you can use the instance from multiple threads simultaneously as long as no thread is modifying the instance. If it returns false, however, the instance must not be used from multiple threads simultaneously under any circumstances.

Fortunately, all types that implement IThreadAware also implement ICloneWithContext, which means that you can use CopyForAnotherThread to make a copy of the instance. Then, the original and any copies can be used from multiple threads simultaneously. Furthermore, it is safe to copy the instance for another thread from multiple threads simultaneously, as long as no thread is modifying the instance at the same time.

The following code creates a PointEvaluator (which implements IThreadAware) to evaluate the location of a Point and then prepares to pass it to a new thread:

C#
PointEvaluator evaluator = point.GetEvaluator();
PointEvaluator evaluatorForNewThread = CopyForAnotherThread.Copy(evaluator);

CopyForAnotherThread can safely be used on any type that implements IThreadAware, whether or not it reports itself as thread-safe. When CopyForAnotherThreadCopyT is invoked on a type that is thread-safe, it returns the existing instance without making another copy of it.

Subdividing Calculations for Multiple Threads

When calculations (such as access) divide a problem up across multiple threads, the built-in approach is generally to divide up the time intervals over which to calculate, and then for each thread to run the calculation for a subset of the overall time. This heuristic is tuned for typical situations where a single calculation is being run for a long time period.

In other situations, this automatic multithreading approach may not provide much parallelism. For example, if you are instead performing many independent calculations for a very short time period (or a single time), then partitioning the problem differently can produce significantly better performance.

It is for these reasons that the Coverage system provides the choice of whether to partition grid points spatially or temporally, to improve performance depending on the specific details of the coverage definition. For other types of problems, you can use the same low-level infrastructure to partition the problem in the way that works best for your situation.

ThreadedCalculation provides static methods to manually partition work across worker threads. These methods are conceptually similar to a for loop, except the body of the loop can be executed in parallel for subsections of the overall range. When using this low-level construct, your code can choose what the index of the loop should mean, allowing you complete control over how the problem is partitioned. The ThreadedCalculation ensures that the parameter passed to the body callback is copied for another thread, but any other data used by the callback should be copied if it is used from multiple threads simultaneously.

In the above situation where we want to perform many independent calculations, a straightforward way to divide up the work would be to create evaluators for each calculation, storing each evaluator in an array. Then, from the body callback, the index would indicate which evaluator in the array to evaluate. The background calculation will automatically configure the ThreadingPolicy to avoid any further multithreading from within the worker thread.

The following example shows a situation where we want to compute many independent access calculations at a single time:

C#
// Assume satellites is a list of many Platforms defining a constellation
// of satellites, and assume facility is a Platform defining a ground station.

// We want to determine the set of satellites that have access to the
// ground station at a single moment in time.

CentralBody earth = CentralBodiesFacet.GetFromContext().Earth;
EvaluatorGroup group = new EvaluatorGroup();

// Create and populate an array of access evaluators.
AccessEvaluator[] evaluators = new AccessEvaluator[satellites.Count];
for (int i = 0; i < satellites.Count; i++)
{
    Platform satellite = satellites[i];

    // Construct the access query between each satellite and the facility.
    LinkSpeedOfLight link = new LinkSpeedOfLight(satellite, facility, earth.InertialFrame);
    AccessQuery query = new CentralBodyObstructionConstraint(link, earth);

    // Get an access evaluator and store in the array.
    evaluators[i] = query.GetEvaluator(satellite, group);
}

// This will be the single time that we compute all access at.
JulianDate date = new GregorianDate(2019, 9, 25).ToJulianDate();

// Create an array to store the results in. No two threads will
// read or write the same element of the array.
AccessClassification[] results = new AccessClassification[evaluators.Length];

// The parameter can be any state that we want to make available to all worker threads.
// For this simple example, we have no data that all worker threads need,
// so we pass null, and ignore the value in the callback.
object parameter = null;
ThreadedCalculation.For(0, evaluators.Length, parameter, null, (p, index) =>
{
    // This callback runs on the worker thread.

    // CopyForAnotherThread only makes a copy of the evaluator if it is
    // not thread-safe for read access from multiple threads.
    AccessEvaluator evaluator = CopyForAnotherThread.Copy(evaluators[index]);

    // We compute access for the single time and store our results in the
    // array, using the same index, so the results can be correlated later.
    AccessClassification result = evaluator.Evaluate(date);
    results[index] = result;
});

// After the ThreadedCalculation finishes, results will be populated with the
// results of computing access.
Insight3D

Insight3D currently does not support multithreading. All interaction with Insight3D types must be done from a single thread. It is NOT guaranteed to be safe to use Insight3D types from multiple threads even if the user ensures that only one thread at a time is interacting with Insight3D.