Disabled external gits

This commit is contained in:
2022-04-07 18:46:57 +02:00
parent 88cb3426ad
commit 15e7120d6d
5316 changed files with 4563444 additions and 6 deletions

Submodule cs453-ca/CS453-2021-project deleted from ad9dbfc13b

30
cs453-ca/CS453-2021-project/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Ignore everything in this directory and subdirectories
*
# Except directories and specific files
!*/
!.gitignore
!*.md
!.placeholder
!Makefile
!*.c
!*.C
!*.cc
!*.cpp
!*.cxx
!*.c++
!*.h
!*.hh
!*.hpp
!*.hxx
!*.h++
!*.py
# And of course, we do not leak the solution proposed by the TA :)
solution/

View File

@@ -0,0 +1,27 @@
# CS-453 - Course project
The [project description](https://dcl.epfl.ch/site/_media/education/ca-project.pdf) is available on [Moodle](https://moodle.epfl.ch/course/view.php?id=14334) and the [website of the course](https://dcl.epfl.ch/site/education/ca_2021).
The description includes:
* an introduction to (software) transactional memory
* an introduction to concurrent programming in C11/C++11, with pointers to more resources
* the _specifications_ of the transactional memory you have to implement, i.e. both:
* sufficient properties for a transactional memory to be deemed _correct_
* a thorough description of the transactional memory interface
* practical informations, including:
* how to test your implementation on your local machine and on the evaluation server
* how your submission will be graded
* rules for (optionally) using 3rd-party libraries and collaboration (although the project is _individual_)
This repository provides:
* examples of how to use synchronization primitives (in `sync-examples/`)
* a reference implementation (in `reference/`)
* a "skeleton" implementation (in `template/`)
* this template is written in C11
* feel free to overwrite it completely if you prefer to use C++ (in this case include `<tm.hpp>` instead of `<tm.h>`)
* the program that will test your implementation (in `grading/`)
* the same program will be used on the evaluation server (although possibly with a different seed)
* you can use it to test/debug your implementation on your local machine (see the [description](https://dcl.epfl.ch/site/_media/education/ca-project.pdf))
* a tool to submit your implementation (in `submit.py`)
* you should have received by mail a secret _unique user identifier_ (UUID)
* see the [description](https://dcl.epfl.ch/site/_media/education/ca-project.pdf) for more information

View File

@@ -0,0 +1,55 @@
BIN := ./$(notdir $(lastword $(abspath .)))
EXT_H := h
EXT_HPP := h hh hpp hxx h++
EXT_C := c
EXT_CXX := C cc cpp cxx c++
INCLUDE_DIRS := ../include .
SOURCE_DIRS := .
WILD_EXT = $(strip $(foreach EXT,$($(1)),$(wildcard $(2)/*.$(EXT))))
HDRS_C := $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),$(call WILD_EXT,EXT_H,$(INCLUDE_DIR)))
HDRS_CXX := $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),$(call WILD_EXT,EXT_HPP,$(INCLUDE_DIR)))
SRCS_C := $(foreach SOURCE_DIR,$(SOURCE_DIRS),$(call WILD_EXT,EXT_C,$(SOURCE_DIR)))
SRCS_CXX := $(foreach SOURCE_DIR,$(SOURCE_DIRS),$(call WILD_EXT,EXT_CXX,$(SOURCE_DIR)))
OBJS := $(SRCS_C:%=%.o) $(SRCS_CXX:%=%.o)
CC := $(CC)
CCFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c11 $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),-I$(INCLUDE_DIR))
CXX := $(CXX)
CXXFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c++17 $(foreach INCLUDE_DIR,$(INCLUDE_DIRS),-I$(INCLUDE_DIR))
LD := $(if $(SRCS_CXX),$(CXX),$(CC))
LDFLAGS :=
LDLIBS := -ldl -lpthread
LIB_DIRS := $(filter-out ../include/ ../grading/ ../playground/ ../template/ ../sync-examples/,$(filter-out $(wildcard ../*),$(wildcard ../*/)))
LIB_SOS := $(patsubst %/,%.so,$(filter-out ../reference/,$(LIB_DIRS)))
.PHONY: build build-libs clean clean-libs run
build: $(BIN)
build-libs:
@$(foreach DIR,$(LIB_DIRS),make -C $(DIR) build; )
clean:
$(RM) $(OBJS) $(BIN)
clean-libs:
@$(foreach DIR,$(LIB_DIRS),make -C $(DIR) clean; )
run: $(BIN)
$(BIN) 453 ../reference.so $(LIB_SOS)
define BUILD_C
%.$(1).o: %.$(1) $$(HDRS_C) Makefile
$$(CC) $$(CCFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_C),$(eval $(call BUILD_C,$(EXT))))
define BUILD_CXX
%.$(1).o: %.$(1) $$(HDRS_CXX) Makefile
$$(CXX) $$(CXXFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_CXX),$(eval $(call BUILD_CXX,$(EXT))))
$(BIN): $(OBJS) Makefile
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)

View File

@@ -0,0 +1,333 @@
/**
* @file common.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Common set of helper functions/classes.
**/
#pragma once
// External headers
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <mutex>
#include <thread>
#include <utility>
extern "C" {
#include <time.h>
}
// -------------------------------------------------------------------------- //
/** Define a proposition as likely true.
* @param prop Proposition
**/
#undef likely
#ifdef __GNUC__
#define likely(prop) \
__builtin_expect((prop) ? 1 : 0, 1)
#else
#define likely(prop) \
(prop)
#endif
/** Define a proposition as likely false.
* @param prop Proposition
**/
#undef unlikely
#ifdef __GNUC__
#define unlikely(prop) \
__builtin_expect((prop) ? 1 : 0, 0)
#else
#define unlikely(prop) \
(prop)
#endif
// -------------------------------------------------------------------------- //
// Whether to enable more safety checks
constexpr static auto assert_mode = false;
// Maximum waiting time for initialization/clean-ups (in ms)
constexpr static auto max_side_time = ::std::chrono::milliseconds{2000};
// -------------------------------------------------------------------------- //
namespace Exception {
/** Defines a simple exception.
* @param name Exception name
* @param parent Parent exception (use 'Any' as the root)
* @param deftxt Default explanatory string
**/
#define EXCEPTION(name, parent, deftxt) \
class name: public parent { \
public: \
/** Default explanatory string constructor. \
**/ \
name(): parent{deftxt} {} \
/** Non-default parent forwarding constructor. \
* @param ... Forwarded arguments \
**/ \
template<class... Args> name(Args&&... args): parent{::std::forward<Args>(args)...} {} \
}
/** Root user exception class.
**/
class Any: public ::std::exception {
protected:
char const* text; // Explanation string
public:
/** Explanatory string constructor.
* @param text Explanatory null-terminated string
**/
Any(char const* text): text{text} {}
public:
/** Return the explanatory string.
* @return Explanatory string
**/
virtual char const* what() const noexcept {
return text;
}
};
/** Exception tree.
**/
EXCEPTION(Unreachable, Any, "unreachable code reached");
EXCEPTION(Bounded, Any, "bounded execution exception");
EXCEPTION(BoundedOverrun, Any, "bounded execution overrun");
}
// -------------------------------------------------------------------------- //
/** Non-copyable helper base class.
**/
class NonCopyable {
public:
/** Deleted copy constructor/assignment.
**/
NonCopyable(NonCopyable const&) = delete;
NonCopyable& operator=(NonCopyable const&) = delete;
protected:
/** Protected default constructor, to make sure class is not directly instantiated.
**/
NonCopyable() = default;
};
/** Time accounting class.
**/
class Chrono final {
public:
/** Tick class (always 1 tick = 1 ns).
**/
using Tick = uint_fast64_t;
constexpr static auto invalid_tick = Tick{0xbadc0de}; // Invalid tick value
private:
Tick total; // Total tick counter
Tick local; // Segment tick counter
public:
/** Tick constructor.
* @param tick Initial number of ticks (optional)
**/
Chrono(Tick tick = 0) noexcept: total{tick} {}
private:
/** Call a "clock" function, convert the result to the Tick type.
* @param func "Clock" function to call
* @return Resulting time
**/
static Tick convert(int (*func)(::clockid_t, struct ::timespec*)) noexcept {
struct ::timespec buf;
if (unlikely(func(CLOCK_MONOTONIC, &buf) < 0))
return invalid_tick;
auto res = static_cast<Tick>(buf.tv_nsec) + static_cast<Tick>(buf.tv_sec) * static_cast<Tick>(1000000000ul);
if (unlikely(res == invalid_tick)) // Bad luck...
return invalid_tick + 1;
return res;
}
public:
/** Get the resolution of the clock used.
* @return Resolution (in ns), 'invalid_tick' for unknown
**/
static auto get_resolution() noexcept {
return convert(::clock_getres);
}
public:
/** Start measuring a time segment.
**/
void start() noexcept {
local = convert(::clock_gettime);
}
/** Measure a time segment.
**/
auto delta() noexcept {
return convert(::clock_gettime) - local;
}
/** Stop measuring a time segment, and add it to the total.
**/
void stop() noexcept {
total += delta();
}
/** Reset the total tick counter.
**/
void reset() noexcept {
total = 0;
}
/** Get the total tick counter.
* @return Total tick counter
**/
auto get_tick() const noexcept {
return total;
}
};
/** Atomic waitable latch class.
**/
class Latch final {
private:
::std::mutex lock; // Local lock
::std::condition_variable cv; // For waiting/waking up
bool raised; // State of the latch
public:
/** Deleted copy/move constructor/assignment.
**/
Latch(Latch const&) = delete;
Latch& operator=(Latch const&) = delete;
/** Initial state constructor.
* @param raised Initial state of the latch
**/
Latch(bool raised = false): lock{}, raised{raised} {}
public:
/** Raise the latch, no-op if already raised, release semantic.
**/
void raise() {
// Release fence
::std::atomic_thread_fence(::std::memory_order_release);
// Raise
::std::unique_lock<decltype(lock)> guard{lock};
raised = true;
cv.notify_all();
}
/** Wait for the latch to be raised, then reset it, acquire semantic if no timeout.
* @param maxtick Maximal duration to wait for (in ticks)
* @return Whether the latch was raised before the maximal duration elapsed
**/
bool wait(Chrono::Tick maxtick) {
{ // Wait for raised
::std::unique_lock<decltype(lock)> guard{lock};
// Wait
if (maxtick == Chrono::invalid_tick) {
while (!raised)
cv.wait(guard);
} else {
if (!cv.wait_for(guard, ::std::chrono::nanoseconds{maxtick}, [&]() { return raised; })) // Overtime
return false;
}
// Reset
raised = false;
}
// Acquire fence
::std::atomic_thread_fence(::std::memory_order_acquire);
return true;
}
};
// -------------------------------------------------------------------------- //
/** Pause execution for a "short" period of time.
**/
static void short_pause() {
#if (defined(__i386__) || defined(__x86_64__)) && defined(USE_MM_PAUSE)
_mm_pause();
#else
::std::this_thread::yield();
#endif
}
/** Run some function for some bounded time, throws 'Exception::BoundedOverrun' on overtime.
* @param dur Maximum execution duration
* @param func Function to run (void -> void)
* @param emsg Null-terminated error message
**/
template<class Rep, class Period, class Func> static void bounded_run(::std::chrono::duration<Rep, Period> const& dur, Func&& func, char const* emsg) {
::std::mutex lock;
::std::unique_lock<decltype(lock)> guard{lock};
::std::condition_variable cv;
::std::thread runner{[&]() {
func();
{ // Notify master
::std::unique_lock<decltype(lock)> guard{lock};
cv.notify_all();
}
}};
if (cv.wait_for(guard, dur) != std::cv_status::no_timeout) {
runner.detach();
throw Exception::BoundedOverrun{emsg};
}
runner.join();
}
/** Spin barrier class.
**/
class Barrier final {
public:
/** Counter class.
**/
using Counter = uint_fast32_t;
/** Mode enum class.
**/
enum class Mode {
enter,
leave
};
private:
Counter cardinal; // Total number of threads that synchronize
::std::atomic<Counter> mutable step; // Step counters
::std::atomic<Mode> mutable mode; // Current mode
public:
/** Deleted copy constructor/assignment.
**/
Barrier(Barrier const&) = delete;
Barrier& operator=(Barrier const&) = delete;
/** Number of threads constructor.
* @param cardinal Non-null total number of threads synchronizing on this barrier
**/
Barrier(Counter cardinal): cardinal{cardinal}, step{0}, mode{Mode::enter} {}
public:
/** [thread-safe] Synchronize all the threads.
**/
void sync() const {
// Enter
if (step.fetch_add(1, ::std::memory_order_relaxed) + 1 == cardinal) { // Set leave mode
mode.store(Mode::leave, ::std::memory_order_release);
} else { // Wait for leave mode
while (unlikely(mode.load(::std::memory_order_acquire) != Mode::leave))
short_pause();
}
// Leave
if (step.fetch_sub(1, ::std::memory_order_relaxed) - 1 == 0) { // Set enter mode
mode.store(Mode::enter, ::std::memory_order_release);
} else { // Wait for enter mode
while (unlikely(mode.load(::std::memory_order_acquire) != Mode::enter))
short_pause();
}
}
};

View File

@@ -0,0 +1,353 @@
/**
* @file grading.cpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Grading of the implementations.
**/
// External headers
#include <algorithm>
#include <atomic>
#include <cstring>
#include <iostream>
#include <random>
#include <variant>
// Internal headers
#include "common.hpp"
#include "transactional.hpp"
#include "workload.hpp"
// -------------------------------------------------------------------------- //
/** Tailored thread synchronization class.
**/
class Sync final {
private:
/** Synchronization status.
**/
enum class Status {
Wait, // Workers waiting each others, run as soon as all ready
Run, // Workers running (still full success)
Abort, // Workers running (>0 failure)
Done, // Workers done (all success)
Fail, // Workers done (>0 failure)
Quit // Workers must terminate
};
private:
unsigned int const nbworkers; // Number of workers to support
::std::atomic<unsigned int> nbready; // Number of thread having reached that state
::std::atomic<Status> status; // Current synchronization status
::std::atomic<char const*> errmsg; // Any one of the error message(s)
Chrono runtime; // Runtime between 'master_notify' and when the last worker finished
Latch donelatch; // For synchronization last worker -> master
public:
/** Deleted copy constructor/assignment.
**/
Sync(Sync const&) = delete;
Sync& operator=(Sync const&) = delete;
/** Worker count constructor.
* @param nbworkers Number of workers to support
**/
Sync(unsigned int nbworkers): nbworkers{nbworkers}, nbready{0}, status{Status::Done}, errmsg{nullptr} {}
public:
/** Master trigger "synchronized" execution in all threads (instead of joining).
**/
void master_notify() noexcept {
status.store(Status::Wait, ::std::memory_order_relaxed);
runtime.start();
}
/** Master trigger termination in all threads (instead of notifying).
**/
void master_join() noexcept {
status.store(Status::Quit, ::std::memory_order_relaxed);
}
/** Master wait for all workers to finish.
* @param maxtick Maximum number of ticks to wait before exiting the process on an error (optional, 'invalid_tick' for none)
* @return Total execution time on success, or error constant null-terminated string on failure
**/
::std::variant<Chrono, char const*> master_wait(Chrono::Tick maxtick = Chrono::invalid_tick) {
// Wait for all worker threads, synchronize-with the last one
if (!donelatch.wait(maxtick))
throw Exception::BoundedOverrun{"Transactional library takes too long to process the transactions"};
// Return runtime on success, of error message on failure
switch (status.load(::std::memory_order_relaxed)) {
case Status::Done:
return runtime;
case Status::Fail:
return errmsg;
default:
throw Exception::Unreachable{"Master woke after raised latch, no timeout, but unexpected status"};
}
}
/** Worker spin-wait until next run.
* @return Whether the worker can proceed, or quit otherwise
**/
bool worker_wait() noexcept {
while (true) {
auto res = status.load(::std::memory_order_relaxed);
if (res == Status::Wait)
break;
if (res == Status::Quit)
return false;
short_pause();
}
auto res = nbready.fetch_add(1, ::std::memory_order_relaxed);
if (res + 1 == nbworkers) { // Latest worker, switch to run status
nbready.store(0, ::std::memory_order_relaxed);
status.store(Status::Run, ::std::memory_order_release); // Synchronize-with previous worker waiting for run/abort state
} else do { // Not latest worker, wait for run status
short_pause();
auto res = status.load(::std::memory_order_acquire); // Synchronize-with latest worker switching to run/abort state
if (res == Status::Run || res == Status::Abort)
break;
} while (true);
return true;
}
/** Worker notify termination of its run.
* @param error Error constant null-terminated string ('nullptr' for none)
**/
void worker_notify(char const* error) noexcept {
if (error) {
errmsg.store(error, ::std::memory_order_relaxed);
status.store(Status::Abort, ::std::memory_order_relaxed);
}
auto&& res = nbready.fetch_add(1, ::std::memory_order_acq_rel); // Synchronize-with previous worker(s) potentially setting aborted status
if (res + 1 == nbworkers) { // Latest worker, switch to done/fail status
nbready.store(0, ::std::memory_order_relaxed);
status.store(status.load(::std::memory_order_relaxed) == Status::Abort ? Status::Fail : Status::Done, ::std::memory_order_relaxed);
runtime.stop();
donelatch.raise(); // Synchronize-with 'master_wait'
}
}
};
/** Measure the arithmetic mean of the execution time of the given workload with the given transaction library.
* @param workload Workload instance to use
* @param nbthreads Number of concurrent threads to use
* @param nbrepeats Number of repetitions (keep the median)
* @param seed Seed to use for performance measurements
* @param maxtick_init Timeout for (re)initialization ('Chrono::invalid_tick' for none)
* @param maxtick_perf Timeout for performance measurements ('Chrono::invalid_tick' for none)
* @param maxtick_chck Timeout for correctness check ('Chrono::invalid_tick' for none)
* @return Error constant null-terminated string ('nullptr' for none), execution times (in ns) (undefined if inconsistency detected)
**/
static auto measure(Workload& workload, unsigned int const nbthreads, unsigned int const nbrepeats, Seed seed, Chrono::Tick maxtick_init, Chrono::Tick maxtick_perf, Chrono::Tick maxtick_chck) {
::std::vector<::std::thread> threads(nbthreads);
::std::mutex cerrlock; // To avoid interleaving writes to 'cerr' in case more than one thread throw
Sync sync{nbthreads}; // "As-synchronized-as-possible" starts so that threads interfere "as-much-as-possible"
// We start nbthreads threads to measure performance.
for (unsigned int i = 0; i < nbthreads; ++i) { // Start threads
try {
threads[i] = ::std::thread{[&](unsigned int i) {
// This is the workload that all threads run simulataneously.
// It is devided into a series of small tests. Each test is specified in workload.hpp.
// Threads are synchronized between each test so that they run with a lot of concurrency.
try {
// 1. Initialization
if (!sync.worker_wait()) return; // Sync. of threads
sync.worker_notify(workload.init()); // Runs the test and tells the master about errors
// 2. Performance measurements
for (unsigned int count = 0; count < nbrepeats; ++count) {
if (!sync.worker_wait()) return;
sync.worker_notify(workload.run(i, seed + nbthreads * count + i));
}
// 3. Correctness check
if (!sync.worker_wait()) return;
sync.worker_notify(workload.check(i, std::random_device{}())); // Random seed is wanted here
// Synchronized quit
if (!sync.worker_wait()) return;
throw Exception::Unreachable{"unexpected worker iteration after checks"};
} catch (::std::exception const& err) {
sync.worker_notify("Internal worker exception(s)"); // Exception post-'Sync::worker_wait' (i.e. in 'Workload::run' or 'Workload::check'), since 'Sync::worker_*' do not throw
{ // Print the error
::std::unique_lock<decltype(cerrlock)> guard{cerrlock};
::std::cerr << "⎪⎧ *** EXCEPTION ***" << ::std::endl << "⎪⎩ " << err.what() << ::std::endl;
}
return;
}
}, i};
} catch (...) {
for (unsigned int j = 0; j < i; ++j) // Detach threads to avoid termination due to attached thread going out of scope
threads[j].detach();
throw;
}
}
// This is the master that synchronizes the worker threads.
// It basically triggers each step seen above.
// After all tests succeed, it returns the time it took to run each test.
// It returns early in case of a failure.
try {
char const* error = nullptr;
Chrono::Tick time_init = Chrono::invalid_tick;
Chrono::Tick times[nbrepeats];
Chrono::Tick time_chck = Chrono::invalid_tick;
auto const posmedian = nbrepeats / 2;
{ // Initialization (with cheap correctness test)
sync.master_notify(); // We tell workers to start working.
auto res = sync.master_wait(maxtick_init); // If running the student's version, it will timeout if way slower than the reference.
if (unlikely(::std::holds_alternative<char const*>(res))) { // If an error happened (timeout or violation), we return early!
error = ::std::get<char const*>(res);
goto join;
}
time_init = ::std::get<Chrono>(res).get_tick();
}
{ // Performance measurements (with cheap correctness tests)
for (unsigned int i = 0; i < nbrepeats; ++i) {
sync.master_notify();
auto res = sync.master_wait(maxtick_perf);
if (unlikely(::std::holds_alternative<char const*>(res))) {
error = ::std::get<char const*>(res);
goto join;
}
times[i] = ::std::get<Chrono>(res).get_tick();
}
::std::nth_element(times, times + posmedian, times + nbrepeats); // Partition times around the median
}
{ // Correctness check
sync.master_notify();
auto res = sync.master_wait(maxtick_chck);
if (unlikely(::std::holds_alternative<char const*>(res))) {
error = ::std::get<char const*>(res);
goto join;
}
time_chck = ::std::get<Chrono>(res).get_tick();
}
join: { // Joining
sync.master_join(); // Join with threads
for (unsigned int i = 0; i < nbthreads; ++i)
threads[i].join();
}
return ::std::make_tuple(error, time_init, times[posmedian], time_chck);
} catch (...) {
for (unsigned int i = 0; i < nbthreads; ++i) // Detach threads to avoid termination due to attached thread going out of scope
threads[i].detach();
throw;
}
}
// -------------------------------------------------------------------------- //
/** Program entry point.
* @param argc Arguments count
* @param argv Arguments values
* @return Program return code
**/
int main(int argc, char** argv) {
try {
// Parse command line option(s)
if (argc < 3) {
::std::cout << "Usage: " << (argc > 0 ? argv[0] : "grading") << " <seed> <reference library path> <tested library path>..." << ::std::endl;
return 1;
}
// Get/set/compute run parameters
auto const nbworkers = []() {
auto res = ::std::thread::hardware_concurrency();
if (unlikely(res == 0))
res = 16;
return static_cast<size_t>(res);
}();
auto const nbtxperwrk = 200000ul / nbworkers;
auto const nbaccounts = 32 * nbworkers;
auto const expnbaccounts = 256 * nbworkers;
auto const init_balance = 100ul;
auto const prob_long = 0.5f;
auto const prob_alloc = 0.01f;
auto const nbrepeats = 7;
auto const seed = static_cast<Seed>(::std::stoul(argv[1]));
auto const clk_res = Chrono::get_resolution();
auto const slow_factor = 16ul;
// Print run parameters
::std::cout << "⎧ #worker threads: " << nbworkers << ::std::endl;
::std::cout << "⎪ #TX per worker: " << nbtxperwrk << ::std::endl;
::std::cout << "⎪ #repetitions: " << nbrepeats << ::std::endl;
::std::cout << "⎪ Initial #accounts: " << nbaccounts << ::std::endl;
::std::cout << "⎪ Expected #accounts: " << expnbaccounts << ::std::endl;
::std::cout << "⎪ Initial balance: " << init_balance << ::std::endl;
::std::cout << "⎪ Long TX probability: " << prob_long << ::std::endl;
::std::cout << "⎪ Allocation TX prob.: " << prob_alloc << ::std::endl;
::std::cout << "⎪ Slow trigger factor: " << slow_factor << ::std::endl;
::std::cout << "⎪ Clock resolution: ";
if (unlikely(clk_res == Chrono::invalid_tick)) {
::std::cout << "<unknown>" << ::std::endl;
} else {
::std::cout << clk_res << " ns" << ::std::endl;
}
::std::cout << "⎩ Seed value: " << seed << ::std::endl;
// Library evaluations
double reference = 0.; // Set to avoid irrelevant '-Wmaybe-uninitialized'
auto const pertxdiv = static_cast<double>(nbworkers) * static_cast<double>(nbtxperwrk);
auto maxtick_init = Chrono::invalid_tick;
auto maxtick_perf = Chrono::invalid_tick;
auto maxtick_chck = Chrono::invalid_tick;
for (auto i = 2; i < argc; ++i) {
::std::cout << "⎧ Evaluating '" << argv[i] << "'" << (maxtick_init == Chrono::invalid_tick ? " (reference)" : "") << "..." << ::std::endl;
// Load TM library
TransactionalLibrary tl{argv[i]};
// Initialize workload (shared memory lifetime bound to workload: created and destroyed at the same time)
WorkloadBank bank{tl, nbworkers, nbtxperwrk, nbaccounts, expnbaccounts, init_balance, prob_long, prob_alloc};
try {
// Actual performance measurements and correctness check
auto res = measure(bank, nbworkers, nbrepeats, seed, maxtick_init, maxtick_perf, maxtick_chck);
// Check false negative-free correctness
auto error = ::std::get<0>(res);
if (unlikely(error)) {
::std::cout << "" << error << ::std::endl;
return 1;
}
// Print results
auto tick_init = ::std::get<1>(res);
auto tick_perf = ::std::get<2>(res);
auto tick_chck = ::std::get<3>(res);
auto perfdbl = static_cast<double>(tick_perf);
::std::cout << "⎪ Total user execution time: " << (perfdbl / 1000000.) << " ms";
if (maxtick_init == Chrono::invalid_tick) { // Set reference performance
maxtick_init = slow_factor * tick_init;
if (unlikely(maxtick_init == Chrono::invalid_tick)) // Bad luck...
++maxtick_init;
maxtick_perf = slow_factor * tick_perf;
if (unlikely(maxtick_perf == Chrono::invalid_tick)) // Bad luck...
++maxtick_perf;
maxtick_chck = slow_factor * tick_chck;
if (unlikely(maxtick_chck == Chrono::invalid_tick)) // Bad luck...
++maxtick_chck;
reference = perfdbl;
} else { // Compare with reference performance
::std::cout << " -> " << (reference / perfdbl) << " speedup";
}
::std::cout << ::std::endl;
::std::cout << "⎩ Average TX execution time: " << (perfdbl / pertxdiv) << " ns" << ::std::endl;
} catch (::std::exception const& err) { // Special case: cannot unload library with running threads, so print error and quick-exit
::std::cerr << "⎪ *** EXCEPTION ***" << ::std::endl;
::std::cerr << "" << err.what() << ::std::endl;
::std::quick_exit(2);
}
}
return 0;
} catch (::std::exception const& err) {
::std::cerr << "⎧ *** EXCEPTION ***" << ::std::endl;
::std::cerr << "" << err.what() << ::std::endl;
return 1;
}
}

View File

@@ -0,0 +1,620 @@
/**
* @file transactional.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Transactional memory library management and use.
**/
#pragma once
// External headers
extern "C" {
#include <dlfcn.h>
#include <limits.h>
}
// Internal headers
namespace STM {
#include <tm.hpp>
}
#include "common.hpp"
// -------------------------------------------------------------------------- //
namespace Exception {
/** Exception tree.
**/
EXCEPTION(Path, Any, "path exception");
EXCEPTION(PathResolve, Path, "unable to resolve the given path");
EXCEPTION(Module, Any, "transaction library exception");
EXCEPTION(ModuleLoading, Module, "unable to load a transaction library");
EXCEPTION(ModuleSymbol, Module, "symbol not found in loaded libraries");
EXCEPTION(Transaction, Any, "transaction manager exception");
EXCEPTION(TransactionAlign, Transaction, "incorrect alignment detected before transactional operation");
EXCEPTION(TransactionReadOnly, Transaction, "tried to write/alloc/free using a read-only transaction");
EXCEPTION(TransactionCreate, Transaction, "shared memory region creation failed");
EXCEPTION(TransactionBegin, Transaction, "transaction begin failed");
EXCEPTION(TransactionAlloc, Transaction, "memory allocation failed (insufficient memory)");
EXCEPTION(TransactionRetry, Transaction, "transaction aborted and can be retried");
EXCEPTION(TransactionNotLastSegment, Transaction, "trying to deallocate the first segment");
EXCEPTION(Shared, Any, "operation in shared memory exception");
EXCEPTION(SharedAlign, Shared, "address in shared memory is not properly aligned for the specified type");
EXCEPTION(SharedOverflow, Shared, "index is past array length");
EXCEPTION(SharedDoubleAlloc, Shared, "(probable) double allocation detected before transactional operation");
EXCEPTION(SharedDoubleFree, Shared, "double free detected before transactional operation");
}
// -------------------------------------------------------------------------- //
/** Transactional library management class.
**/
class TransactionalLibrary final: private NonCopyable {
friend class TransactionalMemory;
private:
/** Function types.
**/
using FnCreate = decltype(&STM::tm_create);
using FnDestroy = decltype(&STM::tm_destroy);
using FnStart = decltype(&STM::tm_start);
using FnSize = decltype(&STM::tm_size);
using FnAlign = decltype(&STM::tm_align);
using FnBegin = decltype(&STM::tm_begin);
using FnEnd = decltype(&STM::tm_end);
using FnRead = decltype(&STM::tm_read);
using FnWrite = decltype(&STM::tm_write);
using FnAlloc = decltype(&STM::tm_alloc);
using FnFree = decltype(&STM::tm_free);
private:
void* module; // Module opaque handler
FnCreate tm_create; // Module's initialization function
FnDestroy tm_destroy; // Module's cleanup function
FnStart tm_start; // Module's start address query function
FnSize tm_size; // Module's size query function
FnAlign tm_align; // Module's alignment query function
FnBegin tm_begin; // Module's transaction begin function
FnEnd tm_end; // Module's transaction end function
FnRead tm_read; // Module's shared memory read function
FnWrite tm_write; // Module's shared memory write function
FnAlloc tm_alloc; // Module's shared memory allocation function
FnFree tm_free; // Module's shared memory freeing function
private:
/** Solve a symbol from its name, and bind it to the given function.
* @param name Name of the symbol to resolve
* @param func Target function to bind (optional, to use template parameter deduction)
**/
template<class Signature> auto solve(char const* name) const {
auto res = ::dlsym(module, name);
if (unlikely(!res))
throw Exception::ModuleSymbol{};
return *reinterpret_cast<Signature*>(&res);
}
template<class Signature> void solve(char const* name, Signature& func) const {
func = solve<Signature>(name);
}
public:
/** Loader constructor.
* @param path Path to the library to load
**/
TransactionalLibrary(char const* path) {
{ // Resolve path and load module
char resolved[PATH_MAX];
if (unlikely(!realpath(path, resolved)))
throw Exception::PathResolve{};
module = ::dlopen(resolved, RTLD_NOW | RTLD_LOCAL);
if (unlikely(!module))
throw Exception::ModuleLoading{};
}
{ // Bind module's 'tm_*' symbols
solve("tm_create", tm_create);
solve("tm_destroy", tm_destroy);
solve("tm_start", tm_start);
solve("tm_size", tm_size);
solve("tm_align", tm_align);
solve("tm_begin", tm_begin);
solve("tm_end", tm_end);
solve("tm_read", tm_read);
solve("tm_write", tm_write);
solve("tm_alloc", tm_alloc);
solve("tm_free", tm_free);
}
}
/** Unloader destructor.
**/
~TransactionalLibrary() noexcept {
::dlclose(module); // Close loaded module
}
};
/** One shared memory region management class.
**/
class TransactionalMemory final: private NonCopyable {
private:
/** Check whether the given alignment is a power of 2
**/
constexpr static bool is_power_of_two(size_t align) noexcept {
return align != 0 && (align & (align - 1)) == 0;
}
public:
/** Opaque shared memory region handle class.
**/
using Shared = STM::shared_t;
/** Transaction class alias.
**/
using TX = STM::tx_t;
private:
TransactionalLibrary const& tl; // Bound transactional library
Shared shared; // Handle of the shared memory region used
void* start_addr; // Shared memory region first segment's start address
size_t start_size; // Shared memory region first segment's size (in bytes)
size_t alignment; // Shared memory region alignment (in bytes)
public:
/** Bind constructor.
* @param library Transactional library to use
* @param align Shared memory region required alignment
* @param size Size of the shared memory region to allocate
**/
TransactionalMemory(TransactionalLibrary const& library, size_t align, size_t size): tl{library}, start_size{size}, alignment{align} {
if (unlikely(assert_mode && (!is_power_of_two(align) || size % align != 0)))
throw Exception::TransactionAlign{};
bounded_run(max_side_time, [&]() {
shared = tl.tm_create(size, align);
if (unlikely(shared == STM::invalid_shared))
throw Exception::TransactionCreate{};
start_addr = tl.tm_start(shared);
}, "The transactional library takes too long creating the shared memory");
}
/** Unbind destructor.
**/
~TransactionalMemory() noexcept {
bounded_run(max_side_time, [&]() {
tl.tm_destroy(shared);
}, "The transactional library takes too long destroying the shared memory");
}
public:
/** [thread-safe] Return the start address of the first shared segment.
* @return Address of the first allocated shared region
**/
auto get_start() const noexcept {
return start_addr;
}
/** [thread-safe] Return the size of the first shared segment.
* @return Size in the first allocated shared region (in bytes)
**/
auto get_size() const noexcept {
return start_size;
}
/** [thread-safe] Get the shared memory region global alignment.
* @return Global alignment (in bytes)
**/
auto get_align() const noexcept {
return alignment;
}
public:
/** [thread-safe] Begin a new transaction on the shared memory region.
* @param ro Whether the transaction is read-only
* @return Opaque transaction ID, 'STM::invalid_tx' on failure
**/
auto begin(bool ro) const noexcept {
return tl.tm_begin(shared, ro);
}
/** [thread-safe] End the given transaction.
* @param tx Opaque transaction ID
* @return Whether the whole transaction is a success
**/
auto end(TX tx) const noexcept {
return tl.tm_end(shared, tx);
}
/** [thread-safe] Read operation in the given transaction, source in the shared region and target in a private region.
* @param tx Transaction to use
* @param source Source start address
* @param size Source/target range
* @param target Target start address
* @return Whether the whole transaction can continue
**/
auto read(TX tx, void const* source, size_t size, void* target) const noexcept {
return tl.tm_read(shared, tx, source, size, target);
}
/** [thread-safe] Write operation in the given transaction, source in a private region and target in the shared region.
* @param tx Transaction to use
* @param source Source start address
* @param size Source/target range
* @param target Target start address
* @return Whether the whole transaction can continue
**/
auto write(TX tx, void const* source, size_t size, void* target) const noexcept {
return tl.tm_write(shared, tx, source, size, target);
}
/** [thread-safe] Memory allocation operation in the given transaction, throw if no memory available.
* @param tx Transaction to use
* @param size Size to allocate
* @param target Target start address
* @return Allocation status
**/
auto alloc(TX tx, size_t size, void** target) const noexcept {
return tl.tm_alloc(shared, tx, size, target);
}
/** [thread-safe] Memory freeing operation in the given transaction.
* @param tx Transaction to use
* @param target Target start address
* @return Whether the whole transaction can continue
**/
auto free(TX tx, void* target) const noexcept {
return tl.tm_free(shared, tx, target);
}
};
/** One transaction over a shared memory region management class.
**/
class Transaction final: private NonCopyable {
public:
/** Transaction mode class.
**/
enum class Mode: bool {
read_write = false,
read_only = true
};
private:
TransactionalMemory const& tm; // Bound transactional memory
STM::tx_t tx; // Opaque transaction handle
bool aborted; // Transaction was aborted
bool is_ro; // Whether the transaction is read-only (solely for assertion)
public:
/** Deleted copy constructor/assignment.
**/
Transaction(Transaction const&) = delete;
Transaction& operator=(Transaction const&) = delete;
/** Begin constructor.
* @param tm Transactional memory to bind
* @param ro Whether the transaction is read-only
**/
Transaction(TransactionalMemory const& tm, Mode ro): tm{tm}, tx{tm.begin(static_cast<bool>(ro))}, aborted{false}, is_ro{static_cast<bool>(ro)} {
if (unlikely(tx == STM::invalid_tx))
throw Exception::TransactionBegin{};
}
/** End destructor.
**/
~Transaction() noexcept(false) {
if (likely(!aborted)) {
if (unlikely(!tm.end(tx)))
throw Exception::TransactionRetry{};
}
}
public:
/** [thread-safe] Return the bound transactional memory instance.
* @return Bound transactional memory instance
**/
auto const& get_tm() const noexcept {
return tm;
}
public:
/** [thread-safe] Read operation in the bound transaction, source in the shared region and target in a private region.
* @param source Source start address
* @param size Source/target range
* @param target Target start address
**/
void read(void const* source, size_t size, void* target) {
if (unlikely(!tm.read(tx, source, size, target))) {
aborted = true;
throw Exception::TransactionRetry{};
}
}
/** [thread-safe] Write operation in the bound transaction, source in a private region and target in the shared region.
* @param source Source start address
* @param size Source/target range
* @param target Target start address
**/
void write(void const* source, size_t size, void* target) {
if (unlikely(assert_mode && is_ro))
throw Exception::TransactionReadOnly{};
if (unlikely(!tm.write(tx, source, size, target))) {
aborted = true;
throw Exception::TransactionRetry{};
}
}
/** [thread-safe] Memory allocation operation in the bound transaction, throw if no memory available.
* @param size Size to allocate
* @return Target start address
**/
void* alloc(size_t size) {
if (unlikely(assert_mode && is_ro))
throw Exception::TransactionReadOnly{};
void* target;
switch (tm.alloc(tx, size, &target)) {
case STM::Alloc::success:
return target;
case STM::Alloc::nomem:
throw Exception::TransactionAlloc{};
default: // STM::Alloc::abort
aborted = true;
throw Exception::TransactionRetry{};
}
}
/** [thread-safe] Memory freeing operation in the bound transaction.
* @param target Target start address
**/
void free(void* target) {
if (unlikely(assert_mode && is_ro))
throw Exception::TransactionReadOnly{};
if (unlikely(!tm.free(tx, target))) {
aborted = true;
throw Exception::TransactionRetry{};
}
}
};
// -------------------------------------------------------------------------- //
/** Shared read/write helper class.
* @param Type Specified type (array)
**/
template<class Type> class Shared {
protected:
Transaction& tx; // Bound transaction
Type* address; // Address in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @return Private copy of the content at the shared address
**/
Type read() const {
Type res;
tx.read(address, sizeof(Type), &res);
return res;
}
operator Type() const {
return read();
}
/** Write operation.
* @param source Private content to write at the shared address
**/
void write(Type const& source) const {
tx.write(&source, sizeof(Type), address);
}
void operator=(Type const& source) const {
return write(source);
}
public:
/** Address of the first byte after the entry.
* @return First byte after the entry
**/
void* after() const noexcept {
return address + 1;
}
};
template<class Type> class Shared<Type*> {
protected:
Transaction& tx; // Bound transaction
Type** address; // Address in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type**>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type*) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @return Private copy of the content at the shared address
**/
Type* read() const {
Type* res;
tx.read(address, sizeof(Type*), &res);
return res;
}
operator Type*() const {
return read();
}
/** Write operation.
* @param source Private content to write at the shared address
**/
void write(Type* source) const {
tx.write(&source, sizeof(Type*), address);
}
void operator=(Type* source) const {
return write(source);
}
/** Allocate and write operation.
* @param size Size to allocate (defaults to size of the underlying class)
* @return Private copy of the just-written content at the shared address
**/
Type* alloc(size_t size = 0) const {
if (unlikely(assert_mode && read() != nullptr))
throw Exception::SharedDoubleAlloc{};
auto addr = tx.alloc(size > 0 ? size: sizeof(Type));
write(reinterpret_cast<Type*>(addr));
return reinterpret_cast<Type*>(addr);
}
/** Free and write operation.
**/
void free() const {
if (unlikely(assert_mode && read() == nullptr))
throw Exception::SharedDoubleFree{};
tx.free(read());
write(nullptr);
}
public:
/** Address of the first byte after the entry.
* @return First byte after the entry
**/
void* after() const noexcept {
return address + 1;
}
};
template<class Type> class Shared<Type[]> {
protected:
Transaction& tx; // Bound transaction
Type* address; // Address of the first element in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @param index Index to read
* @return Private copy of the content at the shared address
**/
Type read(size_t index) const {
Type res;
tx.read(address + index, sizeof(Type), &res);
return res;
}
/** Write operation.
* @param index Index to write
* @param source Private content to write at the shared address
**/
void write(size_t index, Type const& source) const {
tx.write(tx, &source, sizeof(Type), address + index);
}
public:
/** Reference a cell.
* @param index Cell to reference
* @return Shared on that cell
**/
Shared<Type> operator[](size_t index) const {
return Shared<Type>{tx, address + index};
}
/** Address of the first byte after the entry.
* @param length Length of the array
* @return First byte after the entry
**/
void* after(size_t length) const noexcept {
return address + length;
}
};
template<class Type, size_t n> class Shared<Type[n]> {
protected:
Transaction& tx; // Bound transaction
Type* address; // Address of the first element in shared memory
public:
/** Binding constructor.
* @param tx Bound transaction
* @param address Address to bind to
**/
Shared(Transaction& tx, void* address): tx{tx}, address{reinterpret_cast<Type*>(address)} {
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % tx.get_tm().get_align() != 0))
throw Exception::SharedAlign{};
if (unlikely(assert_mode && reinterpret_cast<uintptr_t>(address) % alignof(Type) != 0))
throw Exception::SharedAlign{};
}
public:
/** Get the address in shared memory.
* @return Address in shared memory
**/
auto get() const noexcept {
return address;
}
public:
/** Read operation.
* @param index Index to read
* @return Private copy of the content at the shared address
**/
Type read(size_t index) const {
if (unlikely(assert_mode && index >= n))
throw Exception::SharedOverflow{};
Type res;
tx.read(address + index, sizeof(Type), &res);
return res;
}
/** Write operation.
* @param index Index to write
* @param source Private content to write at the shared address
**/
void write(size_t index, Type const& source) const {
if (unlikely(assert_mode && index >= n))
throw Exception::SharedOverflow{};
tx.write(tx, &source, sizeof(Type), address + index);
}
public:
/** Reference a cell.
* @param index Cell to reference
* @return Shared on that cell
**/
Shared<Type> operator[](size_t index) const {
if (unlikely(assert_mode && index >= n))
throw Exception::SharedOverflow{};
return Shared<Type>{tx, address + index};
}
/** Address of the first byte after the array.
* @return First byte after the array
**/
void* after() const noexcept {
return address + n;
}
};
// -------------------------------------------------------------------------- //
/** Repeat a given transaction until it commits.
* @param tm Transactional memory
* @param mode Transactional mode
* @param func Transaction closure (Transaction& -> ...)
* @return Returned value (or void) when the transaction committed
**/
template<class Func> static auto transactional(TransactionalMemory const& tm, Transaction::Mode mode, Func&& func) {
do {
try {
Transaction tx{tm, mode};
return func(tx);
} catch (Exception::TransactionRetry const&) {
continue;
}
} while (true);
}

