libwinpr-sync: New complete critical section code

- Complete implementation including recursion support
- Added an intensive ctest (TestSynchCritical)
- Struct members are used exactly as Windows does it internally:
  LockCount starts at -1, RecursionCount at 0
- Same performance optimizations as internally on Windows:
    - Fast lock acquisition path using CAS -> SpinCount -> wait
    - SpinCount automatically disabled on uniprocessor systems
- On Linux SpinCount is disabled because it provided no advantage over NPTL/futex in all tests

Support for CRITICAL_SECTION's DebugInfo is not yet included (but trivial to add).
This commit is contained in:
Norbert Federa 2013-08-07 10:20:04 +02:00
parent 21796ad73d
commit 2b25b4a520
7 changed files with 539 additions and 44 deletions

View File

@ -139,14 +139,22 @@ typedef RTL_CONDITION_VARIABLE CONDITION_VARIABLE, *PCONDITION_VARIABLE;
/* Critical Section */
#if defined(__linux__)
/**
* Linux NPTL thread synchronization primitives are implemented using
* the futex system calls ... we can't beat futex with a spin loop.
*/
#define WINPR_CRITICAL_SECTION_DISABLE_SPINCOUNT
#endif
typedef struct _RTL_CRITICAL_SECTION
{
void* DebugInfo;
PVOID DebugInfo;
LONG LockCount;
LONG RecursionCount;
PVOID OwningThread;
PVOID LockSemaphore;
ULONG SpinCount;
HANDLE OwningThread;
HANDLE LockSemaphore;
ULONG_PTR SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;

View File

@ -34,7 +34,7 @@ set_target_properties(${MODULE_NAME} PROPERTIES VERSION ${WINPR_VERSION_FULL} SO
set_complex_link_libraries(VARIABLE ${MODULE_PREFIX}_LIBS
MONOLITHIC ${MONOLITHIC_BUILD} INTERNAL
MODULE winpr
MODULES winpr-crt winpr-handle winpr-synch)
MODULES winpr-crt winpr-handle)
if(MONOLITHIC_BUILD)

View File

@ -53,7 +53,7 @@ endif()
set_complex_link_libraries(VARIABLE ${MODULE_PREFIX}_LIBS
MONOLITHIC ${MONOLITHIC_BUILD} INTERNAL
MODULE winpr
MODULES winpr-handle)
MODULES winpr-handle winpr-interlocked winpr-thread)
if(MONOLITHIC_BUILD)
set(WINPR_LIBS ${WINPR_LIBS} ${${MODULE_PREFIX}_LIBS} PARENT_SCOPE)
@ -64,3 +64,6 @@ endif()
set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "WinPR")
if(BUILD_TESTING)
add_subdirectory(test)
endif()

View File

