Advertisement

Multithreading within a class

Started by February 22, 2001 04:43 PM
19 comments, last by griffenjam 23 years, 11 months ago
quote:
Original post by amag


quote:
Using a non-kernel object (i.e. a regular variable) to signal for shutdown is faster than constantly calling WaitFor...()


Yes, so there must be a reason for using a kernel object now, mustn't it? I'd rather have a program that works than one really fast that doesn't.



Well, I used a normal variable in my example to signal a stop, not an event. My program still works. Why is that?

Multithreaded programming is difficult. A novice will attempt to solve any multithreading problem by either throwing more synchronization objects at the problem, or by using one synchronization object and acquiring it everywhere. This quickly turns nasty.

What you need to do instead is look at every piece of data that is shared between multiple threads, and ask yourself the question, "What happens if the value of this data changes after I've read it?" If something bad happens, you need to protect the data with a synchronization object.

In my example, the bool m_bStopThread is cleared before the thread stops. The thread will run until it reads the value of this flag as true. The only operation that can set this bool to false is the destructor. What happens in the destructor immediately after m_bStopThread is set? It waits on the thread handle.

So what happens if I set the m_bStopThread after the working thread has checked its value and read it as cleared? Well, my other thread is in an infinite wait on the thread handle. Therefore my worker thread gets all the CPU time, and finishes executing its loop before returning to the top and finding that the m_bStopThread flag is clear. It exits, setting the thread event and letting my original thread continue.

Obviously, there's no reason to make m_bStopThread an event. It only wastes a handle and causes undue context switching.

Now, if you're doing a Sleep within your worker thread, you're going to be switching contexts anyway. In this case, it can be handy (as I mentioned in my original post) to make the flag into an Event, and use the timeout in the WaitForSingleObject in place of your Sleep since they perform the same action. This also allows you to return instantly from the thread as soon as the event is signalled. But it's not a requirement in order for this thread scheme to work.

Edit - to fix nested quotes

Edited by - Stoffel on February 26, 2001 3:42:36 PM
I'm glad we agree Stoffel There is really no point to use an event object when all you are doing with it is quickly polling its state. Where events have their power is when you need to wait for a meaningful duration of time without wasting CPU cycles. If you are just going to use an event as a glorified variable it is actually wasting CPU cycles.

Typically I use events for cross thread communication due to their inherent synchronization abilities (auto-reset events).

I use mutexes for cross process synchronization and critical sections for necessary thread synchronization.

Many people do not realize that you don't need to synchronize *every* section of code. Even variables that are read by more than one thread can be left unsynchronized under certain circumstances (the m_bShutdown variable is a good example).

