Doxygen Awesome

Yet Another Concurrency Library

GitHub license FOSSA status

Linux macOS Windows Sanitizers

Test coverage: coveralls Test coverage: codecov

Discord

Table of Contents

  • About YACLib
  • Getting started
  • Examples
    • Asynchronous pipeline
    • C++20 coroutine
    • Lazy pipeline
    • Thread pool
    • Strand, Serial executor
    • Mutex
    • Rescheduling
    • WhenAll
    • WhenAny
    • Future unwrapping
    • Timed wait
    • WaitGroup
    • Exception recovering
    • Error recovering
    • Using Result for smart recovering
  • Requirements
  • Releases
  • Contributing
  • Thanks
  • Used by
  • Contacts
  • License

About YACLib

YACLib is a lightweight C++ library for concurrent and parallel task execution, that is striving to satisfy the following properties:

  • Zero cost abstractions
  • Easy to use
  • Easy to build
  • Good test coverage

For more details check our design document and documentation.

Getting started

For quick start just paste this code in your CMakeLists.txt file.

include(FetchContent)

FetchContent_Declare(yaclib

GIT_REPOSITORY https://github.com/YACLib/YACLib.git

GIT_TAG main

)

FetchContent_MakeAvailable(yaclib)

link_libraries(yaclib)

For more details check install guide.

For more details about 'yaclib_std' or fault injection, check doc.

Examples

Here are short examples of using some features from YACLib, for details check documentation.

Asynchronous pipeline

return 42;

}).ThenInline([](int r) {

return r + 1;

}).Then([](int r) {

return std::to_string(r);

}).Detach(io_tp, [](std::string&& r) {

std::cout << "Pipeline result: <" << r << ">" << std::endl;

});

TODO(kononovk) Doxygen docs.

auto Run(Func &&f)

Execute Callable func on Inline executor.

We guarantee that no more than one allocation will be made for each step of the pipeline.

We have Then/Detach x IExecutor/previous step IExecutor/Inline.

Also Future/Promise don't contain shared atomic counters!

C++20 coroutine

co_return 42;

}

auto value = co_await task42();

co_return value + 1;

}

Provides a mechanism to access the result of async operations.

You can zero cost-combine Future coroutine code with Future callbacks code. That allows using YAClib for a smooth transfer from C++17 to C++20 with coroutines.

Also Future with coroutine doesn't make additional allocation for Future, only coroutine frame allocation that is caused by compiler, and can be optimized.

And finally co_await doesn't require allocation, so you can combine some async operation without allocation.

Lazy pipeline

return 1;

}).Then([] (int x) {

return x * 2;

});

task.Run();

auto Schedule(Func &&f)

Execute Callable func on Inline executor.

Same as asynchronous pipeline, but starting only after Run/ToFuture/Get. Task can be used as coroutine return type too.

Also running a Task that returns a Future doesn't make allocation. And it doesn't need synchronization, so it is even faster than asynchronous pipeline.

Thread pool

Submit(tp, [] {

});

});

tp.Stop();

tp.Wait();

void Submit(IExecutor &executor, Func &&f)

Submit given func for details.

Strand, Serial executor

for (std::size_t i = 0; i < 100; ++i) {

}).Then(strand, [](auto result) {

}).Then(io_tp, [] {

}

IExecutorPtr MakeStrand(IExecutorPtr e)

Strand is the asynchronous analogue of a mutex.

This is much more efficient than a mutex because

  1. don't block the threadpool thread.
  1. we execute critical sections in batches (the idea is known as flat-combining).

And also the implementation of strand is lock-free and efficient, without additional allocations.

Mutex

co_await On(tp);

auto guard = co_await m.Lock();

co_await guard.UnlockOn(io_tp);

};

for (std::size_t i = 0; i < 100; ++i) {

compute().Detach();

}

auto Lock() noexcept

Lock mutex.

YACLIB_INLINE detail::OnAwaiter On(IExecutor &e) noexcept

TODO(mkornaukhov03) Add doxygen docs.

First, this is the only correct mutex implementation for C++20 coroutines as far as I know (cppcoro, libunifex, folly::coro implement Unlock incorrectly, it serializes the code after Unlock)

Second, Mutex inherits all the Strand benefits.

Rescheduling

co_await On(cpu);

co_await On(io);

}

This is really zero-cost, just suspend the coroutine and submit its resume to another executor, without synchronization inside the coroutine and allocations anywhere.

WhenAll

std::vector<yaclib::Future<int>> fs;

for (std::size_t i = 0; i < 5; ++i) {

return random() * i;

}));

}

std::vector<int> unique_ints = std::move(all).Then([](std::vector<int> ints) {

ints.erase(std::unique(ints.begin(), ints.end()), ints.end());

return ints;

}).Get().Ok();

YACLIB_INLINE auto WhenAll(Futures... futures)

Doesn't make more than 3 allocations regardless of input size.

WhenAny

std::vector<yaclib::Future<int>> fs;

for (std::size_t i = 0; i < 5; ++i) {

return i;

}));

}

WhenAny(fs.begin(), fs.size()).Detach([](int i) {

});

