Click here to load reader
Upload
abasak
View
342
Download
3
Embed Size (px)
Citation preview
7,248,533 members and growing! (18,507 online) Sign out
threads
Abhishek Basak
30 Article Browse Code Stats Revisions
» General Programming » Threads, Processes & IPC » Multi-threading
Multithreading TutorialBy John Kopplin | 28 Dec 2006
This article demonstrates how to write a multithreaded Windowsprogram in C++ using only the Win32 API.
Download source and demo projects - 425 KB
Background
When you run two programs on an Operating System that offers memory protection, as Windows andUNIX/Linux do, the two programs are executed as separate processes, which means they are givenseparate address spaces. This means that when program #1 modifies the address 0x800A 1234 in itsmemory space, program #2 does not see any change in the contents of its memory at address 0x800A1234. With simpler Operating Systems that cannot accomplish this separation of processes, a faultyprogram can bring down not only itself but other programs running on that computer (including theOperating System itself).
The ability to execute more than one process at a time is known as multi-processing. A processconsists of a program (usually called the application) whose statements are performed in anindependent memory area. There is a program counter that remembers which statement should beexecuted next, and there is a stack which holds the arguments passed to functions as well as thevariables local to functions, and there is a heap which holds the remaining memory requirements of theprogram. The heap is used for the memory allocations that must persist longer than the lifetime of asingle function. In the C language, you use malloc to acquire memory from the heap, and in C++, you
use the new keyword.
Sometimes, it is useful to arrange for two or more processes to work together to accomplish one goal.One situation where this is beneficial is where the computer's hardware offers multiple processors. Inthe old days this meant two sockets on the motherboard, each populated with an expensive Xeon chip.Thanks to advances in VLSI integration, these two processor chips can now fit in a single package.Examples are Intel's "Core Duo" and AMD's "Athlon 64 X2". If you want to keep two microprocessorsbusy working on a single goal, you basically have two choices:
design your program to use multiple processes (which usually means multiple programs), or1.
design your program to use multiple threads.2.
So, what's a thread? A thread is another mechanism for splitting the workload into separate executionstreams. A thread is lighter weight than a process. This means, it offers less flexibility than a full blownprocess, but can be initiated faster because there is less for the Operating System to set up. What'smissing? The separate address space is what is missing. When a program consists of two or morethreads, all the threads share a single memory space. If one thread modifies the contents of the address0x800A 1234, then all the other threads immediately see a change in the contents of their address0x800A 1234. Furthermore, all the threads share a single heap. If one thread allocates (via malloc ornew) all of the memory available in the heap, then attempts at additional allocations by the other
threads will fail.
But each thread is given its own stack. This means, thread #1 can be callingFunctionWhichComputesALot() at the same time that thread #2 is calling
FunctionWhichDrawsOnTheScreen(). Both of these functions were written in the same program.
There is only one program. But, there are independent threads of execution running through thatprogram.
What's the advantage? Well, if your computer's hardware offers two processors, then two threads canrun simultaneously. And even on a uni-processor, multi-threading can offer an advantage. Mostprograms can't perform very many statements before they need to access the hard disk. This is a veryslow operation, and hence the Operating System puts the program to sleep during the wait. In fact, theOperating System assigns the computer's hardware resources to somebody else's program during the
4.40 / 5, 41 votes
Home Articles Questions & Answers Learning Zones Jobs Features Help! Lounge
Sponsored Links
See Also...
Announcements
The Daily Insider
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
1 of 8 8/14/2010 2:49 AM
wait. But, if you have written a multi-threaded program, then when one of your threads stalls, yourother threads can continue.
The Jaeschke Magazine Articles
One good way to learn any new programming concept is to study other people's code. You can findsource code in magazine articles, and posted on the Internet at sites such as CodeProject. I came acrosssome good examples of multi-threaded programs in two articles written for the C/C++ Users Journal, byRex Jaeschke. In the October 2005 issue, Jaeschke wrote an article entitled "C++/CLI Threading: Part1", and in the November 2005 issue, he wrote his follow-up article entitled "C++/CLI Threading: Part2". Unfortunately, the C/C++ Users Journal magazine folded shortly after these articles appeared. But,the original articles and Jaeschke's source code are still available at the following websites:
Part 1Part 2
You'll notice that the content from the defunct C/C++ Users Journal has been integrated into the Dr.Dobb's Portal website, which is associated with Dr. Dobb's Journal, another excellent programmingmagazine.
You might not be familiar with the notation C++/CLI. This stands for "C++ Common LanguageInfrastructure" and is a Microsoft invention. You're probably familiar with Java and C#, which are twolanguages that offer managed code where the Operating System rather than the programmer isresponsible for deallocating all memory allocations made from the heap. C++/CLI is Microsoft's proposalto add managed code to the C++ language.
I am not a fan of this approach, so I wasn't very interested in Jaeschke's original source code. I am sureJava and C# are going to hang around, but C++/CLI attempts to add so many new notations (andconcepts) on top of C++, which is already a very complicated language, that I think this language willdisappear.
But, I still read the original C/C++ Users Journal article and thought Jaeschke had selected goodexamples of multi-threading. I especially liked how his example programs were short and yet displayeddata corruption when run without the synchronization methods that are required for successfulcommunication between threads. So, I sat down and rewrote his programs in standard C++. This is whatI am sharing with you now. The source code I present could also be written in standard C. In fact, that'seasier than accomplishing it in C++ for a reason we will get to in just a minute.
This is probably the right time to read Jaeschke's original articles, since I don't plan to repeat his greatexplanations of multitasking, reentrancy, atomicity, etc. For example, I don't plan to explain how aprogram is given its first thread automatically and all additional threads must be created by explicitactions by the program (oops). The URLs where you can find Jaeschke's two articles are given above.
Creating Threads Under Windows
It is unfortunate that the C++ language didn't standardize the method for creating threads. Therefore,various compiler vendors invented their own solutions. If you are writing a program to run underWindows, then you will want to use the Win32 API to create your threads. This is what I willdemonstrate. The Win32 API offers the following function to create a new thread:
Collapse
uintptr_t _beginthread( void( __cdecl *start_address )( void * ), unsigned stack_size, void *arglist );
This function signature might look intimidating, but using it is easy. The _beginthread() function
takes three passed parameters. The first is the name of the function which you want the new thread tobegin executing. This is called the thread's entry-point-function. You get to write this function, andthe only requirements are that it take a single passed parameter (of type void*) and that it returnsnothing. That is what is meant by the function signature:
Collapse
void( __cdecl *start_address )( void * ),
The second passed parameter to the _beginthread() function is a requested stack size for the new
thread (remember, each thread gets its own stack). However, I always set this parameter to 0, whichforces the Windows Operating System to select the stack size for me, and I haven't had any problemswith this approach. The final passed parameter to the _beginthread() function is the single parameter
you want passed to the entry-point-function. This will be made clear by the following example program:
Collapse
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
2 of 8 8/14/2010 2:49 AM
#include <stdio.h>#include <windows.h>#include <process.h> // needed for _beginthread()
void silly( void * ); // function prototype
int main(){ // Our program's first thread starts in the main() function.
printf( "Now in the main() function.\n" );
// Let's now create our second thread and ask it to start // in the silly() function.
_beginthread( silly, 0, (void*)12 );
// From here on there are two separate threads executing // our one program.
// This main thread can call the silly() function if it wants to.
silly( (void*)-5 ); Sleep( 100 );}
void silly( void *arg ){ printf( "The silly() function was passed %d\n", (INT_PTR)arg ) ;}
Go ahead and compile this program. Simply request a Win32 Console Program from Visual C++ .NET2003's New Project Wizard and then "Add a New item" which is a C++ source file (.CPP file) in whichyou place the statements I have shown. I am providing Visual C++ .NET 2003 workspaces forJaeschke's (modified) programs, but you need to know the key to starting a multi-threaded programfrom scratch: you must remember to perform one modification to the default project properties that theNew Project Wizard gives you. Namely, you must open up the Project Properties dialog (select "Project"from the main Visual C++ menu and then select "Properties"). In the left hand column of this dialog,you will see a tree view control named "Configuration Properties", with the main sub-nodes labeled"C/C++", "Linker", etc. Double-click on the "C/C++" node to open this entry up. Then, click on "CodeGeneration". In the right hand area of the Project Properties dialog, you will now see listed "RuntimeLibrary". This defaults to "Single Threaded Debug (/MLd)". [The notation /MLd indicates that this choicecan be accomplished from the compiler command line using the /MLd switch.] You need to click on thisentry to observe a drop-down list control, where you must select Multi-threaded Debug (/MTd). Ifyou forget to do this, your program won't compile, and the error message will complain about the_beginthread() identifier.
A very interesting thing happens if you comment out the call to the Sleep() function seen in this
example program. Without the Sleep() statement, the program's output will probably only show a
single call to the silly() function, with the passed argument -5. This is because the program's process
terminates as soon as the main thread reaches the end of the main() function, and this may occur
before the Operating System has had the opportunity to create the other thread for this process. This isone of the discrepancies from what Jaeschke says concerning C++/CLI. Evidently, in C++/CLI, eachthread has an independent lifetime, and the overall process (which is the container for all the threads)persists until the last thread has decided to die. Not so for straight C++ Win32 programs: the processdies when the primary thread (the one that started in the main function) dies. The death of this threadmeans the death of all the other threads.
Using a C++ Member Function as the Thread's Entry-Point-Function
The example program I just listed really isn't a C++ program because it doesn't use any classes. It isjust a C language program. The Win32 API was really designed for the C language, and when youemploy it with C++ programs, you sometimes run into difficulties. Such as this difficulty: "How can Iemploy a class member function (a.k.a. an instance function) as the thread's entry-point-function?"
If you are rusty on your C++, let me remind you of the problem. Every C++ member function has ahidden first passed parameter known as the this parameter. Via the this parameter, the function
knows which instance of the class to operate upon. Because you never see these this parameters, it is
easy to forget they exist.
Now, let's again consider the _beginthread() function which allows us to specify an arbitrary entry-point-function for our new thread. This entry-point-function must accept a single void* passed param.
Aye, there's the rub. The function signature required by _beginthread() does not allow the hidden
this parameter, and hence a C++ member function cannot be directly activated by _beginthread().
We would be in a bind were it not for the fact that C and C++ are incredibly expressive languages(famously allowing you the freedom to shoot yourself in the foot) and the additional fact that_beginthread() does allow us to specify an arbitrary passed parameter to the entry-point-function.
So, we use a two-step procedure to accomplish our goal: we ask _beginthread() to employ a staticclass member function (which, unlike an instance function, lacks the hidden this parameter), and we
send this static class function the hidden this pointer as a void*. The static class function knows to
convert the void* parameter to a pointer of a class instance. Voila! We now know which instance of the
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
3 of 8 8/14/2010 2:49 AM
class should call the real entry-point-function, and this call completes the two step process. The relevantcode (from Jaeschke's modified Part 1 Listing 1 program) is shown below:
Collapse
class ThreadX{public:
// In C++ you must employ a free (C) function or a static // class member function as the thread entry-point-function.
static unsigned __stdcall ThreadStaticEntryPoint(void * pThis) { ThreadX * pthX = (ThreadX*)pThis; // the tricky cast
pthX->ThreadEntryPoint(); // now call the true entry-point-function
// A thread terminates automatically if it completes execution, // or it can terminate itself with a call to _endthread().
return 1; // the thread exit code }
void ThreadEntryPoint() { // This is the desired entry-point-function but to get // here we have to use a 2 step procedure involving // the ThreadStaticEntryPoint() function.
}}
Then, in the main() function, we get the two step process started as shown below:
Collapse
hth1 = (HANDLE)_beginthreadex( NULL, // security 0, // stack size ThreadX::ThreadStaticEntryPoint,// entry-point-function o1, // arg list holding the "this" pointer CREATE_SUSPENDED, // so we can later call ResumeThread() &uiThread1ID );
Notice that I am using _beginthreadex() rather than _beginthread() to create my thread. The "ex"
stands for "extended", which means this version offers additional capability not available with_beginthread(). This is typical of Microsoft's Win32 API: when shortcomings were identified, more
powerful augmented techniques were introduced. One of these new extended capabilities is that the_beginthreadex() function allows me to create but not actually start my thread. I elect this choice
merely so that my program better matches Jaeschke's C++/CLI code. Furthermore, _beginthreadex()
allows the entry-point-function to return an unsigned value, and this is handy for reporting status backto the thread creator. The thread's creator can access this status by calling GetExitCodeThread().
This is all demonstrated in the "Part 1 Listing 1" program I provide (the name comes from Jaeschke'smagazine article).
At the end of the main() function, you will see some statements which have no counterpart in
Jaeschke's original program. This is because in C++/CLI, the process continues until the last threadexits. That is, the threads have independent lifetimes. Hence, Jaeschke's original code was designed toshow that the primary thread could exit and not influence the other threads. However, in C++, theprocess terminates when the primary thread exits, and when the process terminates, all its threads arethen terminated. We force the primary thread (the thread that starts in the main() function) to waitupon the other two threads, via the following statements:
Collapse
WaitForSingleObject( hth1, INFINITE ); WaitForSingleObject( hth2, INFINITE );
If you comment out these waits, the non-primary threads will never get a chance to run because theprocess will die when the primary thread reaches the end of the main() function.
Synchronization Between Threads
In the Part 1 Listing 1 program, the multiple threads don't interact with one another, and hence theycannot corrupt each other's data. The point of the Part 1 Listing 2 program is to demonstrate how thiscorruption comes about. This type of corruption is very difficult to debug, and this makes multi-threadedprograms very time consuming if you don't design them correctly. The key is to providesynchronization whenever shared data is accessed (either written or read).
A synchronization object is an object whose handle can be specified in one of the Win32 waitfunctions such as WaitForSingleObject(). The synchronization objects provided by Win32 are:
eventmutex or critical sectionsemaphorewaitable timer
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
4 of 8 8/14/2010 2:49 AM
An event notifies one or more waiting threads that an event has occurred.
A mutex can be owned by only one thread at a time, enabling threads to coordinate mutually exclusiveaccess to a shared resource. The state of a mutex object is set to signaled when it is not owned by anythread, and to nonsignaled when it is owned by a thread. Only one thread at a time can own a mutexobject, whose name comes from the fact that it is useful in coordinating mutually exclusive access to ashared resource.
Critical section objects provide synchronization similar to that provided by mutex objects, except thatcritical section objects can be used only by the threads of a single process (hence they are lighter weightthan a mutex). Like a mutex object, a critical section object can be owned by only one thread at a time,which makes it useful for protecting a shared resource from simultaneous access. There is no guaranteeabout the order in which threads will obtain ownership of the critical section; however, the OperatingSystem will be fair to all threads. Another difference between a mutex and a critical section is that if thecritical section object is currently owned by another thread, EnterCriticalSection() waits
indefinitely for ownership whereas WaitForSingleObject(), which is used with a mutex, allows you
to specify a timeout.
A semaphore maintains a count between zero and some maximum value, limiting the number ofthreads that are simultaneously accessing a shared resource.
A waitable timer notifies one or more waiting threads that a specified time has arrived.
This Part 1 Listing 2 program demonstrates the Critical Section synchronization object. Take a look atthe source code now. Note that in the main() function, we create two threads and ask them both to
employ the same entry-point-function, namely the function called StartUp(). However, because the
two object instances (o1 and o2) have different values for the mover class data member, the two
threads act completely different from each other. Because in one case isMover = true and in the
other case isMover = false, one of the threads continually changes the Point object's x and y
values while the other thread merely displays these values. But, this is enough interaction that theprogram will display a bug if used without synchronization.
Compile and run the program as I provide it to see the problem. Occasionally, the print out of x and yvalues will show a discrepancy between the x and y values. When this happens, the x value will be 1larger than the y value. This happens because the thread that updates x and y was interrupted by thethread that displays the values between the moments when the x value was incremented and when they value was incremented.
Now, go to the top of the Main.cpp file and find the following statement:
Collapse
//#define WITH_SYNCHRONIZATION
Uncomment this statement (that is, remove the double slashes). Then, re-compile and re-run theprogram. It now works perfectly. This one change activates all of the critical section statements in theprogram. I could have just as well used a mutex or a semaphore, but the critical section is the mostlight-weight (hence fastest) synchronization object offered by Windows.
The Producer/Consumer Paradigm
One of the most common uses for a multi-threaded architecture is the familiar producer/consumersituation where there is one activity to create packets of stuff and another activity to receive andprocess those packets. The next example program comes from Jaeschke's Part 2 Listing 1 program. Aninstance of the CreateMessages class acts as the producer, and an instance of the ProcessMessages
class acts as the consumer. The producer creates exactly five messages and then commits suicide. Theconsumer is designed to live indefinitely, until commanded to die. The primary thread waits for theproducer thread to die, and then commands the consumer thread to die.
The program has a single instance of the MessageBuffer class, and this one instance is shared by both
the producer and the consumer threads. Via synchronization statements, this program guarantees thatthe consumer thread can't process the contents of the message buffer until the producer thread has putsomething there, and that the producer thread can't put another message there until the previous onehas been consumed.
Since my Part 1 Listing 2 program demonstrates a critical section, I elected to employ a mutex in thisPart 2 Listing 1 program. As with the Part 1 Listing 2 example program, if you simply compile and runthe Part 2 Listing 1 program as I provide it, you will see that it has a bug. Whereas the producer createsthe five following messages:
Collapse
11111111112222222222333333333344444444445555555555
the consumer receives the five following messages:
Collapse
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
5 of 8 8/14/2010 2:49 AM
12111111111322222222243333333335444444444
There is clearly a synchronization problem: the consumer is getting access to the message buffer assoon as the producer has updated the first character of the new message. But the rest of the messagebuffer has not yet been updated.
Now, go to the top of the Main.cpp file and find the following statement:
Collapse
//#define WITH_SYNCHRONIZATION
Uncomment this statement (that is, remove the double slashes). Then, re-compile and re-run theprogram. It now works perfectly.
Between the English explanation in Jaeschke's original magazine article and all the comments I haveput in my C++ source code, you should be able to follow the flow. The final comment I will make is thatthe GetExitCodeThread() function returns the special value 259 when the thread is still alive (andhence hasn't really exited). You can find the definition for this value in the WinBase header file:
Collapse
#define STILL_ACTIVE STATUS_PENDING
where you can find STATUS_PENDING defined in the WinNT.h header file:
Collapse
#define STATUS_PENDING ((DWORD )0x00000103L)
Note that 0x00000103 = 259.
Thread Local Storage
Jaeschke's Part 2 Listing 3 program demonstrates thread local storage. Thread local storage ismemory that is accessible only to a single thread. At the start of this article, I said that an OperatingSystem could initiate a new thread faster than it could initiate a new process because all threads sharethe same memory space (including the heap) and hence there is less that the Operating System needsto set up when creating a new thread. But, here is the exception to that rule. When you request threadlocal storage, you are asking the Operating System to erect a wall around certain memory locations inorder that only a single one of the threads may access that memory.
The C++ keyword which declares that a variable should employ thread local storage is__declspec(thread).
As with my other example programs, this one will display an obvious synchronization problem if youcompile and run it unchanged. After you have seen the problem, go to the top of the Main.cpp file andfind the following statement:
Collapse
//#define WITH_SYNCHRONIZATION
Uncomment this statement (that is, remove the double slashes). Then, re-compile and re-run theprogram. It now works perfectly.
Atomicity
Jaeschke's Part 2 Listing 4 program demonstrates the problem of atomicity, which is the situationwhere an operation will fail if it is interrupted mid-way through. This usage of the word "atomic" relatesback to the time when an atom was believed to be the smallest particle of matter and hence somethingthat couldn't be further split. Assembly language statements are naturally atomic: they cannot beinterrupted half-way through. This is not true of high-level C or C++ statements. Whereas you mightconsider an update to a 64 bit variable to be an atomic operation, it actually isn't on 32 bit hardware.Microsoft's Win32 API offers the InterlockedIncrement() function as the solution for this type of
atomicity problem.
This example program could be rewritten to employ 64 bit integers (the LONGLONG data type) and theInterlockedIncrement64() function if it only needed to run under a Windows 2003 Server. But,
alas, Windows XP does not support InterlockedIncrement64(). Hence, I was originally worried that I
wouldn't be able to demonstrate an atomicity bug in a Windows XP program that dealt only with 32 bitintegers. But, curiously, such a bug can be demonstrated as long as we employ the Debug mode settingsin the Visual C++ .NET 2003 compiler rather than the Release mode settings. Therefore, you will noticethat unlike the other example programs inside the .ZIP file that I distribute, this one is set for a Debugconfiguration.
As with my other example programs, this one will display an obvious synchronization problem if youcompile and run it unchanged. After you have seen the problem, go to the top of the Main.cpp file and
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
6 of 8 8/14/2010 2:49 AM
Article Top
find the following statement:
Collapse
static bool interlocked = false; // change this to fix the problem
Change false to true, and then re-compile and re-run the program. It now works perfectly because it
is now employing InterlockedIncrement().
The Example Programs
In order that other C++ programmers can experiment with these multithreaded examples, I makeavailable a .ZIP file holding five Visual C++ .NET 2003 workspaces for the Part 1 Listing 1, Part 1 Listing2, Part 2 Listing 1, Part 2 Listing 3, and Part 2 Listing 4 programs from Jaeschke's original article (nowtranslated to C++). Enjoy!
Conclusion
This is my second submission to CodeProject. The first demonstrated how to use Direct3D 8 to model theMunsell color solid so that you could then fly through this color cube as in a video game. I also have awebsite where I offer a complete introduction to programming, including assembly languageprogramming. My home page is www.computersciencelab.com.
License
This article, along with any associated source code and files, is licensed under The Code Project OpenLicense (CPOL)
About the Author
John Kopplin
United States
Member
Comments and Discussions
FAQ SearchSearchSearchSearch
Noise Tolerance Medium Layout Normal Per page 25 UpdateUpdateUpdateUpdate
New Message Msgs 1 to 25 of 30 (Total in Forum: 30) (Refresh) First Prev Next
Olaf Petersen 8:28 10 Aug '10
srivas 23:33 10 Sep '09
amirkool 11:36 22 Mar '09
CNN73 21:35 17 Jan '09
aryamunish7:00 8 Jan '09
RTrelles 0:13 2 Jul '08
Alexei Valyaev 20:13 23 Mar '08
ben_th 0:59 16 Nov '07
yesitookmypills 16:19 30 Oct '07
vic12000 21:53 2 Apr '07
PhilDeets19:52 17 May '07
Ms. Agrawal 0:15 2 Apr '07
sylgas 9:10 6 Feb '07
Rate this article for us! Poor Excellent VoteVoteVoteVote
My vote of 5
A possible flaw
hw t convert c# file to VLSI chip
problem with the Part 2 Listing 3 program
ReadArticleAndRunCode--YouWillBeAmazedWithExcellencyOfArticle.
// No success multithreading with WinMain //
join
Thanks for this great Tutorial !
Great Article
the purpose of the event in Part2Listing1, result of Part2Listing3
Re: the purpose of the event in Part2Listing1, result ofPart2Listing3
Executing Two Functions Simultaneously
multiple parameters in arglist?
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
7 of 8 8/14/2010 2:49 AM
gc3sanjose 12:16 27 Jun '07
Kaixi Luo 12:19 4 Jan '07
Michel Wassink 4:17 4 Jan '07
Jeffrey Walton 0:21 29 Dec '06
John Kopplin 10:45 29 Dec '06
Nguyen Luong Son 1:38 30 Sep '06
WhiteSky 23:53 15 Aug '06
prasikumbhare 5:21 21 Jul '06
ssanand 6:26 18 Jul '06
DotNET42 6:15 18 Jul '06
Thief^ 1:04 18 Jul '06
John Kopplin 21:33 19 Jul '06
Last Visit: 8:28 13 Aug '10 Last Update: 14:07 13 Aug '10 12 Next »
General News Question Answer Joke Rant Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+PgUp/PgDown to switch pages.
PermaLink | Privacy | Terms of UseLast Updated: 28 Dec 2006
Copyright 2006 by John KopplinEverything else Copyright © CodeProject, 1999-2010
Web20 | Advertise on the Code Project
Re: multiple parameters in arglist?
Great!
No obvious synchronization problem
Article Formatting
Re: Article Formatting
Duo core aware?
Thanks
Event Description is missing [modified]
Thanks
C++ 2005
Cache miss?
Re: Cache miss?
Multithreading Tutorial - CodeProject http://www.codeproject.com/KB/threads/MultithreadingTutorial.aspx
8 of 8 8/14/2010 2:49 AM