View File

@@ -0,0 +1,389 @@
/**
* @file workload.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Workload base class and derived workload(s) implementations.
**/
#pragma once
// External headers
#include <cstdint>
#include <random>
// Internal headers
#include "common.hpp"
// -------------------------------------------------------------------------- //
/** Worker unique ID type.
**/
using Uid = uint_fast32_t;
/** Seed type.
**/
using Seed = uint_fast32_t;
/** Workload base class.
**/
class Workload {
protected:
TransactionalLibrary const& tl; // Associated transactional library
TransactionalMemory tm; // Built transactional memory to use
public:
/** Deleted copy constructor/assignment.
**/
Workload(Workload const&) = delete;
Workload& operator=(Workload const&) = delete;
/** Transactional memory constructor.
* @param library Transactional library to use
* @param align Shared memory region required alignment
* @param size Size of the shared memory region to allocate
**/
Workload(TransactionalLibrary const& library, size_t align, size_t size): tl{library}, tm{tl, align, size} {}
/** Virtual destructor.
**/
virtual ~Workload() {};
public:
/** Shared memory (re)initialization.
* @return Constant null-terminated error message, 'nullptr' for none
**/
virtual char const* init() const = 0;
/** [thread-safe] Worker's full run.
* @param Unique ID (between 0 to n-1)
* @param Seed to use
* @return Constant null-terminated error message, 'nullptr' for none
**/
virtual char const* run(Uid, Seed) const = 0;
/** [thread-safe] Worker's false negative-free check.
* @param Unique ID (between 0 to n-1)
* @param Seed to use
* @return Constant null-terminated error message, 'nullptr' for none
**/
virtual char const* check(Uid, Seed) const = 0;
};
// -------------------------------------------------------------------------- //
/** Bank workload class.
**/
class WorkloadBank final: public Workload {
public:
/** Account balance class alias.
**/
using Balance = intptr_t;
static_assert(sizeof(Balance) >= sizeof(void*), "Balance class is too small");
private:
/** Shared segment of accounts class.
**/
class AccountSegment final {
private:
/** Dummy structure for size and alignment retrieval.
**/
struct Dummy {
size_t dummy0;
void* dummy1;
Balance dummy2;
Balance dummy3[];
};
public:
/** Get the segment size for a given number of accounts.
* @param nbaccounts Number of accounts per segment
* @return Segment size (in bytes)
**/
constexpr static auto size(size_t nbaccounts) noexcept {
return sizeof(Dummy) + nbaccounts * sizeof(Balance);
}
/** Get the segment alignment for a given number of accounts.
* @return Segment size (in bytes)
**/
constexpr static auto align() noexcept {
return alignof(Dummy);
}
public:
Shared<size_t> count; // Number of allocated accounts in this segment
Shared<AccountSegment*> next; // Next allocated segment
Shared<Balance> parity; // Segment balance correction for when deleting an account
Shared<Balance[]> accounts; // Amount of money on the accounts (undefined if not allocated)
public:
/** Deleted copy constructor/assignment.
**/
AccountSegment(AccountSegment const&) = delete;
AccountSegment& operator=(AccountSegment const&) = delete;
/** Binding constructor.
* @param tx Associated pending transaction
* @param address Block base address
**/
AccountSegment(Transaction& tx, void* address): count{tx, address}, next{tx, count.after()}, parity{tx, next.after()}, accounts{tx, parity.after()} {}
};
private:
size_t nbworkers; // Number of concurrent workers
size_t nbtxperwrk; // Number of transactions per worker
size_t nbaccounts; // Initial number of accounts and number of accounts per segment
size_t expnbaccounts; // Expected total number of accounts
Balance init_balance; // Initial account balance
float prob_long; // Probability of running a long, read-only control transaction
float prob_alloc; // Probability of running an allocation/deallocation transaction, knowing a long transaction won't run
Barrier barrier; // Barrier for thread synchronization during 'check'
public:
/** Bank workload constructor.
* @param library Transactional library to use
* @param nbworkers Total number of concurrent threads (for both 'run' and 'check')
* @param nbtxperwrk Number of transactions per worker
* @param nbaccounts Initial number of accounts and number of accounts per segment
* @param expnbaccounts Initial number of accounts and number of accounts per segment
* @param init_balance Initial account balance
* @param prob_long Probability of running a long, read-only control transaction
* @param prob_alloc Probability of running an allocation/deallocation transaction, knowing a long transaction won't run
**/
WorkloadBank(TransactionalLibrary const& library, size_t nbworkers, size_t nbtxperwrk, size_t nbaccounts, size_t expnbaccounts, Balance init_balance, float prob_long, float prob_alloc): Workload{library, AccountSegment::align(), AccountSegment::size(nbaccounts)}, nbworkers{nbworkers}, nbtxperwrk{nbtxperwrk}, nbaccounts{nbaccounts}, expnbaccounts{expnbaccounts}, init_balance{init_balance}, prob_long{prob_long}, prob_alloc{prob_alloc}, barrier{nbworkers} {}
private:
/** Long read-only transaction, summing the balance of each account.
* @param count Loosely-updated number of accounts
* @return Whether no inconsistency has been found
**/
bool long_tx(size_t& nbaccounts) const {
return transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
auto count = 0ul; // Total number of accounts seen.
auto sum = Balance{0}; // Total balance on all seen accounts + parity ammount.
auto start = tm.get_start(); // The list of accounts starts at the first word of the shared memory region.
while (start) {
AccountSegment segment{tx, start}; // We interpret the memory as a segment/array of accounts.
decltype(count) segment_count = segment.count;
count += segment_count; // And accumulate the total number of accounts.
sum += segment.parity; // We also sum the money that results from the destruction of accounts.
for (decltype(count) i = 0; i < segment_count; ++i) {
Balance local = segment.accounts[i];
if (unlikely(local < 0)) // If one account has a negative balance, there's a consistency issue.
return false;
sum += local;
}
start = segment.next; // Accounts are stored in linked segments, we move to the next one.
}
nbaccounts = count;
return sum == static_cast<Balance>(init_balance * count); // Consistency check: no money should ever be destroyed or created out of thin air.
});
}
/** Account (de)allocation transaction, adding accounts with initial balance or removing them.
* @param trigger Trigger level that will decide whether to allocate or deallocate
**/
void alloc_tx(size_t trigger) const {
return transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
auto count = 0ul; // Total number of accounts seen.
void* prev = nullptr;
auto start = tm.get_start();
while (true) {
AccountSegment segment{tx, start};
decltype(count) segment_count = segment.count;
count += segment_count;
decltype(start) segment_next = segment.next;
if (!segment_next) { // Currently at the last segment
if (count > trigger && likely(count > 2)) { // If we have seen "too many" accounts, we will destroy one.
--segment_count; // Let's remove the last account from the last segment.
auto new_parity = segment.parity.read() + segment.accounts[segment_count] - init_balance; // We remove 1x the initial balance but don't break parity.
if (segment_count > 0) { // Just remove one account from the (last) segment without deallocating memory.
segment.count = segment_count;
segment.parity = new_parity;
} else { // If there's no one in the last segment anymore, we deallocate it.
if (unlikely(assert_mode && prev == nullptr))
throw Exception::TransactionNotLastSegment{};
AccountSegment prev_segment{tx, prev};
prev_segment.next.free();
prev_segment.parity = prev_segment.parity.read() + new_parity;
}
} else { // If we don't destroy any account, then let's create a new one.
if (segment_count < nbaccounts) { // If there's room in the last segment, then let's create the account in it without allocating memory.
segment.accounts[segment_count] = init_balance;
segment.count = segment_count + 1;
} else { // Otherwise, we really need to allocate memory for the new account.
AccountSegment next_segment{tx, segment.next.alloc(AccountSegment::size(nbaccounts))};
next_segment.count = 1;
next_segment.accounts[0] = init_balance;
}
}
return;
}
prev = start;
start = segment_next;
}
});
}
/** Short read-write transaction, transferring one unit from an account to an account (potentially the same).
* @param send_id Index of the sender account
* @param recv_id Index of the receiver account (potentially same as source)
* @return Whether the parameters were satisfying and the transaction committed on useful work
**/
bool short_tx(size_t send_id, size_t recv_id) const {
return transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
void* send_ptr = nullptr;
void* recv_ptr = nullptr;
// Get the account pointers in shared memory
auto start = tm.get_start();
while (true) {
AccountSegment segment{tx, start};
size_t segment_count = segment.count;
if (!send_ptr) {
if (send_id < segment_count) {
send_ptr = segment.accounts[send_id].get();
if (recv_ptr)
break;
} else {
send_id -= segment_count;
}
}
if (!recv_ptr) {
if (recv_id < segment_count) {
recv_ptr = segment.accounts[recv_id].get();
if (send_ptr)
break;
} else {
recv_id -= segment_count;
}
}
start = segment.next;
if (!start) // Current segment is the last segment
return false; // At least one account does not exist => do nothing
}
// Transfer the money if enough fund
Shared<Balance> sender{tx, send_ptr}; // Shared is a template that overloads copy to use tm_read/tm_write.
Shared<Balance> recver{tx, recv_ptr};
auto send_val = sender.read();
if (send_val > 0) {
sender = send_val - 1;
recver = recver.read() + 1;
}
return true;
});
}
public:
/**
* Initialize the first segment of accounts and check the initial ballance (2 transactions).
**/
virtual char const* init() const {
transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
AccountSegment segment{tx, tm.get_start()};
segment.count = nbaccounts;
for (size_t i = 0; i < nbaccounts; ++i)
segment.accounts[i] = init_balance;
});
auto correct = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
AccountSegment segment{tx, tm.get_start()};
return segment.accounts[0] == init_balance;
});
if (unlikely(!correct))
return "Violated consistency (check that committed writes in shared memory get visible to the following transactions' reads)";
return nullptr;
}
/**
* Run nbtxperwrk random transactions until completion.
* @param seed Randomness source
**/
virtual char const* run(Uid uid [[gnu::unused]], Seed seed) const {
::std::minstd_rand engine{seed};
::std::bernoulli_distribution long_dist{prob_long};
::std::bernoulli_distribution alloc_dist{prob_alloc};
::std::gamma_distribution<float> alloc_trigger(expnbaccounts, 1);
size_t count = nbaccounts;
for (size_t cntr = 0; cntr < nbtxperwrk; ++cntr) {
if (long_dist(engine)) { // We roll a dice and, if "lucky", run a long transaction.
if (unlikely(!long_tx(count))) // If it fails, then we return an error message.
return "Violated isolation or atomicity";
} else if (alloc_dist(engine)) { // Let's roll a dice again to trigger an allocation transaction.
alloc_tx(alloc_trigger(engine));
} else { // No luck with previous rolls, let's just run a short transaction.
::std::uniform_int_distribution<size_t> account{0, count - 1};
while (unlikely(!short_tx(account(engine), account(engine))));
}
}
{ // Last long transaction
size_t dummy;
if (!long_tx(dummy))
return "Violated isolation or atomicity";
}
return nullptr;
}
/**
* Test in which we check that multiple concurrent transactions can decrease a counter in a sequential manner.
* @param uid Id of the thread to run the check
**/
virtual char const* check(Uid uid, Seed seed [[gnu::unused]]) const {
constexpr size_t nbtxperwrk = 100;
barrier.sync();
if (uid == 0) { // Only the first thread initializes the shared memory.
// We first write the initial value,
auto init_counter = nbtxperwrk * nbworkers;
transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
counter = init_counter;
});
// And check in another transaction that it was written correctly.
auto correct = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
return counter == init_counter;
});
if (unlikely(!correct)) {
barrier.sync();
barrier.sync();
return "Violated consistency during initialization";
}
}
// In each thread,
barrier.sync();
for (size_t i = 0; i < nbtxperwrk; ++i) {
// We first fetch the last value of the counter,
auto last = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
return counter.read();
});
// And then we decrease the value of the counter after checking that it didn't increase since the last read.
auto correct = transactional(tm, Transaction::Mode::read_write, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
auto value = counter.read();
if (unlikely(value > last))
return false;
counter = value - 1;
return true;
});
if (unlikely(!correct)) {
barrier.sync();
return "Violated consistency, isolation or atomicity";
}
}
// Finally, a last transaction runs in the first thread to check that the counter reached 0 (i.e., each transaction decreased it by 1.).
barrier.sync();
if (uid == 0) {
auto correct = transactional(tm, Transaction::Mode::read_only, [&](Transaction& tx) {
Shared<size_t> counter{tx, tm.get_start()};
return counter == 0;
});
if (unlikely(!correct))
return "Violated consistency";
}
return nullptr;
}
};

