#include <libkern/OSAtomic.h>
#include <dispatch/dispatch.h>
-#include <darwintest.h>
+#include "darwintest_defaults.h"
#define NUM_THREADS 8
usleep((useconds_t) uexpected);
bool loop = true;
while (loop) {
- T_ASSERT_POSIX_ZERO(pthread_mutex_lock(&context.mutex),
+ T_QUIET; T_ASSERT_POSIX_ZERO(pthread_mutex_lock(&context.mutex),
"pthread_mutex_lock");
if (context.count <= 0) {
loop = false;
}
- T_ASSERT_POSIX_ZERO(pthread_mutex_unlock(&context.mutex),
+ T_QUIET; T_ASSERT_POSIX_ZERO(pthread_mutex_unlock(&context.mutex),
"pthread_mutex_unlock");
}
((uint64_t) start.tv_sec * USEC_PER_SEC + (uint64_t) start.tv_usec);
T_LOG("waittime actual: %llu us", uelapsed);
}
+
+struct prodcons_context {
+ pthread_cond_t cond;
+ pthread_mutex_t mutex;
+ bool consumer_ready;
+ bool workitem_available;
+ bool padding[6];
+};
+
+static void *consumer_thread(void *ptr) {
+ struct prodcons_context *context = ptr;
+
+ // tell producer thread that we are ready
+ T_ASSERT_POSIX_ZERO(pthread_mutex_lock(&context->mutex), "pthread_mutex_lock");
+
+ context->consumer_ready = true;
+ T_ASSERT_POSIX_ZERO(pthread_cond_signal(&context->cond), "pthread_cond_signal");
+
+ // wait for a work item to become available
+ do {
+ // mutex will be dropped and allow producer thread to acquire
+ T_ASSERT_POSIX_ZERO(pthread_cond_wait(&context->cond, &context->mutex), "pthread_cond_wait");
+
+ // loop in case of spurious wakeups
+ } while (context->workitem_available == false);
+
+ // work item has been sent, so dequeue it and tell producer
+ context->workitem_available = false;
+ T_ASSERT_POSIX_ZERO(pthread_cond_signal(&context->cond), "pthread_cond_signal");
+
+ // unlock mutex, we are done here
+ T_ASSERT_POSIX_ZERO(pthread_mutex_unlock(&context->mutex), "pthread_mutex_unlock");
+
+ T_PASS("Consumer thread exiting");
+
+ return NULL;
+}
+
+#define TESTCASE_TIMEOUT (10) /* seconds */
+typedef enum {
+ eNullTimeout,
+ eZeroTimeout,
+ eBeforeEpochTimeout,
+ eRecentPastTimeout
+} TimeOutType;
+
+static DT_TEST_RETURN cond_timedwait_timeouts_internal(TimeOutType timeout, bool relative);
+
+T_DECL(cond_timedwait_nulltimeout, "pthread_cond_timedwait() with NULL timeout, ensure mutex is unlocked")
+{
+ cond_timedwait_timeouts_internal(eNullTimeout, false);
+}
+
+T_DECL(cond_timedwait_zerotimeout, "pthread_cond_timedwait() with zero timeout, ensure mutex is unlocked")
+{
+ cond_timedwait_timeouts_internal(eZeroTimeout, false);
+}
+
+T_DECL(cond_timedwait_beforeepochtimeout, "pthread_cond_timedwait() with timeout before the epoch, ensure mutex is unlocked")
+{
+ cond_timedwait_timeouts_internal(eBeforeEpochTimeout, false);
+}
+
+T_DECL(cond_timedwait_pasttimeout, "pthread_cond_timedwait() with timeout in the past, ensure mutex is unlocked")
+{
+ cond_timedwait_timeouts_internal(eRecentPastTimeout, false);
+}
+
+T_DECL(cond_timedwait_relative_nulltimeout, "pthread_cond_timedwait_relative_np() with relative NULL timeout, ensure mutex is unlocked")
+{
+ cond_timedwait_timeouts_internal(eNullTimeout, true);
+}
+
+T_DECL(cond_timedwait_relative_pasttimeout, "pthread_cond_timedwait_relative_np() with relative timeout in the past, ensure mutex is unlocked")
+{
+ cond_timedwait_timeouts_internal(eRecentPastTimeout, true);
+}
+
+static DT_TEST_RETURN cond_timedwait_timeouts_internal(TimeOutType timeout, bool relative)
+{
+ // This testcase mimics a producer-consumer model where the consumer checks
+ // in and waits until work becomes available. The producer then waits until
+ // the work has been consumed and the consumer quiesces. Since condition
+ // variables may have spurious wakeups, the timeout should not matter,
+ // but there have been functional issues where the mutex would not be unlocked
+ // for a timeout in the past.
+ struct prodcons_context context = {
+ .cond = PTHREAD_COND_INITIALIZER,
+ .mutex = PTHREAD_MUTEX_INITIALIZER,
+ .consumer_ready = false,
+ .workitem_available = false
+ };
+
+ struct timeval test_timeout;
+ gettimeofday(&test_timeout, NULL);
+ test_timeout.tv_sec += TESTCASE_TIMEOUT;
+
+ T_ASSERT_POSIX_ZERO(pthread_mutex_lock(&context.mutex), "pthread_mutex_lock");
+
+ pthread_t p;
+ T_ASSERT_POSIX_ZERO(pthread_create(&p, NULL, consumer_thread, &context),
+ "pthread_create");
+
+ // Wait until consumer thread is able to acquire the mutex, check in, and block
+ // in its own condition variable. We do not want to start generating work before
+ // the consumer thread is available
+ do {
+ // mutex will be dropped and allow consumer thread to acquire
+ T_ASSERT_POSIX_ZERO(pthread_cond_wait(&context.cond, &context.mutex), "pthread_cond_wait");
+
+ // loop in case of spurious wakeups
+ } while (context.consumer_ready == false);
+
+ // consumer is ready and blocked in its own condition variable, and
+ // producer has mutex acquired. Send a work item and wait for it
+ // to be dequeued
+
+ context.workitem_available = true;
+ T_ASSERT_POSIX_ZERO(pthread_cond_signal(&context.cond), "pthread_cond_signal");
+
+ do {
+ struct timeval now;
+
+ gettimeofday(&now, NULL);
+ T_QUIET; T_ASSERT_TRUE(timercmp(&now, &test_timeout, <), "timeout reached waiting for consumer thread to consume");
+
+ struct timespec ts;
+
+ if (relative) {
+ switch (timeout) {
+ case eNullTimeout:
+ break;
+ case eRecentPastTimeout:
+ ts.tv_sec = -1;
+ ts.tv_nsec = 0;
+ break;
+ case eZeroTimeout:
+ case eBeforeEpochTimeout:
+ break;
+ }
+ } else {
+ switch (timeout) {
+ case eNullTimeout:
+ break;
+ case eZeroTimeout:
+ ts.tv_sec = 0;
+ ts.tv_nsec = 0;
+ break;
+ case eBeforeEpochTimeout:
+ ts.tv_sec = -1;
+ ts.tv_nsec = 0;
+ break;
+ case eRecentPastTimeout:
+ ts.tv_sec = now.tv_sec - 1;
+ ts.tv_nsec = now.tv_usec / 1000;
+ break;
+ }
+ }
+
+ int ret;
+ if (relative) {
+ ret = pthread_cond_timedwait_relative_np(&context.cond, &context.mutex, timeout == eNullTimeout ? NULL : &ts);
+ } else {
+ ret = pthread_cond_timedwait(&context.cond, &context.mutex, timeout == eNullTimeout ? NULL : &ts);
+ }
+ if (ret != 0 && ret != EINTR && ret != ETIMEDOUT) T_ASSERT_POSIX_ZERO(ret, "timedwait returned error");
+
+ usleep(10*1000); // avoid spinning in a CPU-bound loop
+
+ // loop in case of spurious wakeups
+ } while (context.workitem_available == true);
+
+ T_ASSERT_POSIX_ZERO(pthread_mutex_unlock(&context.mutex), "pthread_mutex_unlock");
+
+ T_ASSERT_POSIX_ZERO(pthread_join(p, NULL), "pthread_join");
+
+ T_PASS("Consumer completed work");
+}