mirror of
https://gitlab.gnome.org/GNOME/glib.git
synced 2025-07-31 22:23:39 +02:00
This (dubious) testcase fails before the previous commit due to errno being clobbered by the interrupted wait on the contended mutex. The previous commit fixes that. The testcase is dubious because, in theory (as per POSIX), g_cond_wait_until() is permitted to return TRUE at any time for any reason, due to so-called "spurious wakeups". Having a testcase that asserts that the return value should be FALSE is therefore fundamentally broken. We do it anyway, though. We're only really trying to test a bug in our homemade Linux/futex implementation here, and it takes a fair amount of effort to actually convince the old code to fail (including some system stuff which probably isn't available on Windows). There's also the spurious wakeup situation mentioned above to worry about on other systems. For all of those reasons, this test is only enabled on Linux.
379 lines
8.5 KiB
C
379 lines
8.5 KiB
C
/* Unit tests for GCond
|
|
* Copyright (C) 2011 Red Hat, Inc
|
|
* Author: Matthias Clasen
|
|
*
|
|
* This work is provided "as is"; redistribution and modification
|
|
* in whole or in part, in any medium, physical or electronic is
|
|
* permitted without restriction.
|
|
*
|
|
* This work is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
*
|
|
* In no event shall the authors or contributors be liable for any
|
|
* direct, indirect, incidental, special, exemplary, or consequential
|
|
* damages (including, but not limited to, procurement of substitute
|
|
* goods or services; loss of use, data, or profits; or business
|
|
* interruption) however caused and on any theory of liability, whether
|
|
* in contract, strict liability, or tort (including negligence or
|
|
* otherwise) arising in any way out of the use of this software, even
|
|
* if advised of the possibility of such damage.
|
|
*/
|
|
|
|
/* We are testing some deprecated APIs here */
|
|
#define GLIB_DISABLE_DEPRECATION_WARNINGS
|
|
|
|
#include <glib.h>
|
|
|
|
static GCond cond;
|
|
static GMutex mutex;
|
|
static volatile gint next;
|
|
|
|
static void
|
|
push_value (gint value)
|
|
{
|
|
g_mutex_lock (&mutex);
|
|
while (next != 0)
|
|
g_cond_wait (&cond, &mutex);
|
|
next = value;
|
|
if (g_test_verbose ())
|
|
g_printerr ("Thread %p producing next value: %d\n", g_thread_self (), value);
|
|
if (value % 10 == 0)
|
|
g_cond_broadcast (&cond);
|
|
else
|
|
g_cond_signal (&cond);
|
|
g_mutex_unlock (&mutex);
|
|
}
|
|
|
|
static gint
|
|
pop_value (void)
|
|
{
|
|
gint value;
|
|
|
|
g_mutex_lock (&mutex);
|
|
while (next == 0)
|
|
{
|
|
if (g_test_verbose ())
|
|
g_printerr ("Thread %p waiting for cond\n", g_thread_self ());
|
|
g_cond_wait (&cond, &mutex);
|
|
}
|
|
value = next;
|
|
next = 0;
|
|
g_cond_broadcast (&cond);
|
|
if (g_test_verbose ())
|
|
g_printerr ("Thread %p consuming value %d\n", g_thread_self (), value);
|
|
g_mutex_unlock (&mutex);
|
|
|
|
return value;
|
|
}
|
|
|
|
static gpointer
|
|
produce_values (gpointer data)
|
|
{
|
|
gint total;
|
|
gint i;
|
|
|
|
total = 0;
|
|
|
|
for (i = 1; i < 100; i++)
|
|
{
|
|
total += i;
|
|
push_value (i);
|
|
}
|
|
|
|
push_value (-1);
|
|
push_value (-1);
|
|
|
|
if (g_test_verbose ())
|
|
g_printerr ("Thread %p produced %d altogether\n", g_thread_self (), total);
|
|
|
|
return GINT_TO_POINTER (total);
|
|
}
|
|
|
|
static gpointer
|
|
consume_values (gpointer data)
|
|
{
|
|
gint accum = 0;
|
|
gint value;
|
|
|
|
while (TRUE)
|
|
{
|
|
value = pop_value ();
|
|
if (value == -1)
|
|
break;
|
|
|
|
accum += value;
|
|
}
|
|
|
|
if (g_test_verbose ())
|
|
g_printerr ("Thread %p accumulated %d\n", g_thread_self (), accum);
|
|
|
|
return GINT_TO_POINTER (accum);
|
|
}
|
|
|
|
static GThread *producer, *consumer1, *consumer2;
|
|
|
|
static void
|
|
test_cond1 (void)
|
|
{
|
|
gint total, acc1, acc2;
|
|
|
|
producer = g_thread_create (produce_values, NULL, TRUE, NULL);
|
|
consumer1 = g_thread_create (consume_values, NULL, TRUE, NULL);
|
|
consumer2 = g_thread_create (consume_values, NULL, TRUE, NULL);
|
|
|
|
total = GPOINTER_TO_INT (g_thread_join (producer));
|
|
acc1 = GPOINTER_TO_INT (g_thread_join (consumer1));
|
|
acc2 = GPOINTER_TO_INT (g_thread_join (consumer2));
|
|
|
|
g_assert_cmpint (total, ==, acc1 + acc2);
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
GMutex mutex;
|
|
GCond cond;
|
|
gint limit;
|
|
gint count;
|
|
} Barrier;
|
|
|
|
static void
|
|
barrier_init (Barrier *barrier,
|
|
gint limit)
|
|
{
|
|
g_mutex_init (&barrier->mutex);
|
|
g_cond_init (&barrier->cond);
|
|
barrier->limit = limit;
|
|
barrier->count = limit;
|
|
}
|
|
|
|
static gint
|
|
barrier_wait (Barrier *barrier)
|
|
{
|
|
gint ret;
|
|
|
|
g_mutex_lock (&barrier->mutex);
|
|
barrier->count--;
|
|
if (barrier->count == 0)
|
|
{
|
|
ret = -1;
|
|
barrier->count = barrier->limit;
|
|
g_cond_broadcast (&barrier->cond);
|
|
}
|
|
else
|
|
{
|
|
ret = 0;
|
|
while (barrier->count != barrier->limit)
|
|
g_cond_wait (&barrier->cond, &barrier->mutex);
|
|
}
|
|
g_mutex_unlock (&barrier->mutex);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
barrier_clear (Barrier *barrier)
|
|
{
|
|
g_mutex_clear (&barrier->mutex);
|
|
g_cond_clear (&barrier->cond);
|
|
}
|
|
|
|
static Barrier b;
|
|
static gint check;
|
|
|
|
static gpointer
|
|
cond2_func (gpointer data)
|
|
{
|
|
gint value = GPOINTER_TO_INT (data);
|
|
gint ret;
|
|
|
|
g_atomic_int_inc (&check);
|
|
|
|
if (g_test_verbose ())
|
|
g_printerr ("thread %d starting, check %d\n", value, g_atomic_int_get (&check));
|
|
|
|
g_usleep (10000 * value);
|
|
|
|
g_atomic_int_inc (&check);
|
|
|
|
if (g_test_verbose ())
|
|
g_printerr ("thread %d reaching barrier, check %d\n", value, g_atomic_int_get (&check));
|
|
|
|
ret = barrier_wait (&b);
|
|
|
|
g_assert_cmpint (g_atomic_int_get (&check), ==, 10);
|
|
|
|
if (g_test_verbose ())
|
|
g_printerr ("thread %d leaving barrier (%d), check %d\n", value, ret, g_atomic_int_get (&check));
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* this test demonstrates how to use a condition variable
|
|
* to implement a barrier
|
|
*/
|
|
static void
|
|
test_cond2 (void)
|
|
{
|
|
gint i;
|
|
GThread *threads[5];
|
|
|
|
g_atomic_int_set (&check, 0);
|
|
|
|
barrier_init (&b, 5);
|
|
for (i = 0; i < 5; i++)
|
|
threads[i] = g_thread_create (cond2_func, GINT_TO_POINTER (i), TRUE, NULL);
|
|
|
|
for (i = 0; i < 5; i++)
|
|
g_thread_join (threads[i]);
|
|
|
|
g_assert_cmpint (g_atomic_int_get (&check), ==, 10);
|
|
|
|
barrier_clear (&b);
|
|
}
|
|
|
|
static void
|
|
test_wait_until (void)
|
|
{
|
|
gint64 until;
|
|
GMutex lock;
|
|
GCond cond;
|
|
|
|
/* This test will make sure we don't wait too much or too little.
|
|
*
|
|
* We check the 'too long' with a timeout of 60 seconds.
|
|
*
|
|
* We check the 'too short' by verifying a guarantee of the API: we
|
|
* should not wake up until the specified time has passed.
|
|
*/
|
|
g_mutex_init (&lock);
|
|
g_cond_init (&cond);
|
|
|
|
until = g_get_monotonic_time () + G_TIME_SPAN_SECOND;
|
|
|
|
/* Could still have spurious wakeups, so we must loop... */
|
|
g_mutex_lock (&lock);
|
|
while (g_cond_wait_until (&cond, &lock, until))
|
|
;
|
|
g_mutex_unlock (&lock);
|
|
|
|
/* Make sure it's after the until time */
|
|
g_assert_cmpint (until, <=, g_get_monotonic_time ());
|
|
|
|
/* Make sure it returns FALSE on timeout */
|
|
until = g_get_monotonic_time () + G_TIME_SPAN_SECOND / 50;
|
|
g_mutex_lock (&lock);
|
|
g_assert (g_cond_wait_until (&cond, &lock, until) == FALSE);
|
|
g_mutex_unlock (&lock);
|
|
|
|
g_mutex_clear (&lock);
|
|
g_cond_clear (&cond);
|
|
}
|
|
|
|
#ifdef __linux__
|
|
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <unistd.h>
|
|
|
|
static pthread_t main_thread;
|
|
|
|
static void *
|
|
mutex_holder (void *data)
|
|
{
|
|
GMutex *lock = data;
|
|
|
|
g_mutex_lock (lock);
|
|
|
|
/* Let the lock become contended */
|
|
g_usleep (G_TIME_SPAN_SECOND);
|
|
|
|
/* Interrupt the wait on the other thread */
|
|
pthread_kill (main_thread, SIGHUP);
|
|
|
|
/* If we don't sleep here, then the g_mutex_unlock() below will clear
|
|
* the mutex, causing the interrupted futex call in the other thread
|
|
* to return success (which is not what we want).
|
|
*
|
|
* The other thread needs to have time to wake up and see that the
|
|
* lock is still contended.
|
|
*/
|
|
g_usleep (G_TIME_SPAN_SECOND / 10);
|
|
|
|
g_mutex_unlock (lock);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
signal_handler (int sig)
|
|
{
|
|
}
|
|
|
|
static void
|
|
test_wait_until_errno (void)
|
|
{
|
|
gboolean result;
|
|
GMutex lock;
|
|
GCond cond;
|
|
|
|
struct sigaction act = { { signal_handler, } }; /* important: no SA_RESTART (we want EINTR) */
|
|
|
|
g_test_summary ("Check proper handling of errno in g_cond_wait_until with a contended mutex");
|
|
g_test_bug_base ("https://gitlab.gnome.org/GNOME/glib/");
|
|
g_test_bug ("merge_requests/957");
|
|
|
|
g_mutex_init (&lock);
|
|
g_cond_init (&cond);
|
|
|
|
main_thread = pthread_self ();
|
|
sigaction (SIGHUP, &act, NULL);
|
|
|
|
g_mutex_lock (&lock);
|
|
|
|
/* We create an annoying worker thread that will do two things:
|
|
*
|
|
* 1) hold the lock that we want to reacquire after returning from
|
|
* the condition variable wait
|
|
*
|
|
* 2) send us a signal to cause our wait on the contended lock to
|
|
* return EINTR, clobbering the errno return from the condition
|
|
* variable
|
|
*/
|
|
g_thread_unref (g_thread_new ("mutex-holder", mutex_holder, &lock));
|
|
|
|
result = g_cond_wait_until (&cond, &lock,
|
|
g_get_monotonic_time () + G_TIME_SPAN_SECOND / 50);
|
|
|
|
/* Even after all that disruption, we should still successfully return
|
|
* 'timed out'.
|
|
*/
|
|
g_assert_false (result);
|
|
|
|
g_mutex_unlock (&lock);
|
|
|
|
g_cond_clear (&cond);
|
|
g_mutex_clear (&lock);
|
|
}
|
|
|
|
#else
|
|
static void
|
|
test_wait_until_errno (void)
|
|
{
|
|
g_test_skip ("We only test this on Linux");
|
|
}
|
|
#endif
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
g_test_init (&argc, &argv, NULL);
|
|
|
|
g_test_add_func ("/thread/cond1", test_cond1);
|
|
g_test_add_func ("/thread/cond2", test_cond2);
|
|
g_test_add_func ("/thread/cond/wait-until", test_wait_until);
|
|
g_test_add_func ("/thread/cond/wait-until/contended-and-interrupted", test_wait_until_errno);
|
|
|
|
return g_test_run ();
|
|
}
|