Многопоточное приложение
Summary
Whether you are developing for computers with one processor or several, you want your application to provide the most responsive interaction with the user, even if the application is currently doing other work. Using multiple threads of execution is one of the most powerful ways to keep your application responsive to the user and at the same time make use of the processor in between or even during user events.
Использование многопоточности
Operating systems use processes to separate the different applications that they are executing. Threads are the basic unit to which an operating system allocates processor time, and more than one thread can be executing code inside that process. Each thread maintains exception handlers, a scheduling priority, and a set of structures the system uses to save the thread context until it is scheduled. The thread context includes all the information the thread needs to seamlessly resume execution, including the thread's set of CPU registers and stack, in the address space of the thread's host process.
Преимущества
Without modification, the same application would dramatically increase user satisfaction when run on a computer with more than one processor. Your single application domain could use multiple threads to accomplish the following tasks:
- Communicate over a network, to a Web server, and to a database.
- Perform operations that take a large amount of time.
- Distinguish tasks of varying priority. For example, a high-priority thread manages time-critical tasks, and a low-priority thread performs other tasks.
- Allow the user interface to remain responsive, while allocating time to background tasks.
Недостатки
It is recommended that you use
as few threads as possible, thereby minimizing the use of operating-system resources and improving performance. Threading also has resource requirements and potential conflicts to be considered when designing your application. The resource requirements are as follows:
- The system consumes memory for the context information required by processes, AppDomain? objects, and threads. Therefore, the number of processes, AppDomain? objects, and threads that can be created is limited by available memory.
- Keeping track of a large number of threads consumes significant processor time. If there are too many threads, most of them will not make significant progress. If most of the current threads are in one process, threads in other processes are scheduled less frequently.
- Controlling code execution with many threads is complex, and can be a source of many bugs.
- Destroying threads requires knowing what could happen and handling those issues.
Providing shared access to resources can create conflicts. To avoid conflicts, you must synchronize, or control the access to, shared resources.
Resources that require synchronization include:
- System resources (such as communications ports).
- Resources shared by multiple processes (such as file handles).
- The resources of a single application domain (such as global, static, and instance fields) accessed by multiple threads.
Использование нитей
In general, using the
ThreadPool class is the easiest way to handle multiple threads for relatively short tasks that will not block other threads and when you do not expect any particular scheduling of the tasks. However, there are a number of reasons to create your own threads:
- If you need a task to have a particular priority.
- If you have a task that might run a long time (and therefore block other tasks).
- If you need to place threads into a single-threaded apartment (all ThreadPool threads are in the multithreaded apartment).
- If you need a stable identity associated with the thread. For example, you should use a dedicated thread to abort that thread, suspend it, or discover it by name.
Создание дополнительного потока
Creating a new instance of a Thread object creates new managed threads. As its only parameter, the constructor for
Thread
takes a
ThreadStart
delegate that wraps the method that will be invoked by the new
Thread
when you call
Thread.Start
. Calling
Thread.Start
more than once will cause a
ThreadStateException
to be thrown.
Thread.Start
submits an asynchronous request to the system, and the call returns immediately, possibly before the new thread has actually started. You can use
Thread.ThreadState
and
Thread.IsAlive
to determine the state of the thread at any one moment.
Thread.Abort
aborts a thread, marking it for garbage collection. The following code example creates two new threads to call instance and static methods on another object.
using System;
using System.Threading;
public class ServerClass{
// The method that will be called when the thread is started.
public void InstanceMethod(){
Console.WriteLine("ServerClass.InstanceMethod is running on another thread.");
// Pause for a moment to provide a delay to make threads more apparent.
Thread.Sleep(3000);
Console.WriteLine("The instance method called by the worker thread has ended.");
}
public static void StaticMethod(){
Console.WriteLine("ServerClass.StaticMethod is running on another thread.");
// Pause for a moment to provide a delay to make threads more apparent.
Thread.Sleep(5000);
Console.WriteLine("The static method called by the worker thread has ended.");
}
}
public class Simple{
public static int Main(String[] args){
Console.WriteLine("Thread Simple Sample");
ServerClass serverObject = new ServerClass();
// Create the thread object, passing in the
// serverObject.InstanceMethod method using a ThreadStart delegate.
Thread InstanceCaller = new Thread(new ThreadStart(serverObject.InstanceMethod));
// Start the thread.
InstanceCaller.Start();
Console.WriteLine("The Main() thread calls this after starting the new InstanceCaller thread.");
// Create the thread object, passing in the
// serverObject.StaticMethod method using a ThreadStart delegate.
Thread StaticCaller = new Thread(new ThreadStart(ServerClass.StaticMethod));
// Start the thread.
StaticCaller.Start();
Console.WriteLine("The Main() thread calls this after starting the new StaticCaller threads.");
return 0;
}
}
The
ThreadStart
delegate has no parameters or return value. This means that you cannot start a thread using a method that takes parameters, or obtain a return value from the method.
- To pass data to a thread, create an object to hold the data and the thread method, as shown in the two code examples that follow.
- To retrieve the results of a thread method, you can use a callback method
Примеры можно найти в
MSDN
Остановка/продолжение исполнения потоков
-
Thread.Sleep
causes the current thread to immediately block for the number of milliseconds you pass to Sleep
, yielding the remainder of its time slice to another thread. Calling Thread.Sleep(Timeout.Infinite)
causes a thread to sleep until it is interrupted or aborted by another thread (Thread.Interrupt
, Thread.Abort
) (NB One thread cannot call Sleep
on another thread )
- When a thread calls
Thread.Suspend
on itself, the call blocks until the thread is resumed by another thread. When one thread calls Thread.Suspend
on another thread, the call is a nonblocking call that causes the other thread to pause.
- Calling
Thread.Resume
breaks another thread out of the suspend state and causes the thread to resume execution, regardless of how many times Thread.Suspend
was called. (NB Unlike Thread.Sleep
, Thread.Suspend
does not cause a thread to immediately stop execution. The common language runtime must wait until the thread has reached a safe point).
See also:
Thread.Suspend, Garbage Collection, and Safe Points
Удаление потока
The
Thread.Abort
method is used to stop a logical thread permanently. When you call
Abort
, the common language runtime throws a
ThreadAbortException
, which the thread can catch.
Because
Thread.Abort
does not cause the thread to abort immediately, you must call
Thread.Join
to wait on the thread if you need to be sure the thread is stopped. Join is a blocking call that does not return until the thread has actually stopped executing. Once a thread is aborted, it cannot be restarted.
You can also call
Thread.Join
and pass a time-out period. If the thread dies before the time out has elapsed, the call returns
true. Otherwise, if the time expires before the thread dies, the call returns
false. Threads that are waiting on a call to
Thread.Join
can be interrupted by other threads that call
Thread.Interrupt
.
See also:
Thread Destroying at MSDN
Управление потоками
Thread.IsAlive
read-only propery
true if this thread has been started and has not terminated normally or aborted; otherwise,
false.
Thread.Join
ожидает завершения выполнения нити до ее полной остановки или в течение времени, переданного в качестве параметра.
Background threads are identical to foreground threads with one exception: a background thread will not keep the managed execution environment alive. Once all foreground threads have been stopped in a managed process (where the .exe file is a managed assembly), the system stops all background threads and shuts down. A thread can be designated as a background or a foreground thread by setting the
Thread.IsBackground
property.
Every thread has a thread priority assigned to it. Threads created within the common language runtime are initially assigned the priority of
ThreadPriority.Normal
. Threads created outside the runtime retain the priority they had before they entered the managed environment. You can get or set the priority of any thread with the
Thread.Priority
property.
Состояния потоков
The property
Thread.ThreadState
provides a bit mask that indicates the thread's current state. A thread is always in at least one of the possible states in the
ThreadState? enumeration, and can be in multiple states at the same time.
When you create a managed thread it is in the
Unstarted
state. The thread remains in the
Unstarted
state until it is moved into the started state by calling
Thread.Start
. Unmanaged threads that enter the managed environment are already in the started state. Once in the started state, there are a number of actions that can cause the thread to change states. The following table lists the actions that cause a change of state, along with the corresponding new state.
Action | Resulting new state |
Another thread calls Thread.Start. | Unchanged |
The thread responds to Thread.Start and starts running. | Running |
The thread calls Thread.Sleep . | WaitSleepJoin |
The thread calls Monitor.Wait on another object. | WaitSleepJoin |
The thread calls Thread.Join on another thread. | WaitSleepJoin |
Another thread calls Thread.Suspend . | SuspendRequested |
The thread responds to a Thread.Suspend request. | Suspended |
Another thread calls Thread.Resume . | Running |
Another thread calls Thread.Interrupt . | Running |
Another thread calls Thread.Abort . | AbortRequested |
The thread responds to a Thread.Abort . | Aborted |
Threads are often in more than one state at any given time. For example, if a thread is blocked from a
Wait
call and another thread calls
Abort
on that same thread, the thread will be in both the
WaitSleepJoin
and the
AbortRequested
state at the same time. In that case, as soon as the thread returns from the call to
Wait
or is interrupted, it will receive the
ThreadAbortException
.
Once a thread leaves the
Unstarted
state as the result of a call to
Thread.Start
, it can never return to the
Unstarted
state. A thread can never leave the
Stopped state
, either.
You can use thread pooling to make much more efficient use of multiple threads, depending on your application. Many applications use multiple threads, but often those threads spend a great deal of time in the sleeping state waiting for an event to occur.
See also:
ThreadPool
Лучшие практики
http://msdn.microsoft.com/library/en-us/cpguide/html/cpconmanagedthreadingbestpractices.asp?frame=true
Dead Lock
Race Conditions
Recomendations
Timer
http://msdn.microsoft.com/library/en-us/cpguide/html/cpcontimer.asp?frame=true
Синхронизация нескольких потоков
Monitor/lock
Monitor objects expose the ability to synchronize access to a region of code by taking and releasing a lock on a particular object using the Monitor.Enter, Monitor.TryEnter, and Monitor.Exit methods. Once you have a lock on a code region, you can use the Monitor.Wait, Monitor.Pulse, and Monitor.PulseAll methods. Wait releases the lock if it is held and waits to be notified. When Wait is notified, it returns and obtains the lock again. Both Pulse and
PulseAll? signal for the next thread in the wait queue to proceed.
Monitor locks objects (that is, reference types), not value types. While you can pass a value type to Enter and Exit, it is boxed separately for each call. Since each call creates a separate object, Enter never blocks, and the code it is supposedly protecting is not really synchronized.
using System;
using System.Threading;
public class ThreadingExample : Object {
public static void Main() {
Thread[] threads = new Thread[2];
for( int count=0;count<2; count++) {
threads[count] = new Thread( new ThreadStart( Count ) );
threads[count].Start();
}
}
// public static Object synchronizeVariable = "locking variable";
public static void Count() {
lock( typeof(ThreadingExample) ) {
for( int count=1;count<=5;count++ ) {
Console.WriteLine( count + " " );
Monitor.PulseAll( typeof(ThreadingExample) );
if( count < 5 ) Monitor.Wait( typeof(ThreadingExample) );
}
}
}
}
WaitHandle?
http://msdn.microsoft.com/library/en-us/cpguide/html/cpconwaithandle.asp?frame=true
Mutex
http://msdn.microsoft.com/library/en-us/cpguide/html/cpconmutex.asp?frame=true
interlocked
The
Interlocked
methods
CompareExchange
,
Decrement
,
Exchange
, and
Increment
provide a simple mechanism for synchronizing access to a variable that is shared by multiple threads. The threads of different processes can use this mechanism if the variable is in shared memory.
The
Increment
and
Decrement
functions combine the operations of incrementing or decrementing the variable and checking the resulting value. This atomic operation is useful in a multitasking operating system, in which the system can interrupt one thread's execution to grant a slice of processor time to another thread.
The
Exchange
function atomically exchanges the values of the specified variables. The
CompareExchange
function combines two operations: comparing two values and storing a third value in one of the variables, based on the outcome of the comparison.
To update an object type variable only if it is null. You can use the following code to update the variable and make the code thread safe.
if (x == null)
{
lock (this)
{
if (x == null)
{
x = y;
}
}
}
You can improve the performance of the previous sample by replacing it with the following code.
System.Threading.Interlocked.CompareExchange(ref x, y, null);
ReaderWriterLock?
http://msdn.microsoft.com/library/en-us/cpguide/html/cpconreaderwriterlock.asp?frame=true
Ссылки
--
AndreyUstyuzhanin - 06 Apr 2004