Archived

This topic is now archived and is closed to further replies.

Shannon Barber

STL & Threads

Recommended Posts

null_pointer    289
Well, if, as the link on thread safety states, that you need to provide your own synchronization mechanisms (see HMUTEX in Win32 SDK) if any of the threads will write to the container while others will be reading/writing from the same container, you might as well lock the whole thing yourself.

Just write one lock as a class template (if you''re familiar with templates) and use it to wrap the STL container. Methods that "read" from the container will be "const" functions in the container. Methods that actually change the container will be non-const. So, this should make it easy to create a wrapper class with const-overloaded operators.

Something like this:


template <typename T>
class exclusive_lock
{
// use very similar code to what smart pointers use, look
// at auto_ptr in MSDN to see what functions you should try
// overloading. overload them for const-ness, i.e.:
inline T* operator ->() { lock(); return &T; }
inline const T* operator ->() const { if( is_locked ) lock(); return &T; }

exclusive_lock(T& resource) : resource(resource) {}
~exclusive_lock() { unlock(); CloseMutex(...); }

void lock() { OpenMutex(...); is_locked = true; }
void unlock() { if( mutex ) ReleaseMutex(...); is_locked = false; }

private:
T& resource;
HMUTEX mutex;

volatile bool is_locked;
};



Different types of exclusion:


exclusive - only one thread can read or modify the data at a time; no threads can simultaneously read and modify the data;

write exclusive - multiple threads can read the data simultaneously, but only one thread can modify it. No threads may read from the data while it is being modified.

read exclusive - opposite of write exclusive.



Also, I may have some of the function names mixed up but that should be a good idea for a class.

Note that different ports of STL may need different types of exclusion. You should assume only "exclusive" unless the docs for that port say otherwise. "Write exclusive" could probably be better performance-wise. Which method you choose determines how you write the class. Or write a collection of classes.



- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
Wilka    122
Oops, I didn''t read the thread stuff properly. I was thinking of exceptions. Dunno what happen to my head there, must have been tired or something

Forget that post.

Share this post


Link to post
Share on other sites
mhkrause    122
Just a couple things.

First, avoid using Win32 mutexes. They''re kernel objects, so you have to do a mode switch to use them (which is slow.) Use CRITICAL_SECTION''s instead. They''re intraprocess only, but they''re a lot quicker. They only do the expensive mode switch when the critical section is not locked, which is the uncommon case.

Also, null''s solution is missing a few things. It just provides protection on the data in the containers, without regard to the containers themselves (which are shared.) You have to wrap the code that manipulates the shared containers in critical sections. Also, you can''t make the assumption that the containers and data remain valid between critical sections.



Share this post


Link to post
Share on other sites
null_pointer    289
mhkrause: No, actually it synchronises access via the overloaded operators. Unfortunately, my code is missing something else that I should have noticed before. There''s no way to automatically unlock() the resource after the operator has returned. *null pounds head against nearby wall*

You''ll have to use lock()/unlock() manually.




- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
Stoffel    250
To sum up, the answer is no. You have to manage simultaneous access to STL containers externally. This gives you the most flexibility in your design anyway.

BTW: CriticalSections are faster in Win32, but they aren''t as powerful as Mutexes: they cannot be named (and therefore shared across applications), and there''s no timeout for getting a CS. One thing in MFC that really got me is that I tried to use a CSingleLock with a CriticalSection with a non-Infinite timeout, and my program behaved very oddly. Turned out that MFC doesn''t throw an exception (I would have thrown unsupported exception) when you try to grab a CriticalSection with a timeout value.

Share this post


Link to post
Share on other sites
mhkrause    122
null: The big thing is you are protecting access to the data, not the container, which is shared and can be altered. You can implement what you have, and it''ll work 99% of the time. The other 1% you have an irreproducible, hard to trace bug. Probably not a big issue with games, but there are stories of synchronization bugs that would bring down servers once a month, and take a year to trace down.

Stoffel: The only real advantage mutexes have is that you can share them across process''s, but most people don''t need to do this. They don''t time out, but in general, relying on a timed wait implies a problem with your design elsewhere.

Share this post


Link to post
Share on other sites
null_pointer    289
mhkrause: No, I don''t think you understand. Look at this class:


template <typename type>
class exclusive_lock
{
public:
exclusive_lock(T& container) : container(container), is_locked(false) {}
~exclusive_lock() { if( is_locked ) unlock(); }

// these are for write access
T& operator * () { lock(); return( container ); unlock(); }
T* operator ->() { lock(); return( &container ); unlock(); }
T& operator = (const T& other) { if( this != &other ) { lock(); container = other.container; unlock(); } }

// these are for read access
const T* operator ->() const { lock(); return( container ); unlock(); }

// manual locking/unlocking
void lock() { /* grab the sync object */; is_locked = true; }
void unlock() { /* release the sync object */; is_locked = false; }

// state info (is volatile desirable here too?)
volatile const bool locked() { return( is_locked ); }

private:
T& container; // no other way to access it, except the operators
volatile bool is_locked; // should be an event object
};



Now that I think of it, perhaps it was the word resource that threw you off with the other code that I posted? The lock''s resource could be a container class or anything else. The only way to access the container would be to use the overloaded operators to call its member functions. Before (and this is the key) the operators return, they lock the mutex or critical section or whatever you use. Then, and only then, can the container''s member function be called.

What type of lock you create still depends on the level of thread safety that your port of STL supports.

And the only actual problem with that class (aside from some stupid bugs) is the code after the return statements in the operators. Yes, I did that on purpose. The operators have to be able to unlock themselves, or my class will simply hang your program indefinitely...hmmm...I think I know how to do it... It could be a performance problem unless it''s optimized very well by the compiler. Return a temp object from the operators with the same operators overloaded, containing a pointer to the sync object, and have its destructor unlock() the lock object. I''m not sure if you can use the same overloaded operator through two classes on the same call... It may actually be better to _not_ overload the operators in the lock class, and instead to make the user get the temp object from a member function and then call the operators on that. That''ll be something to play around with when I''ve got some free time...

The simplest way is always to make the user call lock()/unlock() manually.



- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
Ziggy5000    122
null-

on a side note, the method you use to lock/unlock objects- (setting a 32-bit or less variable) is an excellent and fast technique.

except- I wouldn''t try that on a multiprocessor system, since both CPUs could try and access the same RAM at once... I think anyway ''caus I never ran a multiprocessor system...




ill-lusion.com

ziggy@ill-lusion.com

Share this post


Link to post
Share on other sites
null_pointer    289
quote:
Excerpt from MSDN on C++ volatile keyword

Objects declared as volatile are not used in optimizations because their value can change at any time. The system always reads the current value of a volatile object at the point it is requested, even if the previous instruction asked for a value from the same object. Also, the value of the object is written immediately on assignment.



I''m not 100% sure if that affects multiple processor systems, but it seems as if it would work. Windows'' HEVENT objects would be much safer though.

Also, I wasn''t using a bool for the lock - only to indicate the status of the lock. As mhkrause said, you should use a critical section for that.



- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
null_pointer    289
quote:
Original post by mhkrause

The only real advantage mutexes have is that you can share them across process''s, but most people don''t need to do this. They don''t time out, but in general, relying on a timed wait implies a problem with your design elsewhere.



I just remembered why I disagree with this.

1. WAIT_OBJECT_0 is useful for checking the status of the sync object and could be used for more sophisticated code. If an important thread knows that it will be blocked by locking the sync object (perhaps a very long wait, depending on the app), it could check the status without blocking and attend to other tasks while it is waiting.

2. I think that a timed wait (WaitForSingleObjectEx) is the only way to do asynchronous Win32 file I/O.

So I would say that the timed wait feature of the mutex could be a reason to use them instead of critical sections.




- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
msn12b    390
Hmm... you obviously don't understand synchronization methods very well.

Locking mechanisms guarantee a certain invariant holds while a particular piece of code executes. You're code does not guarantee any invariant will hold at any time. For example, a typical read-modify-write operation will never work, as you have to acquire the lock on the read and write operation separately; at any point in between those two operations another thread/process could alter the data. This is bad for a multitude of reasons, all of which I leave as an exercise for the reader.

You should never bind locks to a particular piece of data, rather only to the access to that data. Otherwise, you design crap like this which looks good in your head, but doesn't work at all in the real world.

MSN


Edited by - msn12b on October 31, 2000 2:28:46 PM

Share this post


Link to post
Share on other sites
mhkrause    122
quote:


1. WAIT_OBJECT_0 is useful for checking the status of the sync object and could be used for more sophisticated code. If an important thread knows that it will be blocked by locking the sync object (perhaps a very long wait, depending on the app), it could check the status without blocking and attend to other tasks while it is waiting.



Lookup TryEnterCriticalSection(). But usually if you have to resort to methods like this, your design is flawed. If you need to check the status of the mutex (checking status can be useful on events and other objects, but we''re not discussing that here,) it implies that someone somewhere didn''t drop the lock when supposed to. By using a solution like this, you''re trying to solve the symptom, not the problem.