YACLIB_INLINE auto WhenAny(Futures... futures)

Doesn't make more than 2 allocations regardless of input size.

Future unwrapping

std::cout << "Outer task" << std::endl;

return yaclib::Run(tp_compute, [] { return 42; });

}).Then( [](int result) {

result *= 13;

std::cout << "Result = " << result << std::endl;

});

});

Sometimes it's necessary to return from one async function the result of the other. It would be possible with the wait on this result. But this would cause blocking of the thread while waiting for the task to complete.

This problem can be solved using future unwrapping: when an async function returns a Future object, instead of setting its result to the Future object, the inner Future will "replace" the outer Future. This means that the outer Future will complete when the inner Future finishes and will acquire the result of the inner Future.

It also doesn't require additional allocations.

Timed wait

WaitFor(10ms, f1, f2);

Process(std::as_const(f1).Get());

assert(f1.Valid());

}

if (f2.Ready()) {

Process(std::move(f2).Get());

assert(!f2.Valid());

}

bool Valid() const &noexcept

Check if this Future has Promise.

bool Ready() const &noexcept

Check that Result that corresponds to this Future is computed.

Encapsulated return value from caller.

YACLIB_INLINE std::enable_if_t<(... &&is_waitable_with_timeout_v< Waited >), bool > WaitFor(const std::chrono::duration< Rep, Period > &timeout_duration, Waited &... fs) noexcept

Wait until the specified timeout duration has elapsed or Ready becomes true.

We support Wait/WaitFor/WaitUntil. Also all of them don't make allocation, and we have optimized the path for single Future (used in Future::Get()).

WaitGroup

wg.Add(2);

Submit(tp, [] {

wg.Done();

});

wg.Done();

});

wg.Attach(f1);

wg.Consume(std::move(f2));

co_await On(tp);

co_await wg;

std::cout << f1.Touch().Ok();

};

auto coro_f = coro();

wg.Done();

wg.Wait();

An object that allows you to Add some amount of async operations and then Wait for it to be Done.

Effective like simple atomic counter in intrusive pointer, also doesn't require any allocation.

Exception recovering

if (random() % 2) {

throw std::runtime_error{"1"};

}

return 42;

}).Then([](int y) {

if (random() % 2) {

throw std::runtime_error{"2"};

}

return y + 15;

}).Then([](int z) {

return z * 2;

}).Then([](std::exception_ptr e) {

try {

std::rethrow_exception(e);

} catch (const std::runtime_error& e) {

std::cout << e.what() << std::endl;

}

return 10;

});

int x = std::move(f).Get().Value();

Error recovering

if (random() % 2) {

return std::make_error_code(1);

}

return 42;

}).Then([](int y) {

if (random() % 2) {

return std::make_error_code(2);

}

return y + 15;

}).Then([](int z) {

return z * 2;

}).Then([](std::error_code ec) {

std::cout << ec.value() << std::endl;

return 10;

});

int x = std::move(f).Get().Value();

Contract< V, E > MakeContract()

Creates related future and promise.

Use Result for smart recovering

if (random() % 2) {

return std::make_error_code(1);

}

return 42;

}).Then([](int y) {

if (random() % 2) {

throw std::runtime_error{"2"};

}

return y + 15;

if (!z) {

return 10;

}

return std::move(z).Value();

});

int x = std::move(f).Get().Value();

Requirements

YACLib is a static library, that uses CMake as a build system and requires a compiler with C++17 or newer.

If the library doesn't compile on some compiler satisfying this condition, please create an issue. Pull requests with fixes are welcome!

We can also try to support older standards. If you are interested in it, check this discussion.

We test following configurations:

✅ - CI tested

👌 - manually tested

Compiler\OS Linux Windows macOS Android
GCC ✅ 7+ 👌 MinGW ✅ 7+ 👌
Clang ✅ 8+ ✅ ClangCL ✅ 8+ 👌
AppleClang ✅ 12+
MSVC ✅ 14.20+

MinGW works in CI early, check this.

Releases

YACLib follows the Abseil Live at Head philosophy (update to the latest commit from the main branch as often as possible).

So we recommend using the latest commit in the main branch in your projects.

This is safe because we suggest compiling YACLib from source, and each commit in main goes through dozens of test runs in various configurations. Our test coverage is 100%, to simplify, we run tests on the cartesian product of possible configurations:

os x compiler x stdlib x sanitizer x fault injection backend

However, we realize this philosophy doesn't work for every project, so we also provide Releases.

We don't believe in SemVer (check this), but we use a year.month.day[.patch] versioning approach. I'll release a new version if you ask, or I'll decide we have important or enough changes.

Contributing

We are always open for issues and pull requests. Check our good first issues.

For more details you can check the following links:

Thanks

Used by

Contacts

You can contact us by my email: valer.nosp@m.y.mi.nosp@m.ronow.nosp@m.@gma.nosp@m.il.co.nosp@m.m

Or join our Discord Server

License

YACLib is made available under MIT License. See [LICENSE](LICENSE) file for details.

We would be glad if you let us know that you're using our library.

FOSSA Status