View File

@@ -0,0 +1,61 @@
/**
* @file tm.h
* @author Sébastien ROUAULT <sebastien.rouault@epfl.ch>
* @author Antoine MURAT <antoine.murat@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2021 Sébastien ROUAULT.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Interface declaration for the transaction manager to use (C version).
* YOU SHOULD NOT MODIFY THIS FILE.
**/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
// -------------------------------------------------------------------------- //
typedef void* shared_t; // The type of a shared memory region
static shared_t const invalid_shared = NULL; // Invalid shared memory region
// Note: a uintptr_t is an unsigned integer that is big enough to store an
// address. Said differently, you can either use an integer to identify
// transactions, or an address (e.g., if you created an associated data
// structure).
typedef uintptr_t tx_t; // The type of a transaction identifier
static tx_t const invalid_tx = ~((tx_t) 0); // Invalid transaction constant
typedef int alloc_t;
static alloc_t const success_alloc = 0; // Allocation successful and the TX can continue
static alloc_t const abort_alloc = 1; // TX was aborted and could be retried
static alloc_t const nomem_alloc = 2; // Memory allocation failed but TX was not aborted
// -------------------------------------------------------------------------- //
shared_t tm_create(size_t, size_t);
void tm_destroy(shared_t);
void* tm_start(shared_t);
size_t tm_size(shared_t);
size_t tm_align(shared_t);
tx_t tm_begin(shared_t, bool);
bool tm_end(shared_t, tx_t);
bool tm_read(shared_t, tx_t, void const*, size_t, void*);
bool tm_write(shared_t, tx_t, void const*, size_t, void*);
alloc_t tm_alloc(shared_t, tx_t, size_t, void**);
bool tm_free(shared_t, tx_t, void*);