quote:

2. I think that a timed wait (WaitForSingleObjectEx) is the only way to do asynchronous Win32 file I/O.



What does this have to do with our discussion of mutex''s vs critical sections in Win32???? WaitForSingleObjectEx is used for any kernel object. It happens to be the same API to lock a mutex, join a thread, join a process, block until disk IO complete, etc. Essentially wait until this handle is ready to go.

And there are other ways to do async IO, ReadFileEx, or just have a thread in the background reading off of disk.

Share this post


Link to post
Share on other sites
null_pointer    289
quote:
Original post by msn12b

You should never bind locks to a particular piece of data, rather only to the access to that data. Otherwise, you design crap like this which looks good in your head, but doesn't work at all in the real world.



(edited to remove now unnecessary explanation)

Obviously. I think I just figured out how people are getting these absurd ideas about my exclusive_lock class. The second version contains comments that mean:

/* replace this with some code to do the task described */

while every one is interpreting it as this:

/* description of some code to follow */

As I've said before (how MANY times?) the bool is NOT used as a sync object. It is only used for the purpose of return valid data from the locked() function - a simple bool. I could have used the API functions, but I _didn't_. So sue me. I did flag it so that people would look it up _before_ using the bool like that. I indicated that a better method is needed.

If I were writing production code, rest assured that I would not be so stupid as to let you look at it before it was anywhere near ready to used, and I would test it and look things up in the documentation if need be. I _do_ know how basic synchronization works.




- null_pointer
Sabre Multimedia


Edited by - null_pointer on October 31, 2000 10:55:14 PM

Share this post


Link to post
Share on other sites
null_pointer    289
Ah HA! I knew that I had run into problems with that function before...


quote:
Excerpt from MSDN

TryEnterCriticalSection

The TryEnterCriticalSection function attempts to enter a critical section without blocking. If the call is successful, the calling thread takes ownership of the critical section.

(lots of stuff trimmed for space concerns)

QuickInfo
Windows NT: Requires version 4.0 or later.
Windows: Unsupported.
Windows CE: Unsupported.
Header: Declared in winbase.h.
Import Library: Use kernel32.lib.



I''m not stupid - just very absent-minded.



- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
null_pointer    289
You can run multiple threads on a single processor because Windows "juggles" them so it looks like they''re running simultaneously. I don''t know how multiple processors would affect it... Basically, TryEnterCriticalSection is the functional equivalent of WaitForSingleObject(handle, 0);, but for critical sections. That is, you either get the critical section immediately or you don''t get it, but in neither case is your thread blocked. You have to check the return value to see if you''ve actually acquired the critical section. I''m not sure about differences under the hood.

mhkrause is probably right about the bad design thing though. I just wanted to make sure I wasn''t going mad when I imagined problems with that function (and others, especially in multithreading...). I "resurrected" my old Win32 library project and remembered some of the problems I had.



- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
mhkrause    122
The differences under the hood are:

(This is all pseudo code and is probably close to how Windows does it. You want to acquire a critical section in as few cycles as possible, so it''s probably 100% assembly. Some extra details are left out.)


void EnterCriticalSection(CRITICAL_SECTION* pcs)
{
if(InterlockedExchange(pcs->lock, 1)==1)
{
// Lock is held, transition to kernel mode and
// either reacquire or block.
SystemCall(ACQUIRE_CS, pcs);
}
}

BOOL TryEnterCriticalSection(CRITICAL_SECTION* pcs)
{
return InterlockedExchange(pcs->lock, 1)

}


The only difference is Enter transitions to kernel mode and blocks if the lock is held. Note that we have to check the lock again in kernel mode, because we may have been interrupted and the lock released sometime after the initial InterlockedExchange().

Also, I doubt Win9x transitions to kernel mode, but that involves its design (user programs have too many privileges.)

Share this post


Link to post
Share on other sites
null_pointer    289
Hmm... That''s interesting... thanks for the info! However, I was thinking more along the lines of differences between WaitForSingleObject(handle, 0) and TryEnterCriticalSection, but you said that WaitForSingleObject enters kernel mode anyway like EnterCriticalSection? or is it more like TryEnterCriticalSection?



- null_pointer
Sabre Multimedia

Share this post


Link to post
Share on other sites
mhkrause    122
WaitForSingleObject always enters kernel mode. The handle passed to it is a kernel object, so it can''t be accessed from user mode. That''s why critical sections are faster, in the common case of no lock contention, you do not do a mode switch (which can be costly.)

Share this post


Link to post
Share on other sites