The article will focus on threading constructs and as such, is meant for both
the beginner and those who practice multithreading regularly. A thread is a unit
of execution. Microsoft has used threads since the Win32 and NT days, and an
understanding of this topic is necessary. For any .NET developer. In my limited
knowledge, I have assembled a "manual" that could act as a reference for things
like basic threading and using threading in the real world. This section, while
part of the article, is only meant to be a basic introduction to the concept of
threading. The one experienced in threading should overlook it. To create a
thread, you must follow these steps:
-
Create a method that takes no arguments and does not return any data.
- Create a new ThreadStart delegate and specify the method created in step 1.
- Create a Thread object specifying the ThreadStart object created in step 2.
- Call ThreadStart to begin execution of the new thread. The code will look
something like this:
using
System;
using
System.Threading;
public
class Program
{
public static
void Main()
{
ThreadStart operation =
new ThreadStart(SimpleWork);
Thread thr =
new Thread(operation);
thr.Start();
}
private static
void SimpleWork()
{
Console.WriteLine("Thread:
{0}",
Thread.CurrentThread.ManagedThreadId);
}
}
Here are some of the Thread class's properties:
IsAlive: Gets a value indicating that the current thread is currently
executing.
IsBackground: Gets or sets whether the thread runs as a background
thread.
IsThreadPoolId: Gets whether this thread is a thread in the thread pool.
ManagedThreadId: Gets a number to identify the current thread.
Name: Gets or sets a name associated with the thread.
Priority: Gets or sets the priority of the thread.
ThreadState: Gets the ThreadState value for the thread.
A more likely scenario than the example shown above is one in which you will
want to create multiple threads:
using
System;
using
System.Threading;
public
class Program
{
public static
void Main()
{
ThreadStart operation =
new ThreadStart(SimpleWork);
for (int
x = 1; x <= 5; ++x)
{
Thread thr =
new Thread(operation);
thr.Start();
}
}
private static
void SimpleWork()
{
Console.WriteLine("Thread:
{0}", Thread.CurrentThread.ManagedThreadId);
}
}
Output:
Thread: 3
Thread: 4
Thread: 5
Thread: 6
Thread: 7
Here are some of Thread's methods (not static!!):
Abort: Raises a ThreadAbort exception on the thread to indicate that the
thread should be aborted.
Interrupt: Raises a ThreadInterruptException when a thread is in blocked
state.
Join: Blocks the calling thread until the thread terminates.
Start: Sets a thread to be scheduled for execution.
Using Thread.Join is sometimes necessary because more often than not, you will
need your application to wait for a thread to complete execution.
To accomplish this, the Thread class supports the Join method, which is a static
method:
using
System;
using
System.Collections.Generic;
using
System.Diagnostics;
using System.IO;
using
System.Reflection;
using
System.Runtime;
using
System.Runtime.CompilerServices;
using
System.Security;
using
System.Text;
using
System.Threading;
class
InterruptAwareWorker
{
private bool
interruptRequested;
private Thread
myThread;
public void Interrupt()
{
if (myThread ==
null)
interruptRequested = true;
myThread.Interrupt();
myThread.Join();
}
private void CheckInterrupt()
{
if (interruptRequested)
throw new
ThreadInterruptedException();
}
public void DoWork(object
obj)
{
myThread = Thread.CurrentThread;
try
{
while (true)
{
// Do some work… (including some
blocking operations)
CheckInterrupt();
// Do some more work…
CheckInterrupt();
// And so forth…
}
}
catch (ThreadInterruptedException)
{
// Thread was interrupted; perform any
cleanup.
Console.WriteLine("Thread
was interrupted...");
return;
}
}
public static
void Main()
{
InterruptAwareWorker w =
new
InterruptAwareWorker();
Thread t =
new Thread(w.DoWork);
t.Start();
// Do some work…
// Uh-oh, we need to interrupt the
worker.
w.Interrupt();
t.Join();
}
}
Output:
Thread was interrupted...
This code runs as a console application in Visual Studio 2010,and therefore will
only run on .NET 4.0, yet on the command line. For the sake of understanding the
concept, here is an example of joining threads:
using
System;
using
System.Threading;
public
class Program
{
public static
void Main()
{
int threadCount = 5;
Thread[] threads =
new Thread[threadCount];
for (int i = 0;
i < threadCount; i++)
{
int idx = i;
threads[i] = new
Thread(delegate()
{ Console.WriteLine("Worker
{0}", idx); });
}
// Now begin execution of each thread using the
delegate keyword
Console.WriteLine("Beginning
thread execution...");
Array.ForEach(threads,
delegate(Thread
t) { t.Start(); });
// And lastly join on them (wait for completion):
Console.WriteLine("Waiting
for completion...");
Array.ForEach(threads,
delegate(Thread
t) { t.Join(); });
Console.WriteLine("All
threads complete");
}
}
The result is as expected. Note that when we deal
with multiple threads, we need to wait on all our threads. We can do this by
keeping reference to all of our threads and calling Join on each of the threads
to wait for the threads to complete:
Beginning thread execution...
Worker 0
Worker 1
Worker 2
Worker 3
Waiting for completion...
Worker 4
All threads complete.
Now in the earlier examples, we were using the ThreadStart delegate, which takes
no parameters. In practice, you will need to pass information to individual
threads. To do this, you need to use a new delegate called
ParamterizedThreadStart. This delegate specifies a method signature with a
single parameter of type Object and returns nothing. Here is an example. Notice
that we are passing data to a thread by using this delegate:
using
System;
using
System.Threading;
public
static class
Program
{
public static
void Main()
{
ParameterizedThreadStart operation =
new
ParameterizedThreadStart(WorkWithParameter);
Thread theThread =
new Thread(operation);
theThread.Start("hello");
// a second thread with (data) a different
parameter
Thread newThread =
new Thread(operation);
newThread.Start("goodbye");
}
private static
void WorkWithParameter(object
o)
{
string info = (string)o;
for (int
x = 0; x < 10; ++x)
{
Console.WriteLine("{0}:
{1}", info,
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(10);
}
}
}
Output:
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
goodbye: 4
hello: 3
goodbye: 4
hello: 3
goodbye: 4
hello: 3
hello: 3
goodbye: 4
goodbye: 4
hello: 3
Examine the created method WorkWithParameter(object o). This is a method that
takes a single Object parameter (and therefore can be a reference to any
object). To use this as the starting point of a thread call, you can create a
ParameterizedThreadStart delegate to point at this new method and use the
Thread.Start method's overload that takes a single object parameter.
Where this Leads To
The topic of threading can sometimes fog the beginner once he or she enters the
topics of synchronization, concurrency, parallelism, and the like. Let's takes
the lock statement. The C# lock statement is really just a shorthand notation
for working with the System.Threading.Monitor class type. Thus, if you were to
look under the hood to see what lock() actually resolves, you would find code
like the following:
using
System;
using
System.Threading;
public
class WhatIsAThread
{
private long
refCount = 0;
public void
AddRef()
{
Interlocked.Increment(ref
refCount);
}
public void Release()
{
if (Interlocked.Decrement(ref
refCount) == 0)
{
GC.Collect();
}
}
}
internal
class WorkerClass
{
public void
DoSomeWork()
{
lock(this)
{
for(int
i = 0; i < 5; i++)
{
Console.WriteLine("Worker
says: " + i + ", ");
}
}
// The C#
lock statmement is really...
Monitor.Enter(this);
try
{
//
Do the work.
For (int
i = 0; i < 5; i++)
{
Console.WriteLine("Worker
says: " + i +
", ");
}
}
finally
{
Monitor.Exit(this);
}
}
}
public
class MainClass
{
public static
int Main(string[] args)
{
// Make the worker object.
WorkerClass w =
new WorkerClass();
Thread workerThreadA =
new Thread(new
ThreadStart(w.DoSomeWork));
Thread workerThreadB =
new Thread(new
ThreadStart(w.DoSomeWork));
Thread workerThreadC =
new Thread(new
ThreadStart(w.DoSomeWork));
workerThreadA.Start();
workerThreadB.Start();
workerThreadC.Start();
return 0;
}
}
Output:
Worker says: 0,
Worker says: 1,
Worker says: 2,
Worker says: 3,
Worker says: 4,
Worker says: 0,
Worker says: 1,
Worker says: 2,
Worker says: 3,
Worker says: 4,
Worker says: 0,
Worker says: 1,
Worker says: 2,
Worker says: 3,
Worker says: 4,
Threads: A Deeper Look
This section of the article will continue to be a reference for threading, but
will also include the Operating System environment in which a thread can
execute. Of the kernel objects, we will then cover the event thread
synchronization object. Examining the environment can help us better understand
how to effectively achieve concurrency and multithreading. The topic of thread
creation will come later on in this article. We also want to know why threads
can cost: stated loosely, threads are expensive. Each thread is provided with:
Thread kernel object: The OS allocates and initializes one of these data
structures for each thread created in the system. The data structure contains a
bunch of properties (discussed later in this chapter) that describe the thread.
This data structure also contains what is called the thread's context. The
context is a block of memory that contains a set of the CPU's registers. When
Windows is running on a machine with an x86 CPU, the thread's context uses about
700 bytes of memory. For x64 and IA64 CPUs, the context is about 1,240 and 2,500
bytes of memory, respectively.
Thread environment block (TEB): The TEB is a block of memory allocated
and initialized in user mode (address space that application code can quickly
access). The TEB consumes 1 page of memory (4 KB on x86 and x64 CPUs, 8 KB on an
IA64 CPU). The TEB contains the head of the thread's exception-handling chain.
Each try block that the thread enters inserts a node in the head of this chain;
the node is removed from the chain when the thread exists the try block. In
addition, the TEB contains the thread's thread-local storage data as well as
some data structures for use by the Graphics Device Interface (GDI) and OpenGL
graphics.
User-mode stack: The user-mode stack is used for local variables and
arguments passed to methods. It also contains the address indicating what the
thread should execute next when the current method returns. By default, Windows
allocates 1 MB of memory for each thread's user-mode stack.
Kernel-mode stack: The kernel-mode stack is also used when the
application code passes arguments to a kernel-mode function in the Operating
System. For security reasons, Windows copies any arguments passed from user-mode
code to the kernel from the thread's user-mode stack to the thread's kernel-mode
stack. Once copied, the kernel can verify the argument values, and since the
application code can't access the kernel mode stack, the application can't
modify the argument values after they have been validated and the OS kernel code
begins to operate on them. In addition, the kernel calls methods within itself
and uses the kernel-mode stack to pass its own arguments, to store a function's
local variables, and to store return addresses. The kernel-mode stack is 12 KB
when running on a 32-bit Windows system, and 24 KB when running on a 64-bit
Windows system.
Using multiple threads for a single program can be done to run entirely
independent parts of the program at once. This is called concurrency, and is
frequently used in server-side applications. Using threads to break one big task
down into multiple pieces that can execute concurrently is called parallelism.
Conceptually speaking, a thread is unit of execution - an execution context that
represents in-progress work being performed by a program. Windows must allocate
a kernel object for each thread, along with a set of data structures. Each
thread is mapped onto a processor by the Windows thread scheduler, enabling the
in-progress work to actually execute. Each thread has an Instruction Pointer
that refers to the current executing instruction. "Execution" consists of the
processor fetching the next instruction, decoding it, and issuing it, one
instruction after the other, from the thread's code. During the execution of
some compiled code, program data will be routinely moved into and out of
registers from the attached main memory. While these registers physically reside
on the processor, some of the volatile state also belongs to the thread too. If
the thread must be paused, this state will be captured and saved in memory so it
can be later restored. Doing this enables the same IP fetch, decode, and issue
process to proceed for the thread as though it was never interrupted. The
process of saving or restoring this state from and to the hardware is called a
context switch.
Execution Context
As in Windows, each thread in .NET has data associated with it, and that data is
usually propagated to new threads. This data includes security information (the
IPrinciple and thread identity), the localization strings, and transaction
information from System.Transaction. By default, the execution context flows to
helper threads, but this is costly: a context switch is a heavy-weight
operation. To access the current execution context, the ExecutionContext class
supplies static methods to control the flow of context information. So in the
System.Threading namespace, there is an ExecutionContext class that allows you
to control how a thread's execution context flows from one thread to another.
Here is what the class looks like:
public
sealed class
ExecutionContext : IDisposable, ISerializable
{
[SecurityCritical]
public static
AsyncFlowControl SuppressFlow();
public static
void RestoreFlow();
public static
Boolean IsFlowSuppressed();
// Less commonly used methods are not shown
}
You can use this class to suppress the flowing of an execution context, thereby
improving your application's performance. The performance gains can be quite
substantial for a server application. There is not much performance benefit for
a client application, and the SuppressFlow method is marked with the [SecurityCritical]
attribute, making it impossible to call in some client applications (like
Silverlight). Of course, you should suppress the flowing of execution context
only if the helper thread does not need or access the context information. If
the initiating thread's execution context does not flow to a helper thread, the
helper thread will use whatever execution context is last associated with it.
Therefore, the helper thread really shouldn't execute any code that relies on
the execution context state (such as a user's Windows identity). The example
shown next is MSDN code, and compiles with warnings on .NET 4.0. It compiles and
executes normally on .NET 2.0.
using
System;
using
System.Threading;
using
System.Security;
using
System.Collections;
using
System.Security.Permissions;
using
System.Runtime.Serialization;
using
System.Runtime.Remoting.Messaging;
namespace
Contoso
{
class
ExecutionContextSample
{
static void
Main()
{
try
{
Console.WriteLine("Executing
Main in the primary thread.");
FileDialogPermission fdp =
new
FileDialogPermission(
FileDialogPermissionAccess.OpenSave);
fdp.Deny();
// Capture the execution context
containing the Deny.
ExecutionContext eC =
ExecutionContext.Capture();
// Suppress the flow of the execution context.
AsyncFlowControl aFC =
ExecutionContext.SuppressFlow();
Thread t1 =
new Thread(new
ThreadStart(DemandPermission));
t1.Start();
t1.Join();
Console.WriteLine("Is
the flow suppressed? " +
ExecutionContext.IsFlowSuppressed());
Console.WriteLine("Restore
the flow.");
aFC.Undo();
Console.WriteLine("Is
the flow suppressed? " +
ExecutionContext.IsFlowSuppressed());
Thread t2 =
new Thread(new
ThreadStart(DemandPermission));
t2.Start();
t2.Join();
// Remove the Deny.
CodeAccessPermission.RevertDeny();
// Capture the context that does not
contain the Deny.
ExecutionContext eC2 =
ExecutionContext.Capture();
// Show that the Deny is no longer
present.
Thread t3 =
new Thread(new
ThreadStart(DemandPermission));
t3.Start();
t3.Join();
// Set the context that contains the Deny.
// Show the deny is again
active.
Thread t4 =
new Thread(new
ThreadStart(DemandPermission));
t4.Start();
t4.Join();
// Demonstrate the execution context
methods.
ExecutionContextMethods();
Console.WriteLine("Demo
is complete, press Enter to exit.");
Console.Read();
}
catch (Exception
e)
{
Console.WriteLine(e.Message);
}
}
// Execute the Demand.
static
void DemandPermission()
{
try
{
Console.WriteLine("In
the thread executing a Demand for " +
"FileDialogPermission.");
new
FileDialogPermission(
FileDialogPermissionAccess.OpenSave).Demand();
Console.WriteLine("Successfully
demanded " +
"FileDialogPermission.");
}
catch (Exception
e)
{
Console.WriteLine(e.Message);
}
}
static void
ExecutionContextMethods()
{
// Generate a call context for this
thread.
ContextBoundType cBT =
new ContextBoundType();
cBT.GetServerTime();
ExecutionContext eC1 =
ExecutionContext.Capture();
ExecutionContext eC2 =
eC1.CreateCopy();
Console.WriteLine("The
hash code for the first execution " +
"context is: " +
eC1.GetHashCode());
// Create a SerializationInfo object to be used for
getting the
// object data.
SerializationInfo sI =
new
SerializationInfo(
typeof(ExecutionContext),
new
FormatterConverter());
eC1.GetObjectData(
sI,
new
StreamingContext(StreamingContextStates.All));
LogicalCallContext lCC = (LogicalCallContext)sI.GetValue(
"LogicalCallContext",
typeof(LogicalCallContext));
// The logical call context object should contain the
previously
// created call context.
Console.WriteLine("Is
the logical call context information " +
"available? " + lCC.HasInfo);
}
}
// One means of communicating between client and server is to use the
// CallContext class. Calling CallContext
effectivel puts the data in a thread
// local store. This means that the
information is available to that thread
// or that logical thread (across
application domains) only.
[Serializable]
public class
CallContextString :
ILogicalThreadAffinative
{
String _str =
"";
public CallContextString(String
str)
{
_str = str;
Console.WriteLine("A
CallContextString has been created.");
}
public override
String ToString()
{
return _str;
}
}
public class
ContextBoundType : ContextBoundObject
{
private
DateTime starttime;
public ContextBoundType()
{
Console.WriteLine("An
instance of ContextBoundType has been " +
"created.");
starttime = DateTime.Now;
}
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags = SecurityPermissionFlag.Infrastructure)]
public
DateTime GetServerTime()
{
Console.WriteLine("The
time requested by a client.");
// This call overwrites the client's
// CallContextString.
CallContext.SetData(
"ServerThreadData",
new
CallContextString("This is the server side
replacement " +
"string."));
return
DateTime.Now;
}
}
}
Output:
Executing Main in the primary thread.
In the thread executing a Demand for FileDialogPermission.
Successfully demanded FileDialogPermission.
Is the flow suppressed? True
Restore the flow.
Is the flow suppressed? False
In the thread executing a Demand for FileDialogPermission.
Request for the permission of type 'System.Security.Permissions.FileDialogPermis
sion, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e08
9' failed.
In the thread executing a Demand for FileDialogPermission.
Successfully demanded FileDialogPermission.
In the thread executing a Demand for FileDialogPermission.
Successfully demanded FileDialogPermission.
An instance of ContextBoundType has been created.
The time requested by a client.
A CallContextString has been created.
The hash code for the first execution context is: 54267293
Is the logical call context information available? True
Demo is complete, press Enter to exit.
Threads run Within a Process but Processes Don't Run: Threads Run.
Processes in the .NET Framework correspond one to one with a process in Windows.
A process' main purpose is to manage per-program resources; this includes a
shared virtual address space among all threads running in the process, a HANDLE
table, a shared list of loaded DLLs (mapped into the same address space), and a
variety of other process-wide data stored in the Process Environment Block (PEB).
Problems in one process normally do not affect another because of this type of
isolation. However, because of inter-process communication and machine-wide
shared resources -- such as files, memory-mapped I/O, and named kernel
objects--sometimes one process can interfere with another process.
Windows provides four objects designed for thread and process synchronization--mutexes,
semaphores, events (all of which are kernel objects), and a critical_section
object. Events are a kernel synchronization object used to signal other threads
that some event, such as a message, has occurred. The important capability
offered by events is that multiple threads can be released from a wait
simultaneously when a single event is signaled. In .NET, there is an Event class
where an event (a type of kernel object) can be one of two types: auto reset and
manual reset. When an auto reset event is signaled, the first object waiting for
the event turns its back to a non-signaled state. This behavior is similar to
that of a mutex. Conversely, a manual reset event allows all threads waiting for
it to become unblocked until something manually resets the event to a
non-signaled state. These events are represented as the AutoResetEvent class and
ManualResetEvent class in .NET. Both of these classes inherit from a common
EventWaitHandle class (which itself handles the WaitHandle class). Assume we
want to write some code to schedule some work on the thread-pool:
using
System;
using
System.Threading;
public
sealed class
Program
{
private static
void MyThreadPoolWorker(object
state)
{
ManualResetEvent mre = (ManualResetEvent)state;
// Do some work; this executes on a thread from the
thread-pool...
Console.WriteLine("Work
occurring on the thread-pool: {0}",
Thread.CurrentThread.ManagedThreadId);
// Now set the event to let our caller know we're
done:
mre.Set();
}
public static
void Main()
{
using (ManualResetEvent
mre = new
ManualResetEvent(false))
{
ThreadPool.QueueUserWorkItem(new
WaitCallback(MyThreadPoolWorker), mre);
// Continue working while the thread-pool executes the
work item:
Console.WriteLine("Continuing
work on the main thread: {0}",
Thread.CurrentThread.ManagedThreadId);
// Lastly, wait for the thread-pool to finish:
mre.WaitOne();
}
}
}
This example shows registering wait callbacks for events:
using
System;
using
System.Threading;
public
sealed class
Program
{
public static
void Main()
{
using (EventWaitHandle
ewh = new
ManualResetEvent(false))
using (EventWaitHandle
callbackDoneEvent = new
ManualResetEvent(false))
{
// Register our callback to be fired when
the event is set:
ThreadPool.RegisterWaitForSingleObject(ewh,
delegate
{
Console.WriteLine("Callback
fired: {0}",
Thread.CurrentThread.ManagedThreadId);
callbackDoneEvent.Set();
}, null,
Timeout.Infinite, true);
// Now set the event. Notice the callback
fires
// on a separate (thread-pool)
thread.
Console.WriteLine("Setting
the event: {0}",
Thread.CurrentThread.ManagedThreadId);
ewh.Set();
// wait for the callback to complete
callbackDoneEvent.WaitOne();
}
}
}
The result:
Setting the event: 1
Callback fired: 4
Continuing work on the main thread: 1
Work occurring on the thread-pool: 3
Thread Creation
To create a new thread in the .NET Framework, a Thread object must first be
created by of Threadd's many constructors:
public
delegate void
ThreadStart();
public
delegate void
ParameterizedThreadStart(object
obj);
public
class Thread
{
public Thread(ThreadStart
start);
public
Thread(ThreadStart start,
int maxStackSize);
public
Thread(ParameterizedThreadStart start);
public
Thread(ParameterizedThreadStart start,
int maxStackSize);
. . .
}
Recall that a thread created with the ParameterizedThreadStart based constructor
allows a caller to pass an object reference argument to the Start method (as a
parameter), which is then accessible from the new thread's Start routine as obj:
//example
using delegates
using
System;
using
System.Threading;
public
static class
Program
{
public static
void Main()
{
Thread newThread =
new Thread(
new
ParameterizedThreadStart(MyThreadStart));
Console.WriteLine("{0}:
Created thread (ID {1})",
Thread.CurrentThread.ManagedThreadId,
newThread.ManagedThreadId);
newThread.Start("Hello world");
// Begin execution.
newThread.Join();
// Wait for the thread to
finish.
Console.WriteLine("{0}:
Thread exited",
Thread.CurrentThread.ManagedThreadId);
}
private static
void MyThreadStart(object obj)
{
Console.WriteLine("{0}:
Running: {1}",
Thread.CurrentThread.ManagedThreadId,
obj);
}
}
The results:
1: Created thread (ID 3)
3: Running: Hello world
1: Thread exited
Here is a thread creation example that uses anonymous delegates:
using
System;
using
System.Threading;
public
static class
Program
{
public static
void Main()
{
Thread newThread =
new Thread(delegate(object
obj)
{
Console.WriteLine("{0}:
Running {1}",
Thread.CurrentThread.ManagedThreadId,
obj);
});
newThread.Start("Hello world (with anon delegates)");
newThread.Join();
}
}
Output:
3: Running Hello world (with anon delegates)
Finally, here is an example of thread creation using lambdas:
using
System;
using
System.Threading;
public
static class
Program
{
public static
void Main()
{
Thread newThread =
new Thread(obj
=>
Console.WriteLine("{0}:
Running {1}",
Thread.CurrentThread.ManagedThreadId,
obj)
);
newThread.Start("Hello world (with lambdas)");
newThread.Join();
}
}
Output:
3: Running Hello world (with lambdas)
So what have we got? We see that a thread exits. Is the state of the system the
same if the thread is abruptly terminated? We know that an application program
will frequently block execution so that it may perform I/O; for example, reading
the sectors on the disk, communicating with a network endpoint, etc. But UIs
work by processing messages enqueued onto a per-UI-thread message queue. Several
types of blocking enable the UI's message pump to run. But others do not. This
can cause messages (e.g., WM_CLOSE, WM_PAINT, etc.) to get clogged in the queue
until the I/O completes (i.e., it runs synchronously). For lengthy operations,
this can lead to an unresponsive UI. Here is an example of a small Windows Forms
program that demonstrates the very basics of maintaining the UI:
using
System;
using
System.Drawing;
using
System.Threading;
using
System.Windows.Forms;
class
Program : Form
{
private System.Windows.Forms.ProgressBar
_ProgressBar;
[STAThread]
static void
Main()
{
Application.Run(new Program());
}
public Program()
{
InitializeComponent();
ThreadStart threadStart = Increment;
threadStart.BeginInvoke(null,
null);
}
void UpdateProgressBar()
{
if (_ProgressBar.InvokeRequired)
{
MethodInvoker updateProgressBar = UpdateProgressBar;
_ProgressBar.Invoke(updateProgressBar);
}
else
{
_ProgressBar.Increment(1);
}
}
private void
Increment()
{
for (int
i = 0; i < 100; i++)
{
UpdateProgressBar();
Thread.Sleep(100);
}
}
private void
InitializeComponent()
{
_ProgressBar = new ProgressBar();
SuspendLayout();
_ProgressBar.Location = new Point(13,
17);
_ProgressBar.Size = new Size(267, 19);
ClientSize = new Size(292, 53);
Controls.Add(this._ProgressBar);
Text = "Multithreading in Windows Forms";
ResumeLayout(false);
}
}
The CLR Thread Pool basics involve queuing up a chunk of work that will be run
by the thread pool, use the pool to run some work when asynchronous I/Os
complete, execute work on a recurring or timed basis using timers, and/or
schedule some work to run when a kernel object becomes signaled. There are
articles about concurrency at MSDN that describe either managing the thread pool
order or outright building a custom thread pool to optimize the execution
context.