In .NET, parallel program execution is possible
using multiple threads. A thread is an independent code path that can
run parallel to other threads. With threads, you can process several
tasks simultaneously.
Threads
are more lightweight than processes; that means the system can switch
between threads faster. In the .NET Micro Framework, only one
application (process) can run at a time, so for parallel program
execution with the .NET Micro Framework, only threads are applicable.
A
.NET application starts as an individual thread, the main thread. The
main thread is created automatically by the runtime environment. An
application can consist of several threads, and the different threads
can access common data. Therefore, you need to synchronize access to
shared resources from different threads.
Threads
run on single-processor systems, and like all platforms for the .NET
Micro Framework, are not actually parallel, but the runtime environment
alternates among threads to provide each thread processor time. You can
control the assignment of computing time more precisely by setting the
thread priority of each thread. Thus important tasks can be processed
faster than, for example, some monitoring in the background. The thread
priority indicates how much processor time a thread gets compared to
other threads.
1. Using Threads
Threads
are programmed in the .NET Micro Framework in a way that's similar to
the full .NET Framework. Of course, the functionality has been reduced
to a minimum. Nevertheless, the most necessary methods and properties
are available. A thread is represented by the System.Threading.Thread class, as shown in Listing 9-1.
Example 1. The System.Threading.Thread Class
using System;
namespace System.Threading { public sealed class Thread { public Thread(ThreadStart start);
public static Thread CurrentThread { get; } public bool IsAlive { get; } public ThreadPriority Priority { get; set; } public ThreadState ThreadState { get; }
public void Abort(); public static AppDomain GetDomain(); public void Join(); public bool Join(int millisecondsTimeout); public bool Join(TimeSpan timeout); public void Resume(); public static void Sleep(int millisecondsTimeout); public void Start(); public void Suspend(); } }
|
NOTE
The static Sleep method of the Thread
class has already been used in several sample programs. You should
consider that the thread from which the method is called is always
paused. That means calling the Sleep
method from the main thread will pause the main thread, and calling the
method from code that is executed from a background thread will pause
the background thread.
Now you will learn how to create and start a thread. Listing 9-2
shows how this is done. You need to pass a pointer to a parameterless
method containing the task you want to process with the constructor.
That method is called a callback, or thread, method and can be static
or belong to an object instance. Afterward, you need to start the
thread with its Start method. The thread is terminated as soon as the thread worker method is done.
Example 2. Creating and Starting a Thread
public class Program { public static void Main() { Thread myThread = new Thread(MyThreadMethod); myThread.Start(); //main thread continues here ... }
private static void MyThreadMethod() { //do some work here } }
|
It
is possible to wait for completion or termination of another thread
before moving on with the application, which is done by calling the Join method of the thread you want to wait for. There are three overloads of this method (see Listing 9-1):
the parameterless version blocks and waits infinitely for the
termination. The other two variants expect a timeout parameter
indicating the maximum waiting time. The methods return true if the thread was terminated in the specified time and false if the thread is still alive.
2. Synchronization
Conflicts can occur any time several threads access shared resources, so you need to synchronize access to shared data.
The
interrupt method of an interrupt port and the timer method of a timer
are each called in the context of a separate thread. Access to
resources within these methods has to be synchronized as well.
|
|
The
.NET Framework provides several possibilities for synchronization. A
simple way to prevent a method from being executed by several threads
at the same time is to mark the method as synchronized by adding the
following attribute:
[MethodImpl(MethodImplOptions.Synchronized)]
private void MySynchronizedMethod()
{
//...
}
The
synchronized method will be locked for other threads as soon as it is
entered by a thread. Other threads trying to execute this method are
blocked until the locking thread returns from the method.
In
the preceding example, the method will be locked during the entire time
a thread is executing it, even if critical resources are accessed only
in a small part of the entire method. A better approach is to lock a
method only during the execution of critical parts. That type of access
to objects can be synchronized with the MonitorSystem.Threading namespace: class from the
private static MyThreadMethod1()
{
...
Monitor.Enter(myCriticalObject);
//access shared data here
Monitor.Exit(myCriticalObject);
...
}
private static MyThreadMethod2()
{
...
Monitor.Enter(myCriticalObject);
//access shared data here
Monitor.Exit(myCriticalObject);
...
}
Both thread methods are executed at the same time, and both access the same data at the same time. After the Enter method is called, other threads reaching EnterExit method is called by the locking thread. You should place the Exit call within the finally
block of an exception handler, so the lock is released in case of an
exception so that other threads will not be blocked forever. To
simplify this situation, you can use the C# keyword lock. will be blocked until the
lock(myCriticalObject)
{
//access data
}
The preceding construct is compiled by the C# compiler to the following code:
try
{
Monitor.Enter(myCriticalObject);
//access data
}
catch(Exception)
{
Monitor.Exit(myCriticalObject);
}
The Interlocked class from the System.Threading
namespace offers the possibility to increment or decrement by one an
integer variable or swap the value of two variables as an atomic
operation that cannot be interrupted. An atomic operation is always
executed completely and, therefore, will not be interrupted by another
thread during execution.
3. Events
In addition to using the Monitor class or the lock
construct to synchronize access to shared resources, you can use
events. Events allow you to signal other waiting threads that a certain
action, like the completion of a task, has occurred. You can also wait
for multiple events.
For synchronization with events, there are the two classes: ManualResetEvent and AutoResetEvent. These classes differ only in the fact that you must reset the status of a ManualResetEvent manually after signaling an event. Both classes are in the System.Threading namespace, and both inherit from the abstract WaitHandle class, which possesses the methods represented in Listing 9-3. The documentation of the .NET Micro Framework SDK is not correct and describes only two members of this class.
Example 3. The System.Threading.WaitHandle Base Class of ManualResetEvent and AutoResetEvent
namespace System.Threading { public abstract class WaitHandle : MarshalByRefObject { public const int WaitTimeout = 258;
protected WaitHandle();
public static bool WaitAll(WaitHandle[] waitHandles); public static bool WaitAll(WaitHandle[] waitHandles, int millisecondsTimeout, bool exitContext); public static int WaitAny(WaitHandle[] waitHandles); public static int WaitAny(WaitHandle[] waitHandles, int millisecondsTimeout, bool exitContext); public virtual bool WaitOne(); public virtual bool WaitOne(int millisecondsTimeout, bool exitContext); } }
|
The two event classes, ManualResetEvent and AutoResetEvent, possess the methods Set and Reset. When creating an instance of an event class, you can pass the initial state to the constructor. After an AutoResetEvent object is set, it remains signaled until another thread waits for this event (at which time, it resets automatically).
In Listing 4, you can see how to use the AutoResetEvent class. The main thread creates a new thread, starts it, and waits until the created thread signals this event.
Example 4. Using AutoResetEvent
using System.Threading;
using Microsoft.SPOT;
namespace ThreadingEventSample
{
public class Program
{
private static AutoResetEvent ev = new AutoResetEvent(false);
static void Main()
{
Thread thr = new Thread(WaitForEvent);
thr.Start();
Debug.Print("Waiting...");
ev.WaitOne(); //waiting for notification
Debug.Print("Notified");
}
private static void WaitForEvent()
{
Thread.Sleep(1000); //sleep to simulate doing something
ev.Set(); //wake up other thread
}
}
}