@ -3,6 +3,7 @@
* Synchronization Functions
*
* Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
* Copyright 2013 Norbert Federa <norbert.federa@thinstuff.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,6 +23,9 @@
#endif
#include <winpr/synch.h>
#include <winpr/sysinfo.h>
#include <winpr/interlocked.h>
#include <winpr/thread.h>
#include "synch.h"
@ -31,84 +35,201 @@
#ifndef _WIN32
/**
* TODO: EnterCriticalSection allows recursive calls from the same thread.
* Implement this using pthreads (see PTHREAD_MUTEX_RECURSIVE_NP)
* http://msdn.microsoft.com/en-us/library/windows/desktop/ms682608%28v=vs.85%29.aspx
*/
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
{
if (lpCriticalSection)
{
lpCriticalSection->DebugInfo = NULL;
lpCriticalSection->LockCount = 0;
lpCriticalSection->RecursionCount = 0;
lpCriticalSection->SpinCount = 0;
lpCriticalSection->OwningThread = NULL;
lpCriticalSection->LockSemaphore = NULL;
lpCriticalSection->LockSemaphore = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(lpCriticalSection->LockSemaphore, NULL);
}
InitializeCriticalSectionEx(lpCriticalSection, 0, 0);
}
BOOL InitializeCriticalSectionEx(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount, DWORD Flags)
{
/**
* See http://msdn.microsoft.com/en-us/library/ff541979(v=vs.85).aspx
* - The LockCount field indicates the number of times that any thread has
* called the EnterCriticalSection routine for this critical section,
* minus one. This field starts at -1 for an unlocked critical section.
* Each call of EnterCriticalSection increments this value; each call of
* LeaveCriticalSection decrements it.
* - The RecursionCount field indicates the number of times that the owning
* thread has called EnterCriticalSection for this critical section.
*/
if (Flags != 0) {
fprintf(stderr, "warning: InitializeCriticalSectionEx Flags unimplemented\n");
}
return InitializeCriticalSectionAndSpinCount(lpCriticalSection, dwSpinCount);
lpCriticalSection->DebugInfo = NULL;
lpCriticalSection->LockCount = -1;
lpCriticalSection->SpinCount = 0;
lpCriticalSection->RecursionCount = 0;
lpCriticalSection->OwningThread = NULL;
lpCriticalSection->LockSemaphore = (winpr_sem_t*) malloc(sizeof(winpr_sem_t));
#if defined(__APPLE__)
semaphore_create(mach_task_self(), lpCriticalSection->LockSemaphore, SYNC_POLICY_FIFO, 0);
#else
sem_init(lpCriticalSection->LockSemaphore, 0, 0);
#endif
SetCriticalSectionSpinCount(lpCriticalSection, dwSpinCount);
return TRUE;
}
BOOL InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount)
{
InitializeCriticalSection(lpCriticalSection);
SetCriticalSectionSpinCount(lpCriticalSection, dwSpinCount);
return TRUE;
return InitializeCriticalSectionEx(lpCriticalSection, dwSpinCount, 0);
}
DWORD SetCriticalSectionSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount)
{
#if !defined(WINPR_CRITICAL_SECTION_DISABLE_SPINCOUNT)
SYSTEM_INFO sysinfo;
DWORD dwPreviousSpinCount = lpCriticalSection->SpinCount;
if (dwSpinCount)
{
/* Don't spin on uniprocessor systems! */
GetNativeSystemInfo(&sysinfo);
if (sysinfo.dwNumberOfProcessors < 2)
dwSpinCount = 0;
}
lpCriticalSection->SpinCount = dwSpinCount;
return dwPreviousSpinCount;
#else
return 0;
#endif
}
VOID _WaitForCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
{
#if defined(__APPLE__)
semaphore_wait(*((winpr_sem_t*) lpCriticalSection->LockSemaphore));
#else
sem_wait((winpr_sem_t*) lpCriticalSection->LockSemaphore);
#endif
}
VOID _UnWaitCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
{
#if defined __APPLE__
semaphore_signal(*((winpr_sem_t*) lpCriticalSection->LockSemaphore));
#else
sem_post((winpr_sem_t*) lpCriticalSection->LockSemaphore);
#endif
}
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
{
/**
* Linux NPTL thread synchronization primitives are implemented using
* the futex system calls ... no need for performing a trylock loop.
*/
#if !defined(__linux__)
ULONG spin = lpCriticalSection->SpinCount;
while (spin--)
#if !defined(WINPR_CRITICAL_SECTION_DISABLE_SPINCOUNT)
ULONG SpinCount = lpCriticalSection->SpinCount;
/* If we're lucky or if the current thread is already owner we can return early */
if (SpinCount && TryEnterCriticalSection(lpCriticalSection))
return;
/* Spin requested times but don't compete with another waiting thread */
while (SpinCount-- && lpCriticalSection->LockCount < 1)
{
if (pthread_mutex_trylock((pthread_mutex_t*)lpCriticalSection->LockSemaphore) == 0)
/* Atomically try to acquire and check the if the section is free. */
if (InterlockedCompareExchange(&lpCriticalSection->LockCount, 0, -1) == -1)
{
lpCriticalSection->RecursionCount = 1;
lpCriticalSection->OwningThread = (HANDLE)GetCurrentThreadId();
return;
pthread_yield();
}
/* Failed to get the lock. Let the scheduler know that we're spinning. */
if (sched_yield()!=0)
{
/**
* On some operating systems sched_yield is a stub.
* usleep should at least trigger a context switch if any thread is waiting.
* A ThreadYield() would be nice in winpr ...
*/
usleep(1);
}
}
#endif
pthread_mutex_lock((pthread_mutex_t*)lpCriticalSection->LockSemaphore);
/* First try the fastest posssible path to get the lock. */
if (InterlockedIncrement(&lpCriticalSection->LockCount))
{
/* Section is already locked. Check if it is owned by the current thread. */
if (lpCriticalSection->OwningThread == (HANDLE)GetCurrentThreadId())
{
/* Recursion. No need to wait. */
lpCriticalSection->RecursionCount++;
return;
}
/* Section is locked by another thread. We have to wait. */
_WaitForCriticalSection(lpCriticalSection);
}
/* We got the lock. Own it ... */
lpCriticalSection->RecursionCount = 1;
lpCriticalSection->OwningThread = (HANDLE)GetCurrentThreadId();
}
BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
{
return (pthread_mutex_trylock((pthread_mutex_t*)lpCriticalSection->LockSemaphore) == 0 ? TRUE : FALSE);
HANDLE current_thread = (HANDLE)GetCurrentThreadId();
/* Atomically acquire the the lock if the section is free. */
if (InterlockedCompareExchange(&lpCriticalSection->LockCount, 0, -1 ) == -1)
{
lpCriticalSection->RecursionCount = 1;
lpCriticalSection->OwningThread = current_thread;
return TRUE;
}
/* Section is already locked. Check if it is owned by the current thread. */
if (lpCriticalSection->OwningThread == current_thread)
{
/* Recursion, return success */
lpCriticalSection->RecursionCount++;
InterlockedIncrement(&lpCriticalSection->LockCount);
return TRUE;
}
return FALSE;
}
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
{
pthread_mutex_unlock((pthread_mutex_t*)lpCriticalSection->LockSemaphore);
/* Decrement RecursionCount and check if this is the last LeaveCriticalSection call ...*/
if (--lpCriticalSection->RecursionCount < 1)
{
/* Last recursion, clear owner, unlock and if there are other waiting threads ... */
lpCriticalSection->OwningThread = NULL;
if (InterlockedDecrement(&lpCriticalSection->LockCount) >= 0)
{
/* ...signal the semaphore to unblock the next waiting thread */
_UnWaitCriticalSection(lpCriticalSection);
}
}
else
{
InterlockedDecrement(&lpCriticalSection->LockCount);
}
}
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
{
pthread_mutex_destroy((pthread_mutex_t*)lpCriticalSection->LockSemaphore);
free(lpCriticalSection->LockSemaphore);
lpCriticalSection->LockCount = -1;
lpCriticalSection->SpinCount = 0;
lpCriticalSection->RecursionCount = 0;
lpCriticalSection->OwningThread = NULL;
if (lpCriticalSection->LockSemaphore != NULL)
{
#if defined __APPLE__
semaphore_destroy(mach_task_self(), *((winpr_sem_t*) lpCriticalSection->LockSemaphore));
#else
sem_destroy((winpr_sem_t*) lpCriticalSection->LockSemaphore);
#endif
free(lpCriticalSection->LockSemaphore);
lpCriticalSection->LockSemaphore = NULL;
}
}
#endif