View File

@@ -0,0 +1,63 @@
/**
* @file tm.hpp
* @author Sébastien ROUAULT <sebastien.rouault@epfl.ch>
* @author Antoine MURAT <antoine.murat@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2021 Sébastien ROUAULT.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Interface declaration for the transaction manager to use (C++ version).
* YOU SHOULD NOT MODIFY THIS FILE.
**/
#pragma once
#include <cstddef>
#include <cstdint>
// -------------------------------------------------------------------------- //
using shared_t = void*; // The type of a shared memory region
constexpr static shared_t invalid_shared = nullptr; // Invalid shared memory region
// Note: a uintptr_t is an unsigned integer that is big enough to store an
// address. Said differently, you can either use an integer to identify
// transactions, or an address (e.g., if you created an associated data
// structure).
using tx_t = uintptr_t; // The type of a transaction identifier
constexpr static tx_t invalid_tx = ~(tx_t(0)); // Invalid transaction constant
enum class Alloc: int {
success = 0, // Allocation successful and the TX can continue
abort = 1, // TX was aborted and could be retried
nomem = 2 // Memory allocation failed but TX was not aborted
};
// -------------------------------------------------------------------------- //
extern "C" {
shared_t tm_create(size_t, size_t) noexcept;
void tm_destroy(shared_t) noexcept;
void* tm_start(shared_t) noexcept;
size_t tm_size(shared_t) noexcept;
size_t tm_align(shared_t) noexcept;
tx_t tm_begin(shared_t, bool) noexcept;
bool tm_end(shared_t, tx_t) noexcept;
bool tm_read(shared_t, tx_t, void const*, size_t, void*) noexcept;
bool tm_write(shared_t, tx_t, void const*, size_t, void*) noexcept;
Alloc tm_alloc(shared_t, tx_t, size_t, void**) noexcept;
bool tm_free(shared_t, tx_t, void*) noexcept;
}

