userver: The Basics
Loading...
Searching...
No Matches
The Basics

Restrictions

Usage of catch (...) without throw; should be avoided as the framework may use exceptions not derived from std::exception to manage some resources. Usage of catch with explicit exception type specification (like std::exception or std::runtime_error) is fine without throw;.

🐙 userver uses its own coroutine scheduler, which is unknown to the C++ standard library, as well as to the libc/pthreads. The standard library for synchronization often uses mutexes, other synchronization primitives and event waiting mechanisms that block the current thread. When using userver, this results in the current thread not being able to be used to execute other coroutines. As a result, the number of threads executing coroutines decreases. This can lead to a huge performance drops and increased latencies.

For the reasons described above, the use of synchronization primitives or IO operations of the C++ standard library and libc in the main task processor should be avoided in high-load applications. The same goes for all functions and classes that use blocking IO operations or synchronization primitives.

⚠️🐙❗ Instead of the standard primitives, you need to use the primitives from the userver:

Standard primitive Replacement from userver
thread_local It depends, but do not use standard thread_local
std::this_thread::sleep_for() engine::SleepFor()
std::this_thread::sleep_until() engine::SleepUntil()
std::mutex engine::Mutex
std::shared_mutex engine::SharedMutex
std::condition_variable engine::ConditionVariable
std::future<T> engine::TaskWithResult<T> or engine::Future
std::async() utils::Async()
std::thread utils::Async()
std::counting_semaphore engine::Semaphore
network sockets engine::io::Socket
std::filesystem:: fs::* (but not fs::blocking::*!)
std::cout LOG_INFO()
std::cerr LOG_WARNING() and LOG_ERROR()

An overview of the main synchronization mechanisms is available on a separate page.

Note that if your application is not meant for high-load and does not require low-latency, then it may be fine to run all the code on the same task processor.


⚠️🐙❗ If you want to run code that uses standard synchronization primitives (for example, code from a third-party library), then this code should be run in a separate engine::TaskProcessor to avoid starvation of main task processors. See Guide on TaskProcessor Usage for more info.


Tasks

The asynchronous task (engine::Task, engine::TaskWithResult) can return a result (possibly in form of an exception) or return nothing. In any case, the task has the semantics of future, i.e. you can wait for it and get the result from it.

To create a task call the utils::Async function. It accepts the name of a task, the user-defined function to execute, and the arguments of the user-defined function:

auto task = utils::Async("my_job", &func, arg1, arg2);
// do something ...
auto result = task.Get();

Waiting

The code inside the coroutine may want to wait for an external event - a response from the database, a response from the HTTP client, the arrival of a certain time. If a coroutine wants to wait, it tells the engine that it wants to suspend its execution, and another coroutine starts executing on the current thread of the operating system instead. As a result, the thread is not idle, but reused by other users. After an external event occurs, the coroutine will be scheduled and executed.

f();
engine::SleepFor(std::chrono::seconds(60)); // voluntarily giving the current thread to other coroutines
g(); // The thread has returned to us

Task cancellation

Task can be notified that it needs to discard its progress and finish early. Cancelling a task restricts the execution of blocking operations in that task.

To cancel a task, just call the engine::Task::RequestCancel() or engine::Task::SyncCancel() method. It cancels only a single task and does not affect the subtasks that were created by the canceled task. The framework typically reports a wait interrupt either by using a return code (for example, CvStatus for engine::ConditionVariable::Wait), or by throwing a module-specific exception (WaitInterruptedException for engine::Task::Wait).

In addition to explicitly calling engine::Task::RequestCancel(), cancellation can occur:

  • due to application shutdown;
  • due to the end of the engine::Task lifetime;
  • for other reasons (lack of resources, task hanging, etc.).

The user is provided with several mechanisms to control the behavior of the application in case of cancellation:

Calling engine::TaskWithResult::Get() on a canceled task would wait for task to finish and a engine::TaskCancelledException exception would be thrown afterwards. For non-canceled tasks the engine::TaskWithResult::Get() returns the result of the task.

Note that the destructor of engine::Task cancels and waits for task to finish if the task has not finished yet. Use concurrent::BackgroundTaskStorage or engine::Task::Detach() to continue task execution out of scope.