Just for fun, here is a little thread locking class I whipped up ( yes I know its been done before but hasn't most code in one way or another )



template
class CGuard
{
public:
CGuard(CLock& lock) : m_lock(&lock)
{
m_lock->Lock();
}

~CGuard()
{
m_lock->Unlock();
}

private:
CLock* m_lock;
};

class CNoLock
{
public:
CNoLock()
{
}

~CNoLock()
{
}

inline void Lock()
{
}

inline void Unlock()
{
}
};

class CThreadLock
{
public:
CThreadLock()
{
InitializeCriticalSection(&m_cs);
}

~CThreadLock()
{
DeleteCriticalSection(&m_cs);
}

inline void Lock()
{
EnterCriticalSection(&m_cs);
}

inline void Unlock()
{
LeaveCriticalSection(&m_cs);
}

private:
CRITICAL_SECTION m_cs;
};



Then you'd use the classes as follows:



void SomeThreadFunc()
{
CGuard(m_threadlock);

// shared data accessed here (static perhaps).

}



Now, even if some code in SomeThreadFunc throws an exception, the lock will still be freed.





Dire Wolf
www.digitalfiends.com

Edited by - Dire.Wolf on February 26, 2001 4:05:41 PM
[email=direwolf@digitalfiends.com]Dire Wolf[/email]
www.digitalfiends.com
Advertisement
Stoffel:
I perfectly know you can use an ordinary variable for some cases of synchronisation, there''s even an algorithm for synchronising two threads without any MT-primitives. I was trying to make a point. When you learn to ride a bike, you don''t learn first to ride without using your hands, and then with, cause then you''d fall on your head, right? (ok, most people fall on their head once in a while anyway, but that''s not the point here ) I think the right way is to learn the priciples first, then the exceptions. Yours is an exception and may I say, a pretty unwarranted one. It falls under the category pre-mature optimisations, and IMHO pre-mature optimisations is the root to all evil, they hardly ever induce any speed increase, rather they induce a bug increase. Ok, so you save a handle, but I''m pretty sure you''ll run out of memory before you run out of handles. Yes it works, but in the case of the beginner (as you mentioned) it is pretty confusing, when is one to use an MT-primitive and when a variable? I try to make it easy by saying always use the MT-primitive, until you like Stoffel can reason when it is not neccessary.
amag:

I wouldn''t consider using a non-kernel object over a kernel object premature optimization. Don''t take this the wrong way but the difference is knowing how to writing proper multithreaded code over overly cautious code.

There is absolutely no reason to use a kernel object for something as simple as shutting down a thread - unless you have a reason. A reason could be that one thread is waiting for another to shutdown but doesn''t want to block. Using a simple boolean variable here would be overly complicated. A kernel object is the right choice (just use the thread''s handle).

Ex.



Thread1.Run();
Thread2.Run();

void Thread2::Run()
{
HANDLE threadhandle = Thread1.GetHandle();

while(WaitForSingleObject(threadhandle, 0) == WAIT_TIMEOUT)
{
// do something processing, else if nothing to do
// the wait timeout should be longer
}
}



Since Thread1''s handle will become signalled when it exits, Thread2 will be able to detect it and shutdown.

Learning the principles of multithreaded programming also means learning when not to use synchronization objects.



Dire Wolf
www.digitalfiends.com
[email=direwolf@digitalfiends.com]Dire Wolf[/email]
www.digitalfiends.com
Will the bool be handled correctly on SMP machines, where it may be accessed by multiple threads running on different processors?

I think delayed creation is a bad design because in many cases clutters objects with details that the language already handles. If you want to create the object dynamically, use indirection and make it a data member instead of a base class.

There is a better solution for the thread class somewhere.


Magmai: The thread class is part of a library, and the event class is already provided for people who use the library. It does assertion checking among other things so I thought I''d reuse it in the thread class.
quote:
Original post by null_pointer

Will the bool be handled correctly on SMP machines, where it may be accessed by multiple threads running on different processors?


Yes, because if the state of the variable is indeterminate it does not cause the process to break. This is basically the root of all multitasking logic: find what data needs to be forced into a determinate state over critical sections, and protect those critical sections with mutexes. There is no such critical section in my example, because the boolean only has an initial state (false) and a final state (true). If there were more state changes, the variable would need to be protected by a mutex.

As a side note, multithreaded logic is a lot like wired state machines in electrical engineering (that''s what I got my B.S. in). In EE, you try to make sure your variable state changes only go to adjacent states in a Karnaugh map so that only one bit changes at a time--a kind of grey code application. Otherwise, you need an externally synchronizing clock signal (which slows down your throughput).

amag:
I would like to see this algorithm that synchronizes two threads without any MT-specific objects, especially since that is, as far as my understanding goes, not possible.

It sounds to me like you are espousing cautious use of synchronization primitives (or MT-objects, as you call them) for beginners. This will give you working code. It will also give you inefficient code that will not scale on multiple processors and might as well be programmed without threads because everything executes serially.

When I first started using threads, I made one mutex, and every function in every thread got that same mutex at the top and released it on exit. Of course, there were no conflicts. But it didn''t work efficiently.

If you''re going to jump into learning multithreaded programming, you should learn to do it correctly. I believe it''s because people start off with these kinds of mistakes, having to fix their conflicts by grabbing a mutex everywhere instead of only when they should, that multithreaded programming gets such a bad rap in the game community.
Advertisement
The WaitForSingleObject does more than just poll the event, it also relinquishes the cpu to other threads, just like Sleep(0) does. It''s generally a good idea to put a sleep in the loop somewhere, even a sleep(0) at the end (or beginning) can help - it allows you to influence where & when the thread will be switched out. And it is more efficent use of the machine to switch out between calculation intensive iterations, as it minimizes cpu/fpu state changes. ''Course that minimal improvement might be blown on the 4us kernel mode switch.

Anyway, using a WaitFor combines the sleep & exit poll, together or seperate it requires one kernel call, as was mentioned above.

What you really want to do is a WaitForMutli... and only do work when you need to, and exit when told. This also allows you to process data at rates above 100Hz (more often than every 10ms), and not use 100% of the cpu.
That''s no good for an infinite amount of work though, like a render loop.

Magmai Kai Holmlor
- The disgruntled & disillusioned
- The trade-off between price and quality does not exist in Japan. Rather, the idea that high quality brings on cost reduction is widely accepted.-- Tajima & Matsubara
Stoffel:
The name of the algoritm is Peterson''s algorithm and it looks like this:
(pseudo-code)
  bool flag1, flag2;int turn;thread1(){  while(true)  {    flag1 = true; // I want to enter critical section    turn = 2; // but I give priority to the other thread    while(flag2 && turn == 2);    MyCriticalCode(); // the things that needs to be protected    flag1 = false; // I''m done    // out of critical section  }}thread2(){  while(true)  {    flag2 = true; // I want to enter critical section    turn = 1; // but I give priority to the other thread    while(flag1 && turn == 1);    MyCriticalCode(); // the things that needs to be protected    flag2 = false; // I''m done    // out of critical section  }}  


quote:

It sounds to me like you are espousing cautious use of synchronization primitives (or MT-objects, as you call them) for beginners. This will give you working code. It will also give you inefficient code that will not scale on multiple processors and might as well be programmed without threads because everything executes serially.


If you think I espouse cautious use of MT-primitives, then please re-read my posts. I believe the granularity of mutexes should be very fine. Using events instead of booleans does not make your program execute serially, then your design is really lousy.

quote:

If you''re going to jump into learning multithreaded programming, you should learn to do it correctly. I believe it''s because people start off with these kinds of mistakes, having to fix their conflicts by grabbing a mutex everywhere instead of only when they should, that multithreaded programming gets such a bad rap in the game community.


I''m glad we agree that it should be learnt correctly, the correct way is not to have one global mutex that protects everything. It is to have one mutex per object/var that needs to be protected. If you do it that way your code will not run in serial and you won''t have problems with synchronisation.
When it comes to events it''s like Magmai says, it''s not just to signal when the thread should quit, it''s also to utilize the CPU better.
When it comes to better utilize multiple processors, I don''t think that''s a beginners task, but then you should think through your designs clearly, and try to remove as much synchronisation as possible. However that does not mean remove synchronisation-primitives, but synchronisation all together (of course the InterLocked*()-functions are quite useful instead of critical sections for shared variables, when possible). The threads should run as independently as possible.
Stoffel: Thanks for answering my question! I may keep the event in the thread class because I use WaitForMultipleObj in my audio stream class. Replacing the event with a bool would mean using a timed wait on my stream positions so that I could periodically break to check the bool. It seems to work fine with a value of 250 ms, which is very infrequent, so I was wondering: which is better performance-wise?
quote:
Original post by null_pointer

Stoffel: Thanks for answering my question! I may keep the event in the thread class because I use WaitForMultipleObj in my audio stream class. Replacing the event with a bool would mean using a timed wait on my stream positions so that I could periodically break to check the bool. It seems to work fine with a value of 250 ms, which is very infrequent, so I was wondering: which is better performance-wise?


Performance-wise, if you''re going to be waiting anyway, keep it as an event and use it in you''re loops WaitForMultipleObjects. I hate to quote myself, but in my first response I said:
"I like to make m_bStopThread an Event, and instead of a while in my thread I have a WaitForSingleObject with a timeout so I can sleep until my next update period."

amag: thanks for listing the algorithm. I can see why kernel objects are used instead of that method (it doesn''t scale well for 2+ threads waiting on an object). We seem to pretty much be agreeing now, so hopefully some coders out there learned something from this discussion.

This topic is closed to new replies.

Advertisement