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

#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.