Sync Barrier¶
Rendezvous and broadcast, checked rather than timed: a barrier that releases a task early is fast and wrong, so what the test asserts is that no task reads a value from the round before, and that two consecutive broadcasts do not share storage.
/******************************************************************************
* Copyright (c) 2009-2026 Hans Pabst *
* Copyright (c) 2009-2026 Intel Corporation *
* This file is part of the LIBXS library. *
* *
* For information on the license, see the LICENSE file. *
* Further information: https://github.com/hfp/libxs/ *
* SPDX-License-Identifier: BSD-3-Clause *
******************************************************************************/
#include <libxs/libxs_source.h>
#if defined(_OPENMP)
# include <omp.h>
#endif
/** Rounds of the concurrent checks. Enough to give the scheduler occasion to
* interleave the tasks, few enough that the test stays a test. */
#if !defined(NREPEAT)
# define NREPEAT 2000
#endif
/**
* A team that cannot arrive must not be waited for, or a mistake in the caller's
* task count becomes a hang rather than an error. Both NULL and a count of zero
* are answered, and a broadcast still yields the value the caller carried in.
*/
static int test_degenerate(void)
{
libxs_barrier_t single;
int result = EXIT_SUCCESS;
libxs_barrier_init(NULL, 4);
libxs_barrier_wait(NULL);
if (7 != libxs_barrier_bcast(NULL, 0, 0, 7)) result = EXIT_FAILURE;
libxs_barrier_init(&single, 0);
if (1 != single.ntasks) result = EXIT_FAILURE;
libxs_barrier_wait(&single);
if (5 != libxs_barrier_bcast(&single, 0, 0, 5)) result = EXIT_FAILURE;
return result;
}
#if defined(_OPENMP)
/**
* What a rendezvous is for: every task stamps a slot of its own with the round
* it is in, and after the rendezvous every task reads every slot. A task released
* before the last arrival reads a stamp from the round before, which no amount of
* luck turns into the right number.
*
* The broadcast is checked in the same team: it carries a value that changes
* every round, so a task reading the publication of the round before differs
* from the one it should have read rather than coinciding with it.
*/
static int test_team(int nthreads)
{
int* stamp = (int*)malloc((size_t)nthreads * sizeof(int));
int result = EXIT_FAILURE;
if (NULL != stamp) {
libxs_barrier_t barrier;
int stale = 0, wrong = 0, i;
for (i = 0; i < nthreads; ++i) stamp[i] = -1;
libxs_barrier_init(&barrier, nthreads);
# pragma omp parallel num_threads(nthreads) reduction(+:stale)
{ const int tid = omp_get_thread_num();
int r, t;
for (r = 0; r < NREPEAT; ++r) {
stamp[tid] = r;
libxs_barrier_wait(&barrier);
for (t = 0; t < nthreads; ++t) {
if (stamp[t] != r) ++stale;
}
/* the second rendezvous keeps a task from stamping the next round over
a slot another task has not read yet */
libxs_barrier_wait(&barrier);
}
}
# pragma omp parallel num_threads(nthreads) reduction(+:wrong)
{ const int tid = omp_get_thread_num();
int r;
for (r = 0; r < NREPEAT; ++r) {
if (libxs_barrier_bcast(&barrier, tid, 0, r * 7 + 1) != r * 7 + 1) {
++wrong;
}
}
}
result = (0 == stale && 0 == wrong) ? EXIT_SUCCESS : EXIT_FAILURE;
free(stamp);
}
return result;
}
/**
* Consecutive broadcasts must land in DIFFERENT slots, which is what lets a task
* be released and read late without reading the next publication. The slots are
* inspected rather than raced against, because a race that has to be lost to be
* observed is not a test: the property is that publication n and n+1 do not share
* storage, and that is decidable by looking.
*/
static int test_slots(int nthreads)
{
libxs_barrier_t barrier;
libxs_barrier_init(&barrier, nthreads);
# pragma omp parallel num_threads(nthreads)
{ const int tid = omp_get_thread_num();
libxs_barrier_bcast(&barrier, tid, 0, 101);
libxs_barrier_bcast(&barrier, tid, 0, 202);
}
return (101 == barrier.value[0] && 202 == barrier.value[1])
? EXIT_SUCCESS : EXIT_FAILURE;
}
#endif
int main(void)
{
int result = test_degenerate();
/**
* Every further check needs a team, so a build without OpenMP has nothing to
* check beyond the contract above. The same holds for a runtime that grants a
* single thread. Either way the reason is said out loud rather than passing
* quietly: tests/test.sh prints a test's stderr beside its verdict, so a run
* that checked less says which part it left out.
*/
#if defined(_OPENMP)
{ int nthreads = 1;
/* the team says how large it is: a barrier told a number the runtime does
not grant would wait for a task that never arrives */
# pragma omp parallel
{ if (0 == omp_get_thread_num()) nthreads = omp_get_num_threads(); }
if (EXIT_SUCCESS == result && 1 < nthreads) {
result = test_team(nthreads);
if (EXIT_SUCCESS == result) result = test_slots(nthreads);
}
else if (EXIT_SUCCESS == result) {
fprintf(stderr, "one thread granted: the rendezvous is not exercised\n");
}
}
#else
if (EXIT_SUCCESS == result) {
fprintf(stderr, "no OpenMP: only the single-task contract is checked\n");
}
#endif
return result;
}