Sign in to follow this  

Thread Pooling Problem

This topic is 2815 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello, I am developing an application (in C++), and I have reached the point where I need to implement thread pooling in order to limit the number of threads executing at any particular moment. Currently, I have a global class that gets created at execution time. To maintain the number of threads, I have a private DWORD variable (dwNumThreads). Before each thread starts, that number is incremented by 1 by the class executing the thread. The DWORD representing the number of threads is then passed to the thread being created via a pointer (pdwNumThreads). Just before a thread terminates, that number is decremented by one ( via (*pdwNumThreads) -= 1 ) from the thread process. The incrementing/decrementing is detinitely even, yet after a period of time (usually it takes 24 hours or so), dwNumThreads somehow overflows and becomes a very large number.. thus, the next thread gets stuck waiting forever for the number of threads to become less than 10, since dwNumThreads is such a large number. Why is dwNumThreads overflowing, even though the incrementing/decrementing is definitely even? I should also mention that dwNumThreads is initialized properly to 0 when the application starts. I thought I knew everything about how programs manipulate numerical values in memory but apparently I do not.

Share this post


Link to post
Share on other sites
Because you're encountering a race condition. If two threads try to access the same value at the same time, bad things can happen. Instead of doing this:


(*pdwNumThreads) -= 1;



do this


::InterlockedDecrement(pdwNumThreads);



and similarly for addition change it to InterlockedIncrement.

Share this post


Link to post
Share on other sites
Ah, I see. I thought "incrementing" a value in memory was just that - changing the value of the variable directly in memory. I was unaware the value had to be copied locally, incremented, then copied back to the final location. That changes things quite a bit, as I thought simple things such as manipulating integers and DWORDS were multithread-safe (were a one-step process).

Thanks a lot for your help.

Share this post


Link to post
Share on other sites
Quote:
Original post by Eleventy
Ah, I see. I thought "incrementing" a value in memory was just that - changing the value of the variable directly in memory. I was unaware the value had to be copied locally, incremented, then copied back to the final location. That changes things quite a bit, as I thought simple things such as manipulating integers and DWORDS were multithread-safe (were a one-step process).

Thanks a lot for your help.


The problem is that the result of incrementing a value depends on what was there before. In x86 assembly it might boil down to the following:


INC [DWORD PTR ebp-0x10]



But you have no guarantee about optimizations that the compiler might perform. Actually in the case of a global variable, you kind of do, but even then only sometimes, in particular if the variable is not in an anonymous namespace or not declared file static.

Anyway, the point is that the rules are complicated. Suppose for example you had something like this:


DWORD ThreadFunc(LPVOID arg)
{
Instance* pInstance = reinterpret_cast<Instance*>(arg);

for (int i=0; i < 10; ++i)
++pInstance->count;
}



This function is called from multiple threads. The compiler might see that and say, "oh hey, I can optimize that so it doesn't have to do a pointer indirection every single time. Let's transform the code like this:


DWORD ThreadFunc(LPVOID arg)
{
Instance* pInstance = reinterpret_cast<Instance*>(arg);

int temp = pInstance->count;
for (int i=0; i < 10; ++i)
++temp;
pInstance->count = temp;
}



Ooops! Race condition. This is a contrived example, but the point is that there are almost always way too many gotchas, most of which are obscure and a mere mortal has no way of realizing all the different ways that things can screw of.

So... moral of the story is always use atomic operations or mutual exclusion locks around shared data, even when you think it doesn't require it. (Especially when you *think* it doesn't require it).

Share this post


Link to post
Share on other sites

This topic is 2815 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this