STL & Threads

Started by
21 comments, last by Shannon Barber 23 years, 5 months ago
Is the STL thread safe? Is there a thread-safe version? Or do you need to add you''re own critical sections etc...?
- 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
Advertisement
Some versions (optionally) are, STLport is thread safe and it''s one of the better STL version out there (it''s also free).
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 &lttypename 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
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.
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.



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
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.
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.
thanks
- 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
mhkrause: No, I don''t think you understand. Look at this class:


template &lttypename 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

This topic is closed to new replies.

Advertisement