View File

@@ -0,0 +1,48 @@
BIN := $(notdir $(lastword $(abspath .)))
EXT_H := h
EXT_HPP := h hh hpp hxx h++
EXT_C := c
EXT_CXX := C cc cpp cxx c++
INCLUDE_DIR := .
SOURCE_DIR := .
WILD_EXT = $(strip $(foreach EXT,$($(1)),$(wildcard $(2)/*.$(EXT))))
HDRS_C := $(call WILD_EXT,EXT_H,$(INCLUDE_DIR))
HDRS_CXX := $(call WILD_EXT,EXT_HPP,$(INCLUDE_DIR))
SRCS_C := $(call WILD_EXT,EXT_C,$(SOURCE_DIR))
SRCS_CXX := $(call WILD_EXT,EXT_CXX,$(SOURCE_DIR))
OBJS := $(SRCS_C:%=%.o) $(SRCS_CXX:%=%.o)
CC := $(CC)
CCFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c11 -fPIC -I$(INCLUDE_DIR)
CXX := $(CXX)
CXXFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c++17 -fPIC -I$(INCLUDE_DIR)
LD := $(if $(SRCS_CXX),$(CXX),$(CC))
LDFLAGS :=
LDLIBS := -lpthread
.PHONY: build run clean
build: $(BIN)
run: $(BIN)
./$(BIN)
clean:
$(RM) $(OBJS) $(BIN)
define BUILD_C
%.$(1).o: %.$(1) $$(HDRS_C) Makefile
$$(CC) $$(CCFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_C),$(eval $(call BUILD_C,$(EXT))))
define BUILD_CXX
%.$(1).o: %.$(1) $$(HDRS_CXX) Makefile
$$(CXX) $$(CXXFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_CXX),$(eval $(call BUILD_CXX,$(EXT))))
$(BIN): $(OBJS) Makefile
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)

View File

@@ -0,0 +1,77 @@
/**
* @file entrypoint.cpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* "Entry point" source file, implementing the playground function 'entry_point' and the lock.
**/
// External headers
#include <atomic>
#include <iostream>
#include <mutex>
// Internal headers
#include "entrypoint.hpp"
#include "runner.hpp"
// -------------------------------------------------------------------------- //
// Lock implementation, that you have to complete
// NOTE: You may want to add data member(s) in 'class Lock' at entrypoint.hpp:30
/** Lock default constructor.
**/
Lock::Lock() {
// ...
}
/** Lock destructor.
**/
Lock::~Lock() {
// ...
}
/** [thread-safe] Acquire the lock, block if it is already acquired.
**/
void Lock::lock() {
// ...
}
/** [thread-safe] Release the lock, assuming it is indeed held by the caller.
**/
void Lock::unlock() {
// ...
}
// -------------------------------------------------------------------------- //
// Thread accessing the shared memory (a mere shared counter in this program)
/** Thread entry point.
* @param nb Total number of threads
* @param id This thread ID (from 0 to nb-1 included)
* @param lock Lock to use to protect the shared memory (read & written by 'shared_access')
**/
void entry_point(size_t nb, size_t id, Lock& lock) {
::printf("Hello from thread %lu/%lu\n", id, nb);
for (int i = 0; i < 10000; ++i) {
::std::lock_guard<Lock> guard{lock}; // Lock is acquired here
::shared_access();
// Lock is automatically released here (thanks to 'lock_guard', upon leaving the scope)
}
}

View File

@@ -0,0 +1,48 @@
/**
* @file entrypoint.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Interface for the "entry point" source file.
**/
#pragma once
// -------------------------------------------------------------------------- //
/** Your lock class.
**/
class Lock final {
public:
/** Deleted copy/move constructor/assignment.
**/
Lock(Lock const&) = delete;
Lock& operator=(Lock const&) = delete;
// NOTE: Actually, one could argue it makes sense to implement move,
// but we don't care about this feature in our simple playground
public:
Lock();
~Lock();
public:
void lock();
void unlock();
};
// -------------------------------------------------------------------------- //
void entry_point(size_t, size_t, Lock&);

View File

@@ -0,0 +1,85 @@
/**
* @file runner.cpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Trivial program that call a function in several threads.
**/
// External headers
#include <atomic>
#include <iostream>
#include <thread>
// Internal headers
#include "entrypoint.hpp"
#include "runner.hpp"
// -------------------------------------------------------------------------- //
// Shared memory, access function and consistency check
static int counter = 0;
static ::std::atomic<int> check_counter{0};
/** Performs some operations on some shared memory.
**/
void shared_access() {
++counter;
check_counter.fetch_add(1, ::std::memory_order_relaxed);
}
/** (Empirically) checks that concurrent operations did not break consistency, warn accordingly.
**/
static void shared_check() {
auto calls = check_counter.load(::std::memory_order_relaxed);
if (counter == calls) {
::std::cout << "** No inconsistency detected (" << counter << " == " << calls << ") **" << ::std::endl;
} else {
::std::cout << "** Inconsistency detected (" << counter << " != " << calls << ") **" << ::std::endl;
}
}
// -------------------------------------------------------------------------- //
// Lock + thread launches and management
/** Program entry point.
* @param argc Arguments count
* @param argv Arguments values
* @return Program return code
**/
int main(int, char**) {
auto const nbworkers = []() {
auto res = ::std::thread::hardware_concurrency();
if (res == 0) {
::std::cout << "WARNING: unable to query '::std::thread::hardware_concurrency()', falling back to 4 threads" << ::std::endl;
res = 4;
}
return static_cast<size_t>(res);
}();
Lock lock;
::std::thread threads[nbworkers];
for (size_t i = 0; i < nbworkers; ++i) {
threads[i] = ::std::thread{[&](size_t i) {
entry_point(nbworkers, i, lock);
}, i};
}
for (auto&& thread: threads)
thread.join();
shared_check();
return 0;
}

View File

@@ -0,0 +1,28 @@
/**
* @file runner.hpp
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2019 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Trivial program that call a function in several threads.
**/
#pragma once
// -------------------------------------------------------------------------- //
void shared_access();

View File

@@ -0,0 +1,46 @@
BIN := ../$(notdir $(lastword $(abspath .))).so
EXT_H := h
EXT_HPP := h hh hpp hxx h++
EXT_C := c
EXT_CXX := C cc cpp cxx c++
INCLUDE_DIR := ../include
SOURCE_DIR := .
WILD_EXT = $(strip $(foreach EXT,$($(1)),$(wildcard $(2)/*.$(EXT))))
HDRS_C := $(call WILD_EXT,EXT_H,$(INCLUDE_DIR))
HDRS_CXX := $(call WILD_EXT,EXT_HPP,$(INCLUDE_DIR))
SRCS_C := $(call WILD_EXT,EXT_C,$(SOURCE_DIR))
SRCS_CXX := $(call WILD_EXT,EXT_CXX,$(SOURCE_DIR))
OBJS := $(SRCS_C:%=%.o) $(SRCS_CXX:%=%.o)
CC := $(CC)
CCFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c11 -fPIC -I$(INCLUDE_DIR)
CXX := $(CXX)
CXXFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c++17 -fPIC -I$(INCLUDE_DIR)
LD := $(if $(SRCS_CXX),$(CXX),$(CC))
LDFLAGS := -shared
LDLIBS :=
.PHONY: build clean
build: $(BIN)
clean:
$(RM) $(OBJS) $(BIN)
define BUILD_C
%.$(1).o: %.$(1) $$(HDRS_C) Makefile
$$(CC) $$(CCFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_C),$(eval $(call BUILD_C,$(EXT))))
define BUILD_CXX
%.$(1).o: %.$(1) $$(HDRS_CXX) Makefile
$$(CXX) $$(CXXFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_CXX),$(eval $(call BUILD_CXX,$(EXT))))
$(BIN): $(OBJS) Makefile
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)

View File

@@ -0,0 +1,27 @@
#include "lock.h"
bool lock_init(struct lock_t* lock) {
return pthread_mutex_init(&(lock->mutex), NULL) == 0
&& pthread_cond_init(&(lock->cv), NULL) == 0;
}
void lock_cleanup(struct lock_t* lock) {
pthread_mutex_destroy(&(lock->mutex));
pthread_cond_destroy(&(lock->cv));
}
bool lock_acquire(struct lock_t* lock) {
return pthread_mutex_lock(&(lock->mutex)) == 0;
}
void lock_release(struct lock_t* lock) {
pthread_mutex_unlock(&(lock->mutex));
}
void lock_wait(struct lock_t* lock) {
pthread_cond_wait(&(lock->cv), &(lock->mutex));
}
void lock_wake_up(struct lock_t* lock) {
pthread_cond_broadcast(&(lock->cv));
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <pthread.h>
#include <stdbool.h>
/**
* @brief A lock that can only be taken exclusively. Contrarily to shared locks,
* exclusive locks have wait/wake_up capabilities.
*/
struct lock_t {
pthread_mutex_t mutex;
pthread_cond_t cv;
};
/** Initialize the given lock.
* @param lock Lock to initialize
* @return Whether the operation is a success
**/
bool lock_init(struct lock_t* lock);
/** Clean up the given lock.
* @param lock Lock to clean up
**/
void lock_cleanup(struct lock_t* lock);
/** Wait and acquire the given lock.
* @param lock Lock to acquire
* @return Whether the operation is a success
**/
bool lock_acquire(struct lock_t* lock);
/** Release the given lock.
* @param lock Lock to release
**/
void lock_release(struct lock_t* lock);
/** Wait until woken up by a signal on the given lock.
* The lock is released until lock_wait completes at which point it is acquired
* again. Exclusive lock access is enforced.
* @param lock Lock to release (until woken up) and wait on.
**/
void lock_wait(struct lock_t* lock);
/** Wake up all threads waiting on the given lock.
* @param lock Lock on which other threads are waiting.
**/
void lock_wake_up(struct lock_t* lock);

View File

@@ -0,0 +1,36 @@
#include <stdbool.h>
/** Define a proposition as likely true.
* @param prop Proposition
**/
#undef likely
#ifdef __GNUC__
#define likely(prop) \
__builtin_expect((prop) ? true : false, true /* likely */)
#else
#define likely(prop) \
(prop)
#endif
/** Define a proposition as likely false.
* @param prop Proposition
**/
#undef unlikely
#ifdef __GNUC__
#define unlikely(prop) \
__builtin_expect((prop) ? true : false, false /* unlikely */)
#else
#define unlikely(prop) \
(prop)
#endif
/** Define a variable as unused.
**/
#undef unused
#ifdef __GNUC__
#define unused(variable) \
variable __attribute__((unused))
#else
#define unused(variable)
#warning This compiler has no support for GCC attributes
#endif

View File

@@ -0,0 +1,25 @@
#include "shared-lock.h"
bool shared_lock_init(struct shared_lock_t* lock) {
return pthread_rwlock_init(&lock->rwlock, NULL) == 0;
}
void shared_lock_cleanup(struct shared_lock_t* lock) {
pthread_rwlock_destroy(&lock->rwlock);
}
bool shared_lock_acquire(struct shared_lock_t* lock) {
return pthread_rwlock_wrlock(&lock->rwlock) == 0;
}
void shared_lock_release(struct shared_lock_t* lock) {
pthread_rwlock_unlock(&lock->rwlock);
}
bool shared_lock_acquire_shared(struct shared_lock_t* lock) {
return pthread_rwlock_rdlock(&lock->rwlock) == 0;
}
void shared_lock_release_shared(struct shared_lock_t* lock) {
pthread_rwlock_unlock(&lock->rwlock);
}

View File

@@ -0,0 +1,53 @@
#pragma once
// Requested feature: pthread_rwlock_t
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#ifndef __USE_XOPEN2K
#define __USE_XOPEN2K
#endif
#include <pthread.h>
#include <stdbool.h>
/**
* @brief A lock that can be taken exclusively but also shared. Contrarily to
* exclusive locks, shared locks do not have wait/wake_up capabilities.
*/
struct shared_lock_t {
pthread_rwlock_t rwlock;
};
/** Initialize the given lock.
* @param lock Lock to initialize
* @return Whether the operation is a success
**/
bool shared_lock_init(struct shared_lock_t* lock);
/** Clean the given lock up.
* @param lock Lock to clean up
**/
void shared_lock_cleanup(struct shared_lock_t* lock);
/** Wait and acquire the given lock exclusively.
* @param lock Lock to acquire
* @return Whether the operation is a success
**/
bool shared_lock_acquire(struct shared_lock_t* lock);
/** Release the given lock that has been taken exclusively.
* @param lock Lock to release
**/
void shared_lock_release(struct shared_lock_t* lock);
/** Wait and acquire the given lock non-exclusively.
* @param lock Lock to acquire
* @return Whether the operation is a success
**/
bool shared_lock_acquire_shared(struct shared_lock_t* lock);
/** Release the given lock that has been taken non-exclusively.
* @param lock Lock to release
**/
void shared_lock_release_shared(struct shared_lock_t* lock);

View File

@@ -0,0 +1,198 @@
/**
* @file tm.c
* @author Sébastien Rouault <sebastien.rouault@epfl.ch>
* @author Antoine Murat <antoine.murat@epfl.ch>
*
* @section LICENSE
*
* Copyright © 2018-2021 Sébastien Rouault.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* any later version. Please see https://gnu.org/licenses/gpl.html
*
* This program 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. See the
* GNU General Public License for more details.
*
* @section DESCRIPTION
*
* Lock-based transaction manager implementation used as the reference.
**/
// Requested feature: posix_memalign
#define _POSIX_C_SOURCE 200809L
// External headers
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
// Internal headers
#include <tm.h>
#include "macros.h"
#include "shared-lock.h"
static const tx_t read_only_tx = UINTPTR_MAX - 10;
static const tx_t read_write_tx = UINTPTR_MAX - 11;
/**
* @brief List of dynamically allocated segments.
*/
struct segment_node {
struct segment_node* prev;
struct segment_node* next;
// uint8_t segment[] // segment of dynamic size
};
typedef struct segment_node* segment_list;
/**
* @brief Simple Shared Memory Region (a.k.a Transactional Memory).
*/
struct region {
struct shared_lock_t lock; // Global (coarse-grained) lock
void* start; // Start of the shared memory region (i.e., of the non-deallocable memory segment)
segment_list allocs; // Shared memory segments dynamically allocated via tm_alloc within transactions
size_t size; // Size of the non-deallocable memory segment (in bytes)
size_t align; // Size of a word in the shared memory region (in bytes)
};
shared_t tm_create(size_t size, size_t align) {
struct region* region = (struct region*) malloc(sizeof(struct region));
if (unlikely(!region)) {
return invalid_shared;
}
// We allocate the shared memory buffer such that its words are correctly
// aligned.
if (posix_memalign(&(region->start), align, size) != 0) {
free(region);
return invalid_shared;
}
if (!shared_lock_init(&(region->lock))) {
free(region->start);
free(region);
return invalid_shared;
}
memset(region->start, 0, size);
region->allocs = NULL;
region->size = size;
region->align = align;
return region;
}
void tm_destroy(shared_t shared) {
// Note: To be compatible with any implementation, shared_t is defined as a
// void*. For this particular implementation, the "real" type of a shared_t
// is a struct region*.
struct region* region = (struct region*) shared;
while (region->allocs) { // Free allocated segments
segment_list tail = region->allocs->next;
free(region->allocs);
region->allocs = tail;
}
free(region->start);
shared_lock_cleanup(&(region->lock));
free(region);
}
// Note: In this particular implementation, tm_start returns a valid virtual
// address (i.e., shared memory locations are virtually addressed).
// This is NOT required. Indeed, as the content of shared memory is only ever
// accessed via tm functions (read/write/free), you can use any naming scheme
// you want to designate a word within the transactional memory as long as it
// fits in a void*. Said functions will need to translate from a void* to a
// specific word. Moreover, your naming scheme should support pointer arithmetic
// (i.e., one should be able to pass tm_start(shared)+align*n to access the
// (n+1)-th word within a memory region).
// You can assume sizeof(void*) == 64b and that the maximum size ever allocated
// will be 2^48.
void* tm_start(shared_t shared) {
return ((struct region*) shared)->start;
}
size_t tm_size(shared_t shared) {
return ((struct region*) shared)->size;
}
size_t tm_align(shared_t shared) {
return ((struct region*) shared)->align;
}
tx_t tm_begin(shared_t shared, bool is_ro) {
// We let read-only transactions run in parallel by acquiring a shared
// access. On the other hand, read-write transactions acquire an exclusive
// access. At any point in time, the lock can be shared between any number
// of read-only transactions or held by a single read-write transaction.
if (is_ro) {
// Note: "unlikely" is a macro that helps branch prediction.
// It tells the compiler (GCC) that the condition is unlikely to be true
// and to optimize the code with this additional knowledge.
// It of course penalizes executions in which the condition turns up to
// be true.
if (unlikely(!shared_lock_acquire_shared(&(((struct region*) shared)->lock))))
return invalid_tx;
return read_only_tx;
} else {
if (unlikely(!shared_lock_acquire(&(((struct region*) shared)->lock))))
return invalid_tx;
return read_write_tx;
}
}
bool tm_end(shared_t shared, tx_t tx) {
if (tx == read_only_tx) {
shared_lock_release_shared(&(((struct region*) shared)->lock));
} else {
shared_lock_release(&(((struct region*) shared)->lock));
}
return true;
}
// Note: "unused" is a macro that tells the compiler that a variable is unused.
bool tm_read(shared_t unused(shared), tx_t unused(tx), void const* source, size_t size, void* target) {
memcpy(target, source, size);
return true;
}
bool tm_write(shared_t unused(shared), tx_t unused(tx), void const* source, size_t size, void* target) {
memcpy(target, source, size);
return true;
}
alloc_t tm_alloc(shared_t shared, tx_t unused(tx), size_t size, void** target) {
// We allocate the dynamic segment such that its words are correctly
// aligned. Moreover, the alignment of the 'next' and 'prev' pointers must
// be satisfied. Thus, we use align on max(align, struct segment_node*).
size_t align = ((struct region*) shared)->align;
align = align < sizeof(struct segment_node*) ? sizeof(void*) : align;
struct segment_node* sn;
if (unlikely(posix_memalign((void**)&sn, align, sizeof(struct segment_node) + size) != 0)) // Allocation failed
return nomem_alloc;
// Insert in the linked list
sn->prev = NULL;
sn->next = ((struct region*) shared)->allocs;
if (sn->next) sn->next->prev = sn;
((struct region*) shared)->allocs = sn;
void* segment = (void*) ((uintptr_t) sn + sizeof(struct segment_node));
memset(segment, 0, size);
*target = segment;
return success_alloc;
}
bool tm_free(shared_t shared, tx_t unused(tx), void* segment) {
struct segment_node* sn = (struct segment_node*) ((uintptr_t) segment - sizeof(struct segment_node));
// Remove from the linked list
if (sn->prev) sn->prev->next = sn->next;
else ((struct region*) shared)->allocs = sn->next;
if (sn->next) sn->next->prev = sn->prev;
free(sn);
return true;
}

View File

@@ -0,0 +1,236 @@
# coding: utf-8
###
# @file submit.py
# @author Sébastien Rouault <sebastien.rouault@alumni.epfl.ch>
#
# @section LICENSE
#
# Copyright © 2018-2019 École Polytechnique Fédérale de Lausanne (EPFL).
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# any later version. Please see https://gnu.org/licenses/gpl.html
#
# This program 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. See the
# GNU General Public License for more details.
#
# @section DESCRIPTION
#
# Client for the automated performance measurement tool of CS-453.
###
if __name__ != "__main__":
raise RuntimeError("Script " + repr(__file__) + " is to be used as the main module only")
# ---------------------------------------------------------------------------- #
# Python version check and imports
import sys
if sys.version_info.major != 3 or sys.version_info.minor < 5:
print("WARNING: python interpreter not supported, please install version 3.5 or compatible (e.g. 3.6, 3.7, etc)")
import argparse
import atexit
import pathlib
import socket
# ---------------------------------------------------------------------------- #
# Configuration
version_uid = b"\x00\x00\x00\x02" # Unique version identifier (must be identical in compatible server)
default_host = "lpdxeon2680.epfl.ch" # Default server hostname or IPv4
default_port = 9997 # Default server TCP port
max_codesize = 100000 # Max code size (in bytes) before AND after deflate (the same as in the server); modifying this value won't change the behavior of the server ;)
# ---------------------------------------------------------------------------- #
# Command line
# Description
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("--uuid",
type=str,
required=True,
help="Secret user unique identifier")
parser.add_argument("--host",
type=str,
default=default_host,
help="Server hostname")
parser.add_argument("--port",
type=int,
default=default_port,
help="Server TCP port")
parser.add_argument("--force-update",
action="store_true",
help="Update your submission to the server; no validation of the correctness of your submission will be performed")
parser.add_argument("--download",
action="store_true",
help="Download your submission currently kept for grading under 'zippath' (instead of submitting a new one); if '--force-update' is used, a \"swap\" is performed")
parser.add_argument("zippath",
type=str,
help="Path to a zip file containing your library code, or that will be overwritten with '--download'")
# Command line parsing
args = parser.parse_args(sys.argv[1:])
# ---------------------------------------------------------------------------- #
# Socket helper
def socket_encode_size(size):
""" Encode the given number into 4 bytes.
Args:
size Size to encode
Returns:
Encoded size
"""
# Assertions
if size < 0 or size >= (1 << 32):
raise OverflowError
# Encoding
res = bytearray(4)
for i in range(4):
res[i] = size & 0xFF
size >>= 8
return res
def socket_decode_size(size):
""" Decode the given 4-byte encoded size into an integer.
Args:
size Encoded size
Returns:
Decoded size
"""
# Assertions
if len(size) != 4:
raise OverflowError
# Decoding
res = 0
for i in range(4):
res <<= 8
res += size[3 - i]
return res
def socket_consume(fd, size):
""" Repeatedly read the socket until the given size has been received.
Args:
fd Socket file descriptor to read
size Size to read from the socket
"""
data = bytes()
while size > 0:
recv = fd.recv(size)
if len(recv) <= 0:
raise IOError("No more data in the socket")
data += recv
size -= len(recv)
return data
def socket_recvfield(fd, maxsize, exact=False):
""" Receive a field from a socket.
Args:
fd Socket file descriptor to read
maxsize Maximum/exact size to accept
exact Whether field size was exact
Returns:
Received field bytes
"""
size = socket_decode_size(socket_consume(fd, 4))
if size > maxsize:
raise IOError("Field is too large")
elif exact and size < maxsize:
raise IOError("Field is too small")
return socket_consume(fd, size)
def socket_sendfield(fd, data):
""" Send a field through a socket.
Args:
fd Socket file descriptor to write
data Data bytes to send
"""
if fd.sendall(socket_encode_size(len(data))) is not None or fd.sendall(data) is not None:
raise IOError("Send failed")
# ---------------------------------------------------------------------------- #
# Client
# Open connection with the server
client_fd = None
for af, socktype, proto, canonname, sa in socket.getaddrinfo(args.host, args.port, socket.AF_INET, socket.SOCK_STREAM):
try:
client_fd = socket.socket(af, socktype, proto)
except OSError as msg:
client_fd = None
continue
try:
client_fd.connect(sa)
except OSError as msg:
client_fd.close()
client_fd = None
continue
break
if client_fd is None:
print("""Unable to connect to %s:%s
Message to the students:
Please make sure you operate from EPFL's network (i.e. either on campus or through the VPN).
If you do, it is not unlikely that the server machine is temporarily being used for another purpose.
Please retry later; contact the TAs only if the problem persists for more than a day.""" % (args.host, args.port))
exit(1)
atexit.register(lambda: client_fd.close())
# Check version
if socket_recvfield(client_fd, len(version_uid)) != version_uid:
print("Protocol version mismatch with the server, please pull/download the latest version of the client.")
exit(1)
# Send the secret user identifier
socket_sendfield(client_fd, args.uuid.encode())
res = socket_recvfield(client_fd, 1, exact=True)
msg = {1: "Invalid user secret identifier", 2: "Unknown user secret identifier", 3: "User is already logged in"}
if res[0] in msg:
print(msg[res[0]]) # Unsuccessful identifications are logged
exit(1)
# Send mode of submission
submit_mode = (1 if args.download else 0) + (2 if args.force_update else 0)
socket_sendfield(client_fd, submit_mode.to_bytes(1, "little"))
# Process according to mode of submission
if args.force_update or not args.download:
# Send zip file
zip_path = pathlib.Path(args.zippath)
if not zip_path.exists():
print("File %r cannot be accessed" % str(zip_path))
exit(1)
socket_sendfield(client_fd, zip_path.read_bytes())
if args.download:
# Receive zip file
with pathlib.Path(args.zippath).open("wb") as fd:
fd.write(socket_recvfield(client_fd, max_codesize))
# Read until the socket is closed (for user feedback/error messages)
try:
prev = bytes()
while True:
data = client_fd.recv(256)
if len(data) <= 0:
# Here 'prev' should be empty or not enough data to decode 'prev' correctly: so do nothing
break
data = prev + data
prev = bytes()
try:
text = data.decode()
except UnicodeDecodeError as err:
prev = data[err.start:]
text = data[:err.start].decode()
if len(text) > 0:
sys.stdout.write(text)
sys.stdout.flush()
except ConnectionResetError:
pass
except KeyboardInterrupt:
pass

View File

@@ -0,0 +1,15 @@
LDFLAGS += -pthread
BIN=counter1 counter2 counter3 counter4 election1 election2 election3 election4 procon1 procon2 procon3 procon4
all: ${BIN}
.PHONY: all
clean:
rm *.o ${BIN}
.PHONY: clean
counter2: lock.o
election2: lock.o
procon2: lock.o
procon4: lock.o

View File

@@ -0,0 +1,73 @@
# Simple synchronization examples.
Modifying a variable that is concurrently accessed by another thread is
dangerous and will (most of the time) lead to Undefined Behaviors and Data
Races.
Through simple examples, we'll see how to use synchronization primitives to
build thread-safe programs using locks and atomic variables.
We build 3 examples:
- A counter. Each thread increments a shared counter repeatedly RUNS times.
We check that the counter = RUNS * THREADS at the end.
- Leader election. In each run, one thread is elected as the leader. We check
that exactly one thread considers itself as the leader in each run.
- Producer-Consumer. One thread generates data in a circular buffer while the
second tries to consume it. They have to be kept in sync.
These examples also show how to start threads and wait for their completion.
YOU DON'T HAVE TO START THREADS YOURSELVES IN THE DUAL-VERSIONED STM ALGO.
In the STM, threads are started by the users of your library (in which they
repeatedly (1) start a transaction, (2) read/write/alloc/free STM memory and (3)
commit the transaction.).
## Counter
### Bad approach #1
We employ no synchronization primitive: Undefined Behavior (UB), bad bad.
### Good approach #1
We take a big nice lock around the ++: correct.
### Bad approach #2
We use an atomic variable but use 2 non atomic operations: not UB, but doesn't
work.
### Good approach #2
We use an atomic variable and an atomic operation (fetch and add): correct.
## Leader election
### Bad approch # 1
We employ no synchronization primitive: Undefined Behavior (UB), bad bad.
### Good approach #1
We take a big nice lock around the test and set: correct.
### Bad approach #2
We use an atomic variable but use 2 non atomic operations: not UB, but doesn't
work.
### Good approach #2
We use an atomic variable and an atomic operation (compare and swap): correct.
## Producer-consumer
### Bad approach
We employ no synchronization primitive: Undefined Behavior (UB), bad bad.
### Okayish approach #1
We take a big nice lock when when writting/reading and checking the bounds:
correct.
### Okayish approach #2
We use atomic variables and fences: correct.
### Good approach
We use a conditional variable. :)
https://www.ibm.com/docs/en/i/7.1?topic=ssw_ibm_i_71/apis/users_78.htm
Conditional variables are synchronization primitives provided by the kernel
that let a thread sleep until it's woken up. Using a "cond var", a consummer
that realizes that data has not been generated yet can go to sleep instead of
busy waiting. It will then be woken up by the producer once the data is
generated. :)

View File

@@ -0,0 +1,42 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#define RUNS (4096 * 256)
#define THREADS 4
static int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
counter++;
// Equivalent to:
//
// int local = counter;
// counter = local + 1;
//
// This is not thread-safe as the increment is not atomic.
// **Incorrect**
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,45 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include "lock.h"
#define RUNS (4096 * 256)
#define THREADS 4
static struct lock_t lock;
static int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
counter++;
lock_release(&lock);
// Still equivalent, but only one thread accesses counter at a time, so it's
// thread safe, no concurrent access/data race. :)
// **Correct**
}
}
int main() {
lock_init(&lock);
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,38 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
int read_copy = counter;
counter = read_copy + 1;
// **Incorrect**, using atomic variables (~atomic registers is not enough).
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,37 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
atomic_fetch_add(&counter, 1);
// **Correct**, using an atomic operation (most likely) provided by the CPU.
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,48 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
// For each run (or round) we try to impose ourselves as the run's leader.
for (int r = 0; r < RUNS; r++) {
if (leader[r] == 0) {
leader[r] = tid;
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
// **Incorrect**, super wrong, plz synchronize.
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,54 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include "lock.h"
#define RUNS (4096 * 256)
#define THREADS 4
static struct lock_t lock;
static int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
if (leader[r] == 0) {
leader[r] = tid;
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
lock_release(&lock);
// **Correct**, a single thread tries to become leader at a time.
}
}
int main() {
lock_init(&lock);
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,48 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
for (int r = 0; r < RUNS; r++) {
if (leader[r] == 0) {
leader[r] = tid;
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
// **Incorrect**, atomic variables (~atomic registers) do not enforce atomic
// operation blocks.
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,48 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
for (int r = 0; r < RUNS; r++) {
int expected = 0;
if (atomic_compare_exchange_strong(&leader[r], &expected, tid)) {
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
// **Correct**, we use the Compare and Swap operation to atomically check if
// a leader has already been elected and if not, we impose ourselves. :)
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,27 @@
#include "lock.h"
bool lock_init(struct lock_t* lock) {
return pthread_mutex_init(&(lock->mutex), NULL) == 0
&& pthread_cond_init(&(lock->cv), NULL) == 0;
}
void lock_cleanup(struct lock_t* lock) {
pthread_mutex_destroy(&(lock->mutex));
pthread_cond_destroy(&(lock->cv));
}
bool lock_acquire(struct lock_t* lock) {
return pthread_mutex_lock(&(lock->mutex)) == 0;
}
void lock_release(struct lock_t* lock) {
pthread_mutex_unlock(&(lock->mutex));
}
void lock_wait(struct lock_t* lock) {
pthread_cond_wait(&(lock->cv), &(lock->mutex));
}
void lock_wake_up(struct lock_t* lock) {
pthread_cond_broadcast(&(lock->cv));
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <pthread.h>
#include <stdbool.h>
/**
* @brief A lock that can only be taken exclusively. Contrarily to shared locks,
* exclusive locks have wait/wake_up capabilities.
*/
struct lock_t {
pthread_mutex_t mutex;
pthread_cond_t cv;
};
/** Initialize the given lock.
* @param lock Lock to initialize
* @return Whether the operation is a success
**/
bool lock_init(struct lock_t* lock);
/** Clean up the given lock.
* @param lock Lock to clean up
**/
void lock_cleanup(struct lock_t* lock);
/** Wait and acquire the given lock.
* @param lock Lock to acquire
* @return Whether the operation is a success
**/
bool lock_acquire(struct lock_t* lock);
/** Release the given lock.
* @param lock Lock to release
**/
void lock_release(struct lock_t* lock);
/** Wait until woken up by a signal on the given lock.
* The lock is released until lock_wait completes at which point it is acquired
* again. Exclusive lock access is enforced.
* @param lock Lock to release (until woken up) and wait on.
**/
void lock_wait(struct lock_t* lock);
/** Wake up all threads waiting on the given lock.
* @param lock Lock on which other threads are waiting.
**/
void lock_wake_up(struct lock_t* lock);

View File

@@ -0,0 +1,72 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 1024
struct data {
char text[DATA_TEXT_SIZE];
};
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
// Super wrong: Overwritting data being read by the consumer.
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
consumed[r] = buffer[r % BUFFER_SIZE];
// Super wrong: Reading data concurrently (or not even) written by the
// producer.
}
}
int main() {
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,97 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#include "lock.h"
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 1024
struct data {
char text[DATA_TEXT_SIZE];
};
static struct lock_t lock;
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
int produced_until = 0;
int consumed_until = 0;
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
lock_acquire(&lock);
if (consumed_until + BUFFER_SIZE > r) break;
lock_release(&lock);
}
printf("can produce %d\n", r);
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
produced_until++;
lock_release(&lock);
// Correct: we wait until nobody consumed data anymore and correctly release
// (prevent reordering) thanks to the lock.
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
lock_acquire(&lock);
if (produced_until > r) break;
lock_release(&lock);
}
printf("can consume %d\n", r);
consumed[r] = buffer[r % BUFFER_SIZE];
consumed_until++;
lock_release(&lock);
// Correct: we wait until nobody produces data anymore and correctly acquire
// + release (prevent reordering) thanks to the lock.
}
}
int main() {
lock_init(&lock);
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,91 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 1024
struct data {
char text[DATA_TEXT_SIZE];
};
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
atomic_int produced_until = 0;
atomic_int consumed_until = 0;
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
// The memory order can be relaxed as we don't read anything "produced"
// by the consumer.
int local_cu = atomic_load_explicit(&consumed_until, memory_order_relaxed);
if (local_cu + BUFFER_SIZE > r) break;
}
printf("can produce %d\n", r);
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
// We want to increment "produced_until" after the buffer has been written.
// By using memory_order_release, we prevent the STOREs on buffer from being
// reordered after the atomic operation.
atomic_fetch_add_explicit(&produced_until, 1, memory_order_release);
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
// We don't want to access the buffer before checking the atomic variable.
// The memory_order_acquire prevents this reordering.
int local_pu = atomic_load_explicit(&produced_until, memory_order_acquire);
if (local_pu > r) break;
}
printf("can consume %d\n", r);
consumed[r] = buffer[r % BUFFER_SIZE];
atomic_fetch_add_explicit(&consumed_until, 1, memory_order_release);
}
}
int main() {
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,103 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#include "lock.h"
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 8
struct data {
char text[DATA_TEXT_SIZE];
};
static struct lock_t lock;
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
int produced_until = 0;
int consumed_until = 0;
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
while (true) {
if (consumed_until + BUFFER_SIZE > r) break;
lock_wait(&lock);
// Note: waiting releases the lock and puts the thread to sleep. Once
// woken up, it will need to acquire back the lock to continue its
// execution.
// Said differently, if multiple threads are waiting, upon "lock_wake_up",
// only one of them will continue the execution at a time (because the
// lock is exclusive).
}
printf("can produce %d\n", r);
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
produced_until++;
lock_release(&lock);
lock_wake_up(&lock); // We tell the consumer it can continue consuming.
// Correct: This is a better version of the lock one in which we do not busy
// wait but rely on a notification primitive to let the "idle" cores rest.
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
while (true) {
if (produced_until > r) break;
lock_wait(&lock);
}
printf("can consume %d\n", r);
consumed[r] = buffer[r % BUFFER_SIZE];
consumed_until++;
lock_release(&lock);
lock_wake_up(&lock); // We tell the producer it can continue producing.
}
}
int main() {
lock_init(&lock);
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,46 @@
BIN := ../$(notdir $(lastword $(abspath .))).so
EXT_H := h
EXT_HPP := h hh hpp hxx h++
EXT_C := c
EXT_CXX := C cc cpp cxx c++
INCLUDE_DIR := ../include
SOURCE_DIR := .
WILD_EXT = $(strip $(foreach EXT,$($(1)),$(wildcard $(2)/*.$(EXT))))
HDRS_C := $(call WILD_EXT,EXT_H,$(INCLUDE_DIR))
HDRS_CXX := $(call WILD_EXT,EXT_HPP,$(INCLUDE_DIR))
SRCS_C := $(call WILD_EXT,EXT_C,$(SOURCE_DIR))
SRCS_CXX := $(call WILD_EXT,EXT_CXX,$(SOURCE_DIR))
OBJS := $(SRCS_C:%=%.o) $(SRCS_CXX:%=%.o)
CC := $(CC)
CCFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c11 -fPIC -I$(INCLUDE_DIR)
CXX := $(CXX)
CXXFLAGS := -Wall -Wextra -Wfatal-errors -O2 -std=c++17 -fPIC -I$(INCLUDE_DIR)
LD := $(if $(SRCS_CXX),$(CXX),$(CC))
LDFLAGS := -shared
LDLIBS :=
.PHONY: build clean
build: $(BIN)
clean:
$(RM) $(OBJS) $(BIN)
define BUILD_C
%.$(1).o: %.$(1) $$(HDRS_C) Makefile
$$(CC) $$(CCFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_C),$(eval $(call BUILD_C,$(EXT))))
define BUILD_CXX
%.$(1).o: %.$(1) $$(HDRS_CXX) Makefile
$$(CXX) $$(CXXFLAGS) -c -o $$@ $$<
endef
$(foreach EXT,$(EXT_CXX),$(eval $(call BUILD_CXX,$(EXT))))
$(BIN): $(OBJS) Makefile
$(LD) $(LDFLAGS) -o $@ $(OBJS) $(LDLIBS)

View File

@@ -0,0 +1,36 @@
#include <stdbool.h>
/** Define a proposition as likely true.
* @param prop Proposition
**/
#undef likely
#ifdef __GNUC__
#define likely(prop) \
__builtin_expect((prop) ? true : false, true /* likely */)
#else
#define likely(prop) \
(prop)
#endif
/** Define a proposition as likely false.
* @param prop Proposition
**/
#undef unlikely
#ifdef __GNUC__
#define unlikely(prop) \
__builtin_expect((prop) ? true : false, false /* unlikely */)
#else
#define unlikely(prop) \
(prop)
#endif
/** Define a variable as unused.
**/
#undef unused
#ifdef __GNUC__
#define unused(variable) \
variable __attribute__((unused))
#else
#define unused(variable)
#warning This compiler has no support for GCC attributes
#endif

View File

@@ -0,0 +1,141 @@
/**
* @file tm.c
* @author Cedric Hölzl
*
* @section LICENSE
*
* [...]
*
* @section DESCRIPTION
*
* Implementation of your own transaction manager.
* You can completely rewrite this file (and create more files) as you wish.
* Only the interface (i.e. exported symbols and semantic) must be preserved.
**/
// Requested features
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#ifdef __STDC_NO_ATOMICS__
#error Current C11 compiler does not support atomic operations
#endif
// External headers
// Internal headers
#include <tm.h>
#include "macros.h"
/** Create (i.e. allocate + init) a new shared memory region, with one first non-free-able allocated segment of the requested size and alignment.
* @param size Size of the first shared segment of memory to allocate (in bytes), must be a positive multiple of the alignment
* @param align Alignment (in bytes, must be a power of 2) that the shared memory region must support
* @return Opaque shared memory region handle, 'invalid_shared' on failure
**/
shared_t tm_create(size_t unused(size), size_t unused(align)) {
// TODO: tm_create(size_t, size_t)
return invalid_shared;
}
/** Destroy (i.e. clean-up + free) a given shared memory region.
* @param shared Shared memory region to destroy, with no running transaction
**/
void tm_destroy(shared_t unused(shared)) {
// TODO: tm_destroy(shared_t)
}
/** [thread-safe] Return the start address of the first allocated segment in the shared memory region.
* @param shared Shared memory region to query
* @return Start address of the first allocated segment
**/
void* tm_start(shared_t unused(shared)) {
// TODO: tm_start(shared_t)
return NULL;
}
/** [thread-safe] Return the size (in bytes) of the first allocated segment of the shared memory region.
* @param shared Shared memory region to query
* @return First allocated segment size
**/
size_t tm_size(shared_t unused(shared)) {
// TODO: tm_size(shared_t)
return 0;
}
/** [thread-safe] Return the alignment (in bytes) of the memory accesses on the given shared memory region.
* @param shared Shared memory region to query
* @return Alignment used globally
**/
size_t tm_align(shared_t unused(shared)) {
// TODO: tm_align(shared_t)
return 0;
}
/** [thread-safe] Begin a new transaction on the given shared memory region.
* @param shared Shared memory region to start a transaction on
* @param is_ro Whether the transaction is read-only
* @return Opaque transaction ID, 'invalid_tx' on failure
**/
tx_t tm_begin(shared_t unused(shared), bool unused(is_ro)) {
// TODO: tm_begin(shared_t)
return invalid_tx;
}
/** [thread-safe] End the given transaction.
* @param shared Shared memory region associated with the transaction
* @param tx Transaction to end
* @return Whether the whole transaction committed
**/
bool tm_end(shared_t unused(shared), tx_t unused(tx)) {
// TODO: tm_end(shared_t, tx_t)
return false;
}
/** [thread-safe] Read operation in the given transaction, source in the shared region and target in a private region.
* @param shared Shared memory region associated with the transaction
* @param tx Transaction to use
* @param source Source start address (in the shared region)
* @param size Length to copy (in bytes), must be a positive multiple of the alignment
* @param target Target start address (in a private region)
* @return Whether the whole transaction can continue
**/
bool tm_read(shared_t unused(shared), tx_t unused(tx), void const* unused(source), size_t unused(size), void* unused(target)) {
// TODO: tm_read(shared_t, tx_t, void const*, size_t, void*)
return false;
}
/** [thread-safe] Write operation in the given transaction, source in a private region and target in the shared region.
* @param shared Shared memory region associated with the transaction
* @param tx Transaction to use
* @param source Source start address (in a private region)
* @param size Length to copy (in bytes), must be a positive multiple of the alignment
* @param target Target start address (in the shared region)
* @return Whether the whole transaction can continue
**/
bool tm_write(shared_t unused(shared), tx_t unused(tx), void const* unused(source), size_t unused(size), void* unused(target)) {
// TODO: tm_write(shared_t, tx_t, void const*, size_t, void*)
return false;
}
/** [thread-safe] Memory allocation in the given transaction.
* @param shared Shared memory region associated with the transaction
* @param tx Transaction to use
* @param size Allocation requested size (in bytes), must be a positive multiple of the alignment
* @param target Pointer in private memory receiving the address of the first byte of the newly allocated, aligned segment
* @return Whether the whole transaction can continue (success/nomem), or not (abort_alloc)
**/
alloc_t tm_alloc(shared_t unused(shared), tx_t unused(tx), size_t unused(size), void** unused(target)) {
// TODO: tm_alloc(shared_t, tx_t, size_t, void**)
return abort_alloc;
}
/** [thread-safe] Memory freeing in the given transaction.
* @param shared Shared memory region associated with the transaction
* @param tx Transaction to use
* @param target Address of the first byte of the previously allocated segment to deallocate
* @return Whether the whole transaction can continue
**/
bool tm_free(shared_t unused(shared), tx_t unused(tx), void* unused(target)) {
// TODO: tm_free(shared_t, tx_t, void*)
return false;
}