mirror of
https://github.com/c3lang/c3c.git
synced 2026-02-27 12:01:16 +00:00
Architecture generic Atomics (C11 + extras) (#958)
* Initial implementation for c11 atomics * Fix cmpxchg usage * Support for floating point atomics * Added atomic min and max * Updated copyright notice * Removed Floats from and or xor. Added mul, div, bitshift * Changed get_atomic_compatible_type to lower_to_atomic_compatible_type * Require non-null pointers * Fix spacing * Added Atomic type * Added macro to reduce code * Small reorder and cleanup * Added cmpxchg constrains * Apply all the restrictions for atomic loads/stores and cmpxchg
This commit is contained in:
committed by
GitHub
parent
efb492eace
commit
7aca8a02cb
@@ -1,5 +1,566 @@
|
||||
// Copyright (c) 2021 Christoffer Lerno. All rights reserved.
|
||||
// Copyright (c) 2023 Eduardo José Gómez Hernández. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license
|
||||
// a copy of which can be found in the LICENSE_STDLIB file.
|
||||
module std::atomic::types(<Type>);
|
||||
|
||||
struct Atomic
|
||||
{
|
||||
Type data;
|
||||
}
|
||||
|
||||
fn Type Atomic.load(&self, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
switch(ordering)
|
||||
{
|
||||
case NOT_ATOMIC: return $$atomic_load(data, false, AtomicOrdering.NOT_ATOMIC.ordinal);
|
||||
case UNORDERED: return $$atomic_load(data, false, AtomicOrdering.UNORDERED.ordinal);
|
||||
case RELAXED: return $$atomic_load(data, false, AtomicOrdering.RELAXED.ordinal);
|
||||
case ACQUIRE: return $$atomic_load(data, false, AtomicOrdering.ACQUIRE.ordinal);
|
||||
|
||||
default:
|
||||
case SEQ_CONSISTENT: return $$atomic_load(data, false, AtomicOrdering.SEQ_CONSISTENT.ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
fn void Atomic.store(&self, Type value, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
switch(ordering)
|
||||
{
|
||||
case NOT_ATOMIC: $$atomic_store(data, value, false, AtomicOrdering.NOT_ATOMIC.ordinal);
|
||||
case UNORDERED: $$atomic_store(data, value, false, AtomicOrdering.UNORDERED.ordinal);
|
||||
case RELAXED: $$atomic_store(data, value, false, AtomicOrdering.RELAXED.ordinal);
|
||||
case RELEASE: $$atomic_store(data, value, false, AtomicOrdering.RELEASE.ordinal);
|
||||
|
||||
default:
|
||||
case SEQ_CONSISTENT: $$atomic_store(data, value, false, AtomicOrdering.SEQ_CONSISTENT.ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
fn Type Atomic.add(&self, Type value, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_add, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.sub(&self, Type value, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_sub, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.mul(&self, Type value, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_mul, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.div(&self, Type value, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_div, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.max(&self, Type value, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_div, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.min(&self, Type value, AtomicOrdering ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_min, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.or(&self, ulong value, AtomicOrdering ordering = SEQ_CONSISTENT) @if(!types::is_float(Type))
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_or, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.xor(&self, ulong value, AtomicOrdering ordering = SEQ_CONSISTENT) @if(!types::is_float(Type))
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_xor, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.and(&self, ulong value, AtomicOrdering ordering = SEQ_CONSISTENT) @if(!types::is_float(Type))
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_and, data, value, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.shift_right(&self, ulong amount, AtomicOrdering ordering = SEQ_CONSISTENT) @if(!types::is_float(Type))
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_shift_right, data, amount, ordering);
|
||||
}
|
||||
|
||||
fn Type Atomic.shift_left(&self, ulong amount, AtomicOrdering ordering = SEQ_CONSISTENT) @if(!types::is_float(Type))
|
||||
{
|
||||
Type* data = &self.data;
|
||||
return @atomic_exec(atomic::fetch_shift_left, data, amount, ordering);
|
||||
}
|
||||
|
||||
macro @atomic_exec(#func, data, value, ordering) @local
|
||||
{
|
||||
switch(ordering)
|
||||
{
|
||||
case RELAXED: return #func(data, value, RELAXED);
|
||||
case ACQUIRE: return #func(data, value, ACQUIRE);
|
||||
case RELEASE: return #func(data, value, RELEASE);
|
||||
case ACQUIRE_RELEASE: return #func(data, value, ACQUIRE_RELEASE);
|
||||
|
||||
default:
|
||||
case SEQ_CONSISTENT: return #func(data, value, SEQ_CONSISTENT);
|
||||
}
|
||||
}
|
||||
|
||||
module std::atomic;
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) || types::is_float($typeof(*ptr)) "Only integer/float pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_add(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = old_value + y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) || types::is_float($typeof(*ptr)) "Only integer/float pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_sub(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = old_value - y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) || types::is_float($typeof(*ptr)) "Only integer/float pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_mul(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = old_value * y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) || types::is_float($typeof(*ptr)) "Only integer/float pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_div(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = old_value / y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) "Only intege pointers may be used."
|
||||
* @require types::is_int($typeof(y)) "The value for or must be an int"
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_or(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
$StorageType storage_y = ($StorageType)y;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = storage_old_value | storage_y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) "Only integer pointers may be used."
|
||||
* @require types::is_int($typeof(y)) "The value for or must be an int"
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_xor(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
$StorageType storage_y = ($StorageType)y;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = storage_old_value ^ storage_y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) "Only integer pointers may be used."
|
||||
* @require types::is_int($typeof(y)) "The value for or must be an int"
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_and(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
$StorageType storage_y = ($StorageType)y;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = storage_old_value & storage_y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) "Only integer pointers may be used."
|
||||
* @require types::is_int($typeof(y)) "The value for or must be an int"
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_shift_right(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
$StorageType storage_y = ($StorageType)y;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = storage_old_value >> storage_y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) "Only integer pointers may be used."
|
||||
* @require types::is_int($typeof(y)) "The value for or must be an int"
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_shift_left(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value;
|
||||
$StorageType storage_y = ($StorageType)y;
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
new_value = storage_old_value << storage_y;
|
||||
storage_new_value = bitcast(new_value, $StorageType);
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) "Only integer pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro flag_set(ptr, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value = true;
|
||||
|
||||
do {
|
||||
old_value = $$atomic_load(ptr, false, $ordering.ordinal);
|
||||
} while (mem::compare_exchange(ptr, old_value, new_value, $ordering, $load_ordering) != old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) "Only integer pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro flag_clear(ptr, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value = false;
|
||||
|
||||
do {
|
||||
old_value = $$atomic_load(ptr, false, $ordering.ordinal);
|
||||
} while (mem::compare_exchange(ptr, old_value, new_value, $ordering, $load_ordering) != old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) || types::is_float($typeof(*ptr)) "Only integer/float pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_max(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value = y;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value = bitcast(new_value, $StorageType);
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
if (old_value >= new_value) break;
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [&in] ptr "the variable or dereferenced pointer to the data."
|
||||
* @param [in] y "the value to be added to ptr."
|
||||
* @param $ordering "atomic ordering of the load, defaults to SEQ_CONSISTENT"
|
||||
* @return "returns the old value of ptr"
|
||||
*
|
||||
* @require types::is_int($typeof(*ptr)) || types::is_float($typeof(*ptr)) "Only integer/float pointers may be used."
|
||||
* @require $ordering != AtomicOrdering.NOT_ATOMIC && $ordering != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
**/
|
||||
macro fetch_min(ptr, y, AtomicOrdering $ordering = SEQ_CONSISTENT)
|
||||
{
|
||||
var $load_ordering = $ordering;
|
||||
$if $ordering == RELEASE || $ordering == ACQUIRE_RELEASE:
|
||||
$load_ordering = AtomicOrdering.SEQ_CONSISTENT;
|
||||
$endif
|
||||
|
||||
var $StorageType = $typefrom(types::lower_to_atomic_compatible_type($typeof(*ptr)));
|
||||
|
||||
$StorageType* storage_ptr = ($StorageType*)ptr;
|
||||
|
||||
$typeof(*ptr) old_value;
|
||||
$typeof(*ptr) new_value = y;
|
||||
|
||||
$StorageType storage_old_value;
|
||||
$StorageType storage_new_value = bitcast(new_value, $StorageType);
|
||||
|
||||
do {
|
||||
storage_old_value = $$atomic_load(storage_ptr, false, $load_ordering.ordinal);
|
||||
old_value = bitcast(storage_old_value, $typeof(*ptr));
|
||||
if (old_value <= new_value) break;
|
||||
} while (mem::compare_exchange(storage_ptr, storage_old_value, storage_new_value, $ordering, $load_ordering) != storage_old_value);
|
||||
|
||||
return old_value;
|
||||
}
|
||||
|
||||
@@ -62,11 +62,19 @@ macro void @atomic_store(&x, value, AtomicOrdering $ordering = SEQ_CONSISTENT, $
|
||||
$$atomic_store(x, value, $volatile, (int)$ordering);
|
||||
}
|
||||
|
||||
/**
|
||||
* @require $success != AtomicOrdering.NOT_ATOMIC && $success != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
* @require $failure != AtomicOrdering.RELEASE && $failure != AtomicOrdering.ACQUIRE_RELEASE "Acquire release is not valid."
|
||||
**/
|
||||
macro compare_exchange(ptr, compare, value, AtomicOrdering $success = SEQ_CONSISTENT, AtomicOrdering $failure = SEQ_CONSISTENT, bool $volatile = true, bool $weak = false, usz $alignment = 0)
|
||||
{
|
||||
return $$compare_exchange(ptr, compare, value, $volatile, $weak, $success.ordinal, $failure.ordinal, $alignment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @require $success != AtomicOrdering.NOT_ATOMIC && $success != AtomicOrdering.UNORDERED "Acquire ordering is not valid."
|
||||
* @require $failure != AtomicOrdering.RELEASE && $failure != AtomicOrdering.ACQUIRE_RELEASE "Acquire release is not valid."
|
||||
**/
|
||||
macro compare_exchange_volatile(ptr, compare, value, AtomicOrdering $success = SEQ_CONSISTENT, AtomicOrdering $failure = SEQ_CONSISTENT)
|
||||
{
|
||||
return compare_exchange(ptr, compare, value, $success, $failure, true);
|
||||
|
||||
@@ -208,6 +208,32 @@ macro bool may_load_atomic($Type)
|
||||
$endswitch
|
||||
}
|
||||
|
||||
macro lower_to_atomic_compatible_type($Type)
|
||||
{
|
||||
$switch ($Type.kindof)
|
||||
$case SIGNED_INT:
|
||||
$case UNSIGNED_INT:
|
||||
return $Type.typeid;
|
||||
$case DISTINCT:
|
||||
return lower_to_atomic_compatible_type($Type.inner);
|
||||
$case FLOAT:
|
||||
$switch ($Type)
|
||||
$case float16:
|
||||
return ushort.typeid;
|
||||
$case float:
|
||||
return uint.typeid;
|
||||
$case double:
|
||||
return ulong.typeid;
|
||||
$case float128:
|
||||
return uint128.typeid;
|
||||
$default:
|
||||
return void.typeid;
|
||||
$endswitch
|
||||
$default:
|
||||
return void.typeid;
|
||||
$endswitch
|
||||
}
|
||||
|
||||
macro bool is_promotable_to_floatlike($Type) => types::is_floatlike($Type) || types::is_int($Type);
|
||||
macro bool is_promotable_to_float($Type) => types::is_float($Type) || types::is_int($Type);
|
||||
|
||||
|
||||
245
test/unit/stdlib/atomic.c3
Normal file
245
test/unit/stdlib/atomic.c3
Normal file
@@ -0,0 +1,245 @@
|
||||
import std::thread;
|
||||
import std::io;
|
||||
import std::atomic;
|
||||
|
||||
uint a;
|
||||
float fa;
|
||||
|
||||
fn void! add() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
a = 0;
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&a, 5);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(a == ts.len * 10 * 5, "Threads returned %d, expected %d", a, ts.len * 10 * 5);
|
||||
}
|
||||
|
||||
fn void! sub() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
a = ts.len * 10 * 5;
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&a, 5);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(a == 0, "Threads returned %d, expected %d", a, 0);
|
||||
}
|
||||
|
||||
fn void! div() @test
|
||||
{
|
||||
Thread[8] ts;
|
||||
a = 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8 * 8;
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_div(&a, 8);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(a == 8, "Threads returned %d, expected %d", a, 8);
|
||||
}
|
||||
|
||||
fn void! max() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
a = 0;
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
uint la = 0;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_max(&a, la);
|
||||
la++;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_max(&a, la);
|
||||
la++;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_max(&a, la);
|
||||
la++;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_max(&a, la);
|
||||
la++;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_max(&a, la);
|
||||
la++;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_max(&a, la);
|
||||
la++;
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(a == 5, "Threads returned %d, expected %d", a, 5);
|
||||
}
|
||||
|
||||
fn void! min() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
a = 10;
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
uint la = 5;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_min(&a, la);
|
||||
la--;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_min(&a, la);
|
||||
la--;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_min(&a, la);
|
||||
la--;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_min(&a, la);
|
||||
la--;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_min(&a, la);
|
||||
la--;
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_min(&a, la);
|
||||
la--;
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(a == 0, "Threads returned %d, expected %d", a, 0);
|
||||
}
|
||||
|
||||
fn void! fadd() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
fa = 0;
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_add(&fa, 0.5f);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(fa == ts.len * 10 * 0.5, "Threads returned %f, expected %f", fa, ts.len * 10 * 0.5);
|
||||
}
|
||||
|
||||
fn void! fsub() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
fa = ts.len * 10 * 0.5;
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
thread::sleep_ms(5);
|
||||
atomic::fetch_sub(&fa, 0.5f);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(fa == 0, "Threads returned %f, expected %f", fa, 0);
|
||||
}
|
||||
154
test/unit/stdlib/atomic_types.c3
Normal file
154
test/unit/stdlib/atomic_types.c3
Normal file
@@ -0,0 +1,154 @@
|
||||
import std::thread;
|
||||
import std::io;
|
||||
import std::atomic::types;
|
||||
|
||||
Atomic(<uint>) a;
|
||||
Atomic(<float>) fa;
|
||||
|
||||
fn void! add() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
a.store(0);
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
thread::sleep_ms(5);
|
||||
a.add(5);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(a.load() == ts.len * 10 * 5, "Threads returned %d, expected %d", a.load(), ts.len * 10 * 5);
|
||||
}
|
||||
|
||||
fn void! sub() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
a.store(ts.len * 10 * 5);
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
thread::sleep_ms(5);
|
||||
a.sub(5);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(a.load() == 0, "Threads returned %d, expected %d", a.load(), 0);
|
||||
}
|
||||
|
||||
fn void! fadd() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
fa.store(0);
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.add(0.5);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(fa.load() == ts.len * 10 * 0.5, "Threads returned %f, expected %f", fa.load(), ts.len * 10 * 0.5);
|
||||
}
|
||||
|
||||
fn void! fsub() @test
|
||||
{
|
||||
Thread[100] ts;
|
||||
fa.store(ts.len * 10 * 0.5);
|
||||
foreach (&t : ts)
|
||||
{
|
||||
t.create(fn int(void* arg) {
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
thread::sleep_ms(5);
|
||||
fa.sub(0.5);
|
||||
return 0;
|
||||
}, null)!;
|
||||
}
|
||||
foreach (&t : ts)
|
||||
{
|
||||
assert(t.join()! == 0);
|
||||
}
|
||||
assert(fa.load() == 0, "Threads returned %f, expected %f", fa.load(), 0);
|
||||
}
|
||||
Reference in New Issue
Block a user