3
winpr/libwinpr/synch/test/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
TestSynch
TestSynch.c

View File

@ -0,0 +1,31 @@
set(MODULE_NAME "TestSynch")
set(MODULE_PREFIX "TEST_SYNCH")
set(${MODULE_PREFIX}_DRIVER ${MODULE_NAME}.c)
set(${MODULE_PREFIX}_TESTS
TestSynchCritical.c)
create_test_sourcelist(${MODULE_PREFIX}_SRCS
${${MODULE_PREFIX}_DRIVER}
${${MODULE_PREFIX}_TESTS})
add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS})
set_complex_link_libraries(VARIABLE ${MODULE_PREFIX}_LIBS
MONOLITHIC ${MONOLITHIC_BUILD}
MODULE winpr
MODULES winpr-synch winpr-sysinfo)
target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS})
set_target_properties(${MODULE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${TESTING_OUTPUT_DIRECTORY}")
foreach(test ${${MODULE_PREFIX}_TESTS})
get_filename_component(TestName ${test} NAME_WE)
add_test(${TestName} ${TESTING_OUTPUT_DIRECTORY}/${MODULE_NAME} ${TestName})
endforeach()
set_property(TARGET ${MODULE_NAME} PROPERTY FOLDER "WinPR/Test")

View File

@ -0,0 +1,329 @@
#include <stdio.h>
#include <winpr/crt.h>
#include <winpr/windows.h>
#include <winpr/synch.h>
#include <winpr/sysinfo.h>
#include <winpr/thread.h>
#include <winpr/interlocked.h>
#define TEST_SYNC_CRITICAL_TEST1_RUNTIME_MS 500
#define TEST_SYNC_CRITICAL_TEST1_RUNS 4
CRITICAL_SECTION critical;
LONG gTestValueVulnerable = 0;
LONG gTestValueSerialized = 0;
BOOL TestSynchCritical_TriggerAndCheckRaceCondition(HANDLE OwningThread, LONG RecursionCount)
{
/* if called unprotected this will hopefully trigger a race condition ... */
gTestValueVulnerable++;
if (critical.OwningThread != OwningThread)
{
printf("CriticalSection failure: OwningThread is invalid\n");
return FALSE;
}
if (critical.RecursionCount != RecursionCount)
{
printf("CriticalSection failure: RecursionCount is invalid\n");
return FALSE;
}
/* ... which we try to detect using the serialized counter */
if (gTestValueVulnerable != InterlockedIncrement(&gTestValueSerialized))
{
printf("CriticalSection failure: Data corruption detected\n");
return FALSE;
}
return TRUE;
}
/* this thread function shall increment the global dwTestValue until the PBOOL passsed in arg is FALSE */
static PVOID TestSynchCritical_Test1(PVOID arg)
{
int i, j, rc;
HANDLE hThread = (HANDLE)GetCurrentThreadId();
PBOOL pbContinueRunning = (PBOOL)arg;
while(*pbContinueRunning)
{
EnterCriticalSection(&critical);
rc = 1;
if (!TestSynchCritical_TriggerAndCheckRaceCondition(hThread, rc))
return (PVOID)1;
/* add some random recursion level */
j = rand()%5;
for (i=0; i<j; i++)
{
if (!TestSynchCritical_TriggerAndCheckRaceCondition(hThread, rc++))
return (PVOID)2;
EnterCriticalSection(&critical);
}
for (i=0; i<j; i++)
{
if (!TestSynchCritical_TriggerAndCheckRaceCondition(hThread, rc--))
return (PVOID)2;
LeaveCriticalSection(&critical);
}
if (!TestSynchCritical_TriggerAndCheckRaceCondition(hThread, rc))
return (PVOID)3;
LeaveCriticalSection(&critical);
}
return 0;
}
/* this thread function tries to call TryEnterCriticalSection while the main thread holds the lock */
static PVOID TestSynchCritical_Test2(PVOID arg)
{
if (TryEnterCriticalSection(&critical)==TRUE)
{
LeaveCriticalSection(&critical);
return (PVOID)1;
}
return (PVOID)0;
}
static PVOID TestSynchCritical_Main(PVOID arg)
{
int i, j;
SYSTEM_INFO sysinfo;
DWORD dwPreviousSpinCount;
DWORD dwSpinCount;
DWORD dwSpinCountExpected;
HANDLE hMainThread;
HANDLE* hThreads;
HANDLE hThread;
DWORD dwThreadCount;
DWORD dwThreadExitCode;
BOOL bTest1Running;
PBOOL pbThreadTerminated = (PBOOL)arg;
GetNativeSystemInfo(&sysinfo);
hMainThread = (HANDLE)GetCurrentThreadId();
/**
* Test SpinCount in SetCriticalSectionSpinCount, InitializeCriticalSectionEx and InitializeCriticalSectionAndSpinCount
* SpinCount must be forced to be zero on on uniprocessor systems and on systems
* where WINPR_CRITICAL_SECTION_DISABLE_SPINCOUNT is defined
*/
dwSpinCount = 100;
InitializeCriticalSectionEx(&critical, dwSpinCount, 0);
while(--dwSpinCount)
{
dwPreviousSpinCount = SetCriticalSectionSpinCount(&critical, dwSpinCount);
dwSpinCountExpected = 0;
#if !defined(WINPR_CRITICAL_SECTION_DISABLE_SPINCOUNT)
if (sysinfo.dwNumberOfProcessors > 1)
dwSpinCountExpected = dwSpinCount+1;
#endif
if (dwPreviousSpinCount != dwSpinCountExpected)
{
printf("CriticalSection failure: SetCriticalSectionSpinCount returned %lu (expected: %lu)\n", dwPreviousSpinCount, dwSpinCountExpected);
goto fail;
}
DeleteCriticalSection(&critical);
if (dwSpinCount%2==0)
InitializeCriticalSectionAndSpinCount(&critical, dwSpinCount);
else
InitializeCriticalSectionEx(&critical, dwSpinCount, 0);
}
DeleteCriticalSection(&critical);
/**
* Test single-threaded recursive TryEnterCriticalSection/EnterCriticalSection/LeaveCriticalSection
*
*/
InitializeCriticalSection(&critical);
for (i=0; i<1000; i++)
{
if (critical.RecursionCount != i)
{
printf("CriticalSection failure: RecursionCount field is %ld instead of %d.\n", critical.RecursionCount, i);
goto fail;
}
if (i%2==0)
{
EnterCriticalSection(&critical);
}
else
{
if (TryEnterCriticalSection(&critical) == FALSE)
{
printf("CriticalSection failure: TryEnterCriticalSection failed where it should not.\n");
goto fail;
}
}
if (critical.OwningThread != hMainThread)
{
printf("CriticalSection failure: Could not verify section ownership (loop index=%d).\n", i);
goto fail;
}
}
while (--i >= 0)
{
LeaveCriticalSection(&critical);
if (critical.RecursionCount != i)
{
printf("CriticalSection failure: RecursionCount field is %ld instead of %d.\n", critical.RecursionCount, i);
goto fail;
}
if (critical.OwningThread != (HANDLE)(i ? hMainThread : NULL))
{
printf("CriticalSection failure: Could not verify section ownership (loop index=%d).\n", i);
goto fail;
}
}
DeleteCriticalSection(&critical);
/**
* Test using multiple threads modifying the same value
*/
dwThreadCount = sysinfo.dwNumberOfProcessors > 1 ? sysinfo.dwNumberOfProcessors : 2;
hThreads = (HANDLE*)calloc(dwThreadCount, sizeof(HANDLE));
for (j=0; j < TEST_SYNC_CRITICAL_TEST1_RUNS; j++)
{
dwSpinCount = j * 1000;
InitializeCriticalSectionAndSpinCount(&critical, dwSpinCount);
gTestValueVulnerable = 0;
gTestValueSerialized = 0;
/* the TestSynchCritical_Test1 threads shall run until bTest1Running is FALSE */
bTest1Running = TRUE;
for (i=0; i<dwThreadCount; i++) {
hThreads[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) TestSynchCritical_Test1, &bTest1Running, 0, NULL);
}
/* let it run for TEST_SYNC_CRITICAL_TEST1_RUNTIME_MS ... */
Sleep(TEST_SYNC_CRITICAL_TEST1_RUNTIME_MS);
bTest1Running = FALSE;
for (i=0; i<dwThreadCount; i++)
{
if (WaitForSingleObject(hThreads[i], INFINITE) != WAIT_OBJECT_0)
{
printf("CriticalSection failure: Failed to wait for thread #%d\n", i);
goto fail;
}
GetExitCodeThread(hThreads[i], &dwThreadExitCode);
if(dwThreadExitCode != 0)
{
printf("CriticalSection failure: Thread #%d returned error code %lu\n", i, dwThreadExitCode);
goto fail;
}
CloseHandle(hThreads[i]);
}
if (gTestValueVulnerable != gTestValueSerialized)
{
printf("CriticalSection failure: unexpected test value %ld (expected %ld)\n", gTestValueVulnerable, gTestValueSerialized);
goto fail;
}
DeleteCriticalSection(&critical);
}
free(hThreads);
/**
* TryEnterCriticalSection in thread must fail if we hold the lock in the main thread
*/
InitializeCriticalSection(&critical);
if (TryEnterCriticalSection(&critical) == FALSE)
{
printf("CriticalSection failure: TryEnterCriticalSection unexpectedly failed.\n");
goto fail;
}
/* This thread tries to call TryEnterCriticalSection which must fail */
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) TestSynchCritical_Test2, NULL, 0, NULL);
if (WaitForSingleObject(hThread, INFINITE) != WAIT_OBJECT_0)
{
printf("CriticalSection failure: Failed to wait for thread\n");
goto fail;
}
GetExitCodeThread(hThread, &dwThreadExitCode);
if(dwThreadExitCode != 0)
{
printf("CriticalSection failure: Thread returned error code %lu\n", dwThreadExitCode);
goto fail;
}
CloseHandle(hThread);
*pbThreadTerminated = TRUE; /* requ. for winpr issue, see below */
return (PVOID)0;
fail:
*pbThreadTerminated = TRUE; /* requ. for winpr issue, see below */
return (PVOID)1;
}
int TestSynchCritical(int argc, char* argv[])
{
BOOL bThreadTerminated = FALSE;
HANDLE hThread;
DWORD dwThreadExitCode;
DWORD dwDeadLockDetectionTimeMs;
int i;
dwDeadLockDetectionTimeMs = 2 * TEST_SYNC_CRITICAL_TEST1_RUNTIME_MS * TEST_SYNC_CRITICAL_TEST1_RUNS;
printf("Deadlock will be assumed after %lu ms.\n", dwDeadLockDetectionTimeMs);
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) TestSynchCritical_Main, &bThreadTerminated, 0, NULL);
/**
* We have to be able to detect dead locks in this test.
* At the time of writing winpr's WaitForSingleObject has not implemented timeout for thread wait
*
* Workaround checking the value of bThreadTerminated which is passed in the thread arg
*/
for (i=0; i<dwDeadLockDetectionTimeMs; i+=100)
{
if (bThreadTerminated)
break;
Sleep(100);
}
if (!bThreadTerminated)
{
printf("CriticalSection failure: Possible dead lock detected\n");
return -1;
}
GetExitCodeThread(hThread, &dwThreadExitCode);
CloseHandle(hThread);
if(dwThreadExitCode != 0)
{
return -1;
}
return 0;
}