Introduction
There are many open source applications which use threading and are limited to either *nix or Windows because Windows handles threading a bit differently than *nix. I develop on macOS so pthreads is my go to but using it effectively locks me out of Windows because Windows doesn’t implement pthread. Instead it has it’s own thread API. There is a pthread implementation that works on Windows but it’s big, and heavy.
Most cross platform thread libraries were written when XP was still supported and widely used. Back then Windows threads didn’t natively support conditionals or read write locks. So these (winpthreads) write emulations for this behavior. Which is a big part of why they’re so heavy.
I do like winpthreads because it brings the pthread API to Windows. This makes porting for *nix easy. Other thread libraries provide their own API but that’s only useful if you’ve already written your app using that library. I’d rather not rewrite my code to use a different threading library if I want to support Windows.
Now that we’re in a modern era, Windows now supports both conditionals and read write locks. This makes things much easier on us because we can make a thin pthread API wrapper around native Windows threads.
The Wrapper
Header
#ifndef __CPTHREAD_H__
#define __CPTHREAD_H__
#ifdef _WIN32
# include <stdbool.h>
# include <windows.h>
#else
# include <pthread.h>
#endif
#ifdef _WIN32
typedef CRITICAL_SECTION pthread_mutex_t;
typedef void pthread_mutexattr_t;
typedef void pthread_condattr_t;
typedef void pthread_rwlockattr_t;
typedef HANDLE pthread_t;
typedef CONDITION_VARIABLE pthread_cond_t;
typedef struct {
SRWLock lock;
bool exclusive;
} pthread_rwlock_t;
struct timespec {
long tv_sec;
long tv_nsec;
};
#endif
#ifdef _WIN32
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
int pthread_join(pthread_t thread, void **value_ptr);
int pthread_detach(pthread_t);
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_cond_init(thread_cond_t *cond, pthread_condattr_t *attr);
int pthread_cond_destroy(thread_cond_t *cond);
int pthread_cond_wait(thread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(thread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_signal(thread_cond_t *cond);
int pthread_cond_broadcast(thread_cond_t *cond);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
#endif
unsigned int pcthread_get_num_procs();
void ms_to_timespec(struct timespec *ts, unsigned int ms);
#endif /* __CPTHREAD_H__ */
For the most part we just have pthread functions if this is used on Windows. However, there are a few things that can’t be directly wrapped.
RWLocks
typedef struct {
SRWLock lock;
bool exclusive;
} pthread_rwlock_t;
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
First we have read write locks. pthreads have a single unlock function because it tracks what type of lock is being held. Windows does not do any tracking and instead has two unlock functions (read and write) so we need to know what kind of read/write lock we’re in so we can call the correct unlock function.
Unfortunately, to keep with the API where the rwlock object is stack
allocated we need to have our pthread_rwlock_t
implementation public.
This is unfortunate because we’re limited in what type of changes we
can make in the future but hopefully we won’t have to make any.
Conditionals
struct timespec {
long tv_sec;
long tv_nsec;
};
int pthread_cond_timedwait(thread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
void ms_to_timespec(struct timespec *ts, unsigned int ms);
When using a timeout on conditionals, pthreads takes an absolute time.
Unfortunately, struct timespec
doesn’t exist on Windows so we need
implement it. To make things easier on us we’re going to provide a helper
function which takes milliseconds intended to be in the future from
now and converts it into an absolute time. While not required it does
make timed_wait
a lot easier to use.
Implementation
Windows
Create
Create needs to use a wrapper function around the thread function because pthread based threads
return a pointer, whereas the Windows threading API returns a status code. Meaning the prototype
of the function our API uses is different than what CreateThread
accepts. So we’ll wrap the
thread function in another function that provides the correct function signature.
typedef struct {
void *(*start_routine)(void *);
void *start_arg;
} win_thread_start_t;
static DWORD WINAPI win_thread_start(void *arg)
{
win_thread_start_t *data = arg;
void *(*start_routine)(void *) = arg->start_routine;
void *start_arg = arg->start_arg;
free(data);
start_routine(start_arg);
return 0; /* ERROR_SUCCESS */
}
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
{
win_thread_start_t *data;
void(attr);
if (thread == NULL || start_routine == NULL)
return 1;
data = mcalloc(sizeof(*data));
data->start_routine = start_routine;
data->start_arg = arg;
*thread = CreateThread(NULL, 0, win_thread_start, data, 0, NULL);
if (*thread == NULL)
return 1;
return 0;
}
Create and start running the thread.
Join and Detach
int pthread_join(pthread_t thread, void **value_ptr)
{
(void)value_ptr;
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
return 0;
}
int pthread_detach(pthread_t thread)
{
CloseHandle(thread);
}
For join we will wait for the thread to stop (blocking) and destroy it.
Detach is a bit odd because it looks like we’re
destroying the thread. This is partly true this will cause
the thread to be cleaned up but CloseHandle
does not
stop the thread. It will keep running without interruption
(and without having this function block) and once finished
be cleaned up.
Mutex
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
{
(void)attr;
if (mutex == NULL)
return 1;
InitializeCriticalSection(mutex);
return 0;
}
int pthread_mutex_destroy(pthread_mutex_t *mutex)
{
if (mutex == NULL)
return 1;
DeleteCriticalSection(mutex);
return 0;
}
int pthread_mutex_lock(pthread_mutex_t *mutex)
{
if (mutex == NULL)
return 1;
EnterCriticalSection(mutex);
return 0;
}
int pthread_mutex_unlock(pthread_mutex_t *mutex)
{
if (mutex == NULL)
return 1;
LeaveCriticalSection(mutex);
return 0;
}
Mutexes on Windows are known as Critical Sections and we can directly wrap them.
Conditionals
int pthread_cond_init(thread_cond_t *cond, pthread_condattr_t *attr)
{
(void)attr;
if (cond == NULL)
return 1;
InitializeConditionVariable(cond);
return 0;
}
int pthread_cond_destroy(thread_cond_t *cond)
{
/* Windows does not have a destroy for conditionals */
(void)cond;
return 0;
}
int pthread_cond_wait(thread_cond_t *cond, pthread_mutex_t *mutex)
{
if (cond == NULL || mutex == NULL)
return 1;
return pthread_cond_timedwait(cond, mutex, NULL)
}
int pthread_cond_timedwait(thread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime)
{
if (cond == NULL || mutex == NULL)
return 1;
if (!SleepConditionVariableCS(cond, mutex, timespec_to_ms(abstime)))
return 1;
return 0;
}
int pthread_cond_signal(thread_cond_t *cond)
{
if (cond == NULL)
return 1;
WakeConditionVariable(cond);
return 0;
}
int pthread_cond_broadcast(thread_cond_t *cond)
{
if (cond == NULL)
return 1;
WakeAllConditionVariable(cond);
return 0;
}
In this case we’re mostly have a directly wrappers. The exception is
pthread_cond_timedwait
because the API takes a struct timespec
but the Windows function SleepConditionVariableCS
takes a DWORD
of milliseconds.
static DWORD timespec_to_ms(const struct timespec *abstime)
{
DWORD t;
if (abstime == NULL)
return INFINITE;
t = ((abstime->tv_sec - time(NULL)) * 1000) + (abstime->tv_nsec / 1000000);
if (t < 0)
t = 1;
return t;
}
Luckily conversion from struct timespec
to milliseconds is trivial
and low and behold we have a helper to do it for us.
Read Write Locks
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr)
{
(void)attr;
if (rwlock == NULL)
return 1;
InitializeSRWLock(&(rwlock->lock));
rwlock->exclusive = false;
return 0;
}
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
{
(void)rwlock;
}
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
{
if (rwlock == NULL)
return 1;
AcquireSRWLockShared(&(rwlock->lock));
}
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)
{
if (rwlock == NULL)
return 1;
return !TryAcquireSRWLockShared(&(rwlock->lock));
}
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
{
if (rwlock == NULL)
return 1;
AcquireSRWLockExclusive(&(rwlock->lock));
rwlock->exclusive = true;
}
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)
{
BOOLEAN ret;
if (rwlock == NULL)
return 1;
ret = TryAcquireSRWLockExclusive(&(rwlock->lock));
if (ret)
rwlock->exclusive = true;
return ret;
}
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
{
if (rwlock == NULL)
return 1;
if (rwlock->exclusive) {
rwlock->exclusive = false;
ReleaseSRWLockExclusive(&(rwlock->lock));
} else {
ReleaseSRWLockShared(&(rwlock->lock));
}
}
For the most part we’re using the SRWLock
within the
pthread_rwlock_t
we made. The exclusive
flag is
only used by the write lock functions so we know if
we’re in a write lock. This is solely to know which
unlock function to call.
exclusive
is a flag and not a count because we can only be in a write
lock if there are no read locks being held. Remember multiple readers can
hold the lock at the same time but only 1 writer can. The 1 writer blocks any
readers from getting the lock so we only have to track if we’re in a write lock.
In pthread_rwlock_unlock
we check the exclusive
flag before
we do anything with the lock and this is okay. The lock should already
be locked before unlock is called so we can assume nothing else
will come in and make changes while we’re reading the flag.
Number of Cores/Processors
#ifdef _WIN32
unsigned int pcthread_get_num_procs()
{
SYSTEM_INFO sysinfo;
GetSystemInfo(&sysinfo);
return sysinfo.dwNumberOfProcessors;
}
#else
#include <unistd.h>
unsigned int pcthread_get_num_procs()
{
return (unsigned int)sysconf(_SC_NPROCESSORS_ONLN);
}
#endif
This isn’t part of pthreads but sometimes it can be helpful to know the number of cores/processors on the system.
Time conversion
void ms_to_timespec(struct timespec *ts, unsigned int ms)
{
if (ts == NULL)
return;
ts->tv_sec = (ms / 1000) + time(NULL);
ts->tv_nsec = (ms % 1000) * 1000000
}
While not thread specific this helper if very nice for cond_timedwait
function.