calog/vendor/tcl/generic/tclMutexTest.c
2026-07-04 23:22:22 -05:00

573 lines
16 KiB
C
Vendored
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* tclMutexTest.c --
*
* This file implements the testmutex command.
*
* Copyright (c) 2025 Ashok P. Nadkarni. All rights reserved.
*
* See the file "license.terms" for information on usage and redistribution of
* this file, and for a DISCLAIMER OF ALL WARRANTIES.
*/
#undef BUILD_tcl
#undef STATIC_BUILD
#ifndef USE_TCL_STUBS
# define USE_TCL_STUBS
#endif
#include "tclInt.h"
#ifdef HAVE_UNISTD_H
# include <unistd.h>
# ifdef _POSIX_PRIORITY_SCHEDULING
# include <sched.h>
# endif
#endif
#if TCL_THREADS
/*
* Types related to Tcl_Mutex tests.
*/
TCL_DECLARE_MUTEX(testContextMutex)
static inline void
LockTestContext(
int numRecursions)
{
for (int j = 0; j < numRecursions; ++j) {
Tcl_MutexLock(&testContextMutex);
}
}
static inline void
UnlockTestContext(
int numRecursions)
{
for (int j = 0; j < numRecursions; ++j) {
Tcl_MutexUnlock(&testContextMutex);
}
}
/*
* ProducerConsumerContext is used in producer consumer tests to
* simulate a resource queue.
*/
typedef struct {
Tcl_Condition canEnqueue; /* Signal producer if queue not full */
Tcl_Condition canDequeue; /* Signal consumer if queue not empty */
unsigned long long totalEnqueued; /* Total enqueued so far */
unsigned long long totalDequeued; /* Total dequeued so far */
int available; /* Number of "resources" available */
int capacity; /* Max number allowed in queue */
} ProducerConsumerQueue;
/*
* MutexSharedContext holds context shared amongst all threads in a test.
* Should only be modified under testContextMutex lock unless only single
* thread has access.
*/
typedef struct {
int numThreads; /* Number of threads in test run */
int numRecursions; /* Number of mutex lock recursions */
int numIterations; /* Number of times each thread should loop */
int yield; /* Whether threads should yield when looping */
union {
unsigned long long counter; /* Used in lock tests */
ProducerConsumerQueue queue; /* Used in condition variable tests */
} u;
} MutexSharedContext;
/*
* MutexThreadContext holds context specific to each test thread. This
* is passed as the clientData argument to each test thread.
*/
typedef struct {
MutexSharedContext *sharedContextPtr; /* Pointer to shared context */
Tcl_ThreadId threadId; /* Only access in creator */
unsigned long long numOperations; /* Use is dependent on the test */
unsigned long long timeouts; /* Timeouts on condition variables */
} MutexThreadContext;
/* Used to track how many test threads running. Also used as trigger */
static volatile int mutexThreadCount;
static Tcl_ThreadCreateType CounterThreadProc(void *clientData);
static int TestMutexLock(Tcl_Interp *interp,
MutexSharedContext *contextPtr);
static int TestConditionVariable(Tcl_Interp *interp,
MutexSharedContext *contextPtr);
static Tcl_ThreadCreateType ConsumerThreadProc(void *clientData);
static Tcl_ThreadCreateType ProducerThreadProc(void *clientData);
static inline void
YieldToOtherThreads(void)
{
#if defined(_WIN32)
Sleep(0);
#elif defined(_POSIX_PRIORITY_SCHEDULING)
(void) sched_yield();
#else
volatile int i;
for (i = 0; i < 1000; ++i) {
/* Just some random delay */
}
#endif
}
#ifdef __cplusplus
extern "C" {
#endif
extern int Tcltest_Init(Tcl_Interp *interp);
#ifdef __cplusplus
}
#endif
// Get the difference (in microseconds) between two Tcl_GetTime() timestamps.
#define USEC_DIFF(before, after) \
(1000000 * ((after).sec - (before).sec) + ((after).usec - (before).usec))
/*
*----------------------------------------------------------------------
*
* TestMutexCmd --
*
* This procedure is invoked to process the "testmutex" Tcl command.
*
* testmutex counter ?numthreads? ?numrecursions? ?numiterations?
* testmutex conditionvariable ?numthreads? ?numrecursions? ?numiterations?
*
* Results:
* A standard Tcl result.
*
*----------------------------------------------------------------------
*/
static int
TestMutexObjCmd(
TCL_UNUSED(void *),
Tcl_Interp *interp, /* Current interpreter. */
int objc, /* Number of arguments. */
Tcl_Obj *const objv[]) /* Argument objects. */
{
static const char *const mutexOptions[] = {
"lock", "condition", NULL
};
enum options {
LOCK, CONDITION
} option;
MutexSharedContext context = {
2, /* numThreads */
1, /* numRecursions */
1000000, /* numIterations */
1, /* yield */
{0}, /* u.counter */
};
if (objc < 2 || objc > 6) {
Tcl_WrongNumArgs(interp, 1, objv,
"option ?numthreads? ?numrecursions? ?numiterations? ?yield?");
return TCL_ERROR;
}
if (Tcl_GetIndexFromObj(interp, objv[1], mutexOptions, "option", 0,
&option) != TCL_OK) {
return TCL_ERROR;
}
if (objc > 2 && Tcl_GetIntFromObj(interp, objv[2],
&context.numThreads) != TCL_OK) {
return TCL_ERROR;
}
if (objc > 3 && Tcl_GetIntFromObj(interp, objv[3],
&context.numRecursions) != TCL_OK) {
return TCL_ERROR;
}
if (objc > 4 && Tcl_GetIntFromObj(interp, objv[4],
&context.numIterations) != TCL_OK) {
return TCL_ERROR;
}
if (objc > 5 && Tcl_GetIntFromObj(interp, objv[5],
&context.yield) != TCL_OK) {
return TCL_ERROR;
}
if (context.numIterations <= 0 || context.numRecursions <= 0 ||
context.numThreads <= 0) {
Tcl_SetResult(interp,
"thread, recursion and iteration counts must be positive",
TCL_STATIC);
return TCL_ERROR;
}
int result = TCL_OK;
switch (option) {
case LOCK:
result = TestMutexLock(interp, &context);
break;
case CONDITION:
result = TestConditionVariable(interp, &context);
break;
}
return result;
}
/*
*------------------------------------------------------------------------
*
* TestMutexLock --
*
* Implements the "testmutex lock" command to test Tcl_MutexLock.
*
* Results:
* A Tcl result code.
*
* Side effects:
* Stores a result in the interpreter.
*
*------------------------------------------------------------------------
*/
static int
TestMutexLock(
Tcl_Interp *interp,
MutexSharedContext *contextPtr)
{
MutexThreadContext *threadContextsPtr = (MutexThreadContext *)
Tcl_Alloc(sizeof(*threadContextsPtr) * contextPtr->numThreads);
contextPtr->u.counter = 0;
mutexThreadCount = 0;
for (int i = 0; i < contextPtr->numThreads; i++) {
threadContextsPtr[i].sharedContextPtr = contextPtr;
threadContextsPtr[i].numOperations = 0; /* Init though not used */
if (Tcl_CreateThread(&threadContextsPtr[i].threadId,
CounterThreadProc, &threadContextsPtr[i],
TCL_THREAD_STACK_DEFAULT, TCL_THREAD_JOINABLE) != TCL_OK) {
Tcl_Panic("Failed to create %d'th thread\n", i);
}
}
mutexThreadCount = contextPtr->numThreads; /* Will fire off all test threads */
/* Wait for all threads */
for (int i = 0; i < contextPtr->numThreads; i++) {
int threadResult;
Tcl_JoinThread(threadContextsPtr[i].threadId, &threadResult);
}
Tcl_Free(threadContextsPtr);
Tcl_SetObjResult(interp, Tcl_NewWideUIntObj(contextPtr->u.counter));
return TCL_OK;
}
/*
*------------------------------------------------------------------------
*
* CounterThreadProc --
*
* Increments a shared counter a specified number of times and exits
* the thread.
*
* Results:
* None.
*
* Side effects:
* None.
*
*------------------------------------------------------------------------
*/
static Tcl_ThreadCreateType
CounterThreadProc(
void *clientData)
{
MutexThreadContext *threadContextPtr = (MutexThreadContext *)clientData;
MutexSharedContext *contextPtr = threadContextPtr->sharedContextPtr;
/* Spin wait until given the run signal */
while (mutexThreadCount < contextPtr->numThreads) {
YieldToOtherThreads();
}
for (int i = 0; i < contextPtr->numIterations; i++) {
LockTestContext(contextPtr->numRecursions);
unsigned long long temp = contextPtr->u.counter;
if (contextPtr->yield) {
/* Some delay. No one else is supposed to modify the counter */
YieldToOtherThreads();
}
contextPtr->u.counter = temp + 1; /* Increment original value read */
UnlockTestContext(contextPtr->numRecursions);
}
Tcl_ExitThread(0);
TCL_THREAD_CREATE_RETURN;
}
/*
*------------------------------------------------------------------------
*
* TestConditionVariable --
*
* Implements the "testmutex condition" command to test Tcl_Condition*.
* The test emulates a producer-consumer scenario.
*
* Results:
* A Tcl result code.
*
* Side effects:
* Stores a result in the interpreter.
*
*------------------------------------------------------------------------
*/
static int
TestConditionVariable(
Tcl_Interp *interp,
MutexSharedContext *contextPtr)
{
if (contextPtr->numThreads < 2) {
Tcl_SetResult(interp, "Need at least 2 threads.", TCL_STATIC);
return TCL_ERROR;
}
int numProducers = contextPtr->numThreads / 2;
int numConsumers = contextPtr->numThreads - numProducers;
contextPtr->u.queue.canDequeue = NULL;
contextPtr->u.queue.canEnqueue = NULL;
/*
* available tracks how many elements in the virtual queue
* capacity is max length of virtual queue.
*/
contextPtr->u.queue.totalEnqueued = 0;
contextPtr->u.queue.totalDequeued = 0;
contextPtr->u.queue.available = 0;
contextPtr->u.queue.capacity = 3; /* Arbitrary for now */
MutexThreadContext *consumerContextsPtr = (MutexThreadContext *)Tcl_Alloc(
sizeof(*consumerContextsPtr) * numConsumers);
MutexThreadContext *producerContextsPtr = (MutexThreadContext *)Tcl_Alloc(
sizeof(*producerContextsPtr) * numProducers);
mutexThreadCount = 0;
for (int i = 0; i < numConsumers; i++) {
consumerContextsPtr[i].sharedContextPtr = contextPtr;
consumerContextsPtr[i].numOperations = 0;
consumerContextsPtr[i].timeouts = 0;
if (Tcl_CreateThread(&consumerContextsPtr[i].threadId,
ConsumerThreadProc, &consumerContextsPtr[i],
TCL_THREAD_STACK_DEFAULT, TCL_THREAD_JOINABLE) != TCL_OK) {
Tcl_Panic("Failed to create %d'th thread\n", (int) i);
}
}
for (int i = 0; i < numProducers; i++) {
producerContextsPtr[i].sharedContextPtr = contextPtr;
producerContextsPtr[i].numOperations = 0;
producerContextsPtr[i].timeouts = 0;
if (Tcl_CreateThread(&producerContextsPtr[i].threadId,
ProducerThreadProc, &producerContextsPtr[i],
TCL_THREAD_STACK_DEFAULT, TCL_THREAD_JOINABLE) != TCL_OK) {
Tcl_Panic("Failed to create %d'th thread\n", (int) i);
}
}
mutexThreadCount = contextPtr->numThreads; /* Will trigger all threads */
/* Producer total, thread, timeouts, Consumer total, thread, timeouts */
Tcl_Obj *results[6];
results[1] = Tcl_NewListObj(numProducers, NULL);
results[4] = Tcl_NewListObj(numConsumers, NULL);
unsigned long long producerTimeouts = 0;
unsigned long long producerOperations = 0;
unsigned long long consumerTimeouts = 0;
unsigned long long consumerOperations = 0;
for (int i = 0; i < numProducers; i++) {
int threadResult;
Tcl_JoinThread(producerContextsPtr[i].threadId, &threadResult);
producerOperations += producerContextsPtr[i].numOperations;
Tcl_ListObjAppendElement(NULL, results[1],
Tcl_NewWideUIntObj(producerContextsPtr[i].numOperations));
producerTimeouts += producerContextsPtr[i].timeouts;
}
for (int i = 0; i < numConsumers; i++) {
int threadResult;
Tcl_JoinThread(consumerContextsPtr[i].threadId, &threadResult);
consumerOperations += consumerContextsPtr[i].numOperations;
Tcl_ListObjAppendElement(NULL, results[4],
Tcl_NewWideUIntObj(consumerContextsPtr[i].numOperations));
consumerTimeouts += consumerContextsPtr[i].timeouts;
}
results[0] = Tcl_NewWideUIntObj(producerOperations);
results[2] = Tcl_NewWideUIntObj(producerTimeouts);
results[3] = Tcl_NewWideUIntObj(consumerOperations);
results[5] = Tcl_NewWideUIntObj(consumerTimeouts);
Tcl_SetObjResult(interp, Tcl_NewListObj(6, results));
Tcl_Free(producerContextsPtr);
Tcl_Free(consumerContextsPtr);
Tcl_ConditionFinalize(&contextPtr->u.queue.canDequeue);
Tcl_ConditionFinalize(&contextPtr->u.queue.canEnqueue);
return TCL_OK;
}
/*
*------------------------------------------------------------------------
*
* ProducerThreadProc --
*
* Acts as a "producer" that enqueues to the virtual resource queue.
*
* Results:
* None.
*
* Side effects:
* None.
*
*------------------------------------------------------------------------
*/
static const Tcl_Time CONDITION_TIMEOUT = {5, 0};
static Tcl_ThreadCreateType
ProducerThreadProc(
void *clientData)
{
MutexThreadContext *threadContextPtr = (MutexThreadContext *)clientData;
MutexSharedContext *contextPtr = threadContextPtr->sharedContextPtr;
/* Limit on total number of operations across all threads */
unsigned long long limit;
limit = contextPtr->numThreads * (unsigned long long) contextPtr->numIterations;
/* Spin wait until given the run signal */
while (mutexThreadCount < contextPtr->numThreads) {
YieldToOtherThreads();
}
LockTestContext(contextPtr->numRecursions);
while (contextPtr->u.queue.totalEnqueued < limit) {
if (contextPtr->u.queue.available == contextPtr->u.queue.capacity) {
Tcl_Time before, after;
Tcl_GetTime(&before);
Tcl_ConditionWait(&contextPtr->u.queue.canEnqueue,
&testContextMutex, &CONDITION_TIMEOUT);
Tcl_GetTime(&after);
if (USEC_DIFF(before, after) >= 1000000 * CONDITION_TIMEOUT.sec) {
threadContextPtr->timeouts += 1;
}
} else {
contextPtr->u.queue.available += 1; /* Enqueue operation */
contextPtr->u.queue.totalEnqueued += 1;
threadContextPtr->numOperations += 1;
Tcl_ConditionNotify(&contextPtr->u.queue.canDequeue);
if (contextPtr->yield) {
/* Simulate real work by unlocking before yielding */
UnlockTestContext(contextPtr->numRecursions);
YieldToOtherThreads();
LockTestContext(contextPtr->numRecursions);
}
}
}
UnlockTestContext(contextPtr->numRecursions);
Tcl_ExitThread(0);
TCL_THREAD_CREATE_RETURN;
}
/*
*------------------------------------------------------------------------
*
* ConsumerThreadProc --
*
* Acts as a "consumer" that dequeues from the virtual resource queue.
*
* Results:
* None.
*
* Side effects:
* None.
*
*------------------------------------------------------------------------
*/
static Tcl_ThreadCreateType
ConsumerThreadProc(
void *clientData)
{
MutexThreadContext *threadContextPtr = (MutexThreadContext *)clientData;
MutexSharedContext *contextPtr = threadContextPtr->sharedContextPtr;
/* Limit on total number of operations across all threads */
unsigned long long limit;
limit = contextPtr->numThreads * (unsigned long long) contextPtr->numIterations;
/* Spin wait until given the run signal */
while (mutexThreadCount < contextPtr->numThreads) {
YieldToOtherThreads();
}
LockTestContext(contextPtr->numRecursions);
while (contextPtr->u.queue.totalDequeued < limit) {
if (contextPtr->u.queue.available == 0) {
Tcl_Time before, after;
Tcl_GetTime(&before);
Tcl_ConditionWait(&contextPtr->u.queue.canDequeue,
&testContextMutex, &CONDITION_TIMEOUT);
Tcl_GetTime(&after);
if (USEC_DIFF(before, after) >= 1000000 * CONDITION_TIMEOUT.sec) {
threadContextPtr->timeouts += 1;
}
} else {
contextPtr->u.queue.totalDequeued += 1;
threadContextPtr->numOperations += 1;
contextPtr->u.queue.available -= 1;
Tcl_ConditionNotify(&contextPtr->u.queue.canEnqueue);
if (contextPtr->yield) {
/* Simulate real work by unlocking before yielding */
UnlockTestContext(contextPtr->numRecursions);
YieldToOtherThreads();
LockTestContext(contextPtr->numRecursions);
}
}
}
UnlockTestContext(contextPtr->numRecursions);
Tcl_ExitThread(0);
TCL_THREAD_CREATE_RETURN;
}
/*
*----------------------------------------------------------------------
*
* TclMutex_Init --
*
* Initialize the testmutex command.
*
* Results:
* TCL_OK if the package was properly initialized.
*
* Side effects:
* Add the "testmutex" command to the interp.
*
*----------------------------------------------------------------------
*/
int
TclMutex_Init(
Tcl_Interp *interp) /* The current Tcl interpreter */
{
Tcl_CreateObjCommand(interp, "testmutex", TestMutexObjCmd, NULL, NULL);
return TCL_OK;
}
#endif /* TCL_THREADS */
/*
* Local Variables:
* mode: c
* c-basic-offset: 4
* fill-column: 78
* End:
*/