Just to make things clear, in threading tutorials the terms "mutex", "critical section" and "lock" are exchangeable, they mean the same. There are several kinds of them on each os. The kinds I usually use are the following:
- spin lock
- normal lock between the threads of a single process
- inter-process lock to synchronize between any threads of any processes, for example if you share data via shared memory between several processes
Spin locks are evil, they are usually used only in case of hardcore optimizations and in case where the time a thread holds a lock is extremely small. You may never need it in your life but good to know about its existence. A normal lock is perfect to synchronize access to a shared resource between several threads inside your program. This is the kind of lock you will use in most cases, it is usually better in performance than an interprocess lock, so dont use interprocess locks when you don't need them. In windows the name of the normal lock is CRITICAL_SECTION, while the name of the inter-process lock is Mutex. Don't get confused by these names, they could have given the "Mutex" name to the normal lock if they wanted to do so, they have just used synonyms to differentiate them. So in most cases you will use the CRITICAL_SECTION in windows. Each platform has different thread synchronization primitives and in case of performance critical scenarios you have to find out or test out which one works best for your case.
Another important property of locks is whether they are recursive or not. For example the windows CRITICAL_SECTION is recursive by default. This means that after a thread acquires the lock for the first time, it can acquire the lock as many times as it wants without blocking, but it must release the lock the same number of times as it was acquired. A non-recursive lock is simpler and usually a bit faster than a recursive one but if your thread tries to acquire it for the second time without releasing the first, it gets into deadlock.
This is how I implement a normal lock on windows:
class CLock
{
public:
CLock()
{
InitializeCriticalSection(&m_CriticalSection);
}
~CLock()
{
DeleteCriticalSection(&m_CriticalSection);
}
void Acquire()
{
EnterCriticalSection(&m_CriticalSection);
}
void Release()
{
LeaveCriticalSection(&m_CriticalSection);
}
private:
CLock(const CLock&); CRITICAL_SECTION m_CriticalSection;
};
class CAutoLock
{
public:
CAutoLock(CLock& lock)
: m_Lock(lock)
{
m_Lock.Acquire();
}
~CAutoLock()
{
m_Lock.Release();
}
private:
CLock &m_Lock;
};
#define CONCAT_MACRO1(var_name, counter) var_name##counter
#define CONCAT_MACRO2(var_name, counter) CONCAT_MACRO1(var_name, counter)
#define CONCAT_COUNTER(var_name) CONCAT_MACRO2(var_name, __COUNTER__)
#define LOCK_SCOPE(var) CAutoLock CONCAT_COUNTER(__auto_lock)(var)
CLock m_Lock2;
class CTest
{
public:
void DoJob()
{
{
LOCK_SCOPE(m_Lock1);
LOCK_SCOPE(m_Lock2);
}
LOCK_SCOPE(m_Lock1);
bool success = DoJob2();
if (!success)
return;
}
private:
bool DoJob2()
{
return true;
}
private:
CLock m_Lock1;
};
In small programs its ok to use pre-baked lock classes from mfc or boost or the standard library. However if you make something serious then your own implementation can help you a lot. You can put debug stuff into your CLock or CMutex or whatever classes. For example in a non-recursive lock class you can ASSERT when the same thread tries to acquire the lock for the second time making it easier to catch big mistakes/deadlocks early. In a spinlock class you can count the number of spins to find out if a spinlock somewhere is effective or not...