Skip to content
Odysseas Georgoudis edited this page Nov 21, 2020 · 3 revisions

Thread Safety

All components and API offered to the user is intended to be thread-safe without any special work needing to be done.

Logger objects are thread safe by default. The same logger object can be used to log by any thread Any thread can safely modify the log level of the logger.

The user is responsible for creating the logger instance first by calling quill::create_logger("logger_name") when a call to quill:get_logger("logger_name") for the same logger name is issued in the different thread.

Logging a non copyable, non movable user defined type

Quill will copy all arguments passed as arguments and perform all the formatting in the background thread. Therefore, the arguments needs to be copyable or at least movable. If the argument is not copyable the user has to convert it to a string first before passing it to the logger. If the object has an operator<<() overload quill::utility::to_string() can be used.

See example

Gotchas

Note: After version 1.3.0 there is protection against this in compile time, enabled by default. All non trivial user defined types have to be explicitly tagged as safe to copy or they have to be formatted by the user and passed as a string. See User Defined Types

The following is good to know before tagging any user defined type as safe to copy.

Sometimes even copyable objects are not thread safe when they have a non owning pointer or reference as a member that is also used by another thread.

class A
{
    std::atomic<int> x;
};

class B
{
    A* a;
};

There is a scenario where : thread_1 owns class A thread_2 owns class B that has a reference to class A used by thread_1

In thread_2 we want to log class B. This will create a copy of class B with the non owning pointer on the caller thread. A few microseconds later the backend_thread will access the copy of class B and call operator<<(). However, in the meanwhile thread_1 changed the value of class A to something else. This will lead to logging the latest value of class A instead of logging the value of class A when the logging statement was issued.

A workaround the above problem is performing the formatting in the caller thread by calling quill::utility::to_string(object) from quill/Utility.h The user is responsible to check for those conditions when passing a logger object to the asynchronous logger.

The same problem applies to all user defined types with a mutable raw pointer, a std::shared_ptr or any mutable references. Since the formatting happens in a background thread, the caller thread can easily mutable those and since have only shallow copied them :

  • They can get mutated after the LOG_ call but before the formatting.
  • They can get mutated while they are being formatted.

Guaranteed Logging Mode

Quill uses a thread-local single-producer-single-consumer queue to forward logs records to the backend thread. The queue is initially small for performance reasons (cache locallity). If the queue becomes full the user will suffer a small performance penalty as a new queue will get allocated. Log messages are never dropped. The queue size if configurable.

Customising the queue size

To use a custom queue size the library has to be re-compiled with the following option set in Tweakme.h or passed via cmake.

#define QUILL_QUEUE_CAPACITY 262'144

Enabling non-guaranteed logging mode

If this option is enabled in Tweakme.h then the queue will never re-allocate but log messages will be dropped instead.

#define QUILL_USE_BOUNDED_QUEUE

Note: QUILL_USE_BOUNDED_QUEUE can be used together with QUILL_QUEUE_CAPACITY to setup a bigger queue that would never re-allocate.

Flush Policy and Force Flushing

By default quill lets the underlying libc flush whenever it sees fit in order to achieve good performance. You can use the quill::flush() to instruct the logger to flush its contents. The logger will in turn flush all existing handlers. The thread that calls quill::flush() will block until every message up to that point is flushed.

Application Crash Policy

When main() is terminated gracefully Quill will go through its destructor where all messages are guaranteed to be logged.

However, if the applications crashes, log messages can be lost. To avoid losing messages whe the application crashes due to a signal interrupt the user must setup it's own signal handler and call quill::flush() inside the signal handler.

Quill comes with a signal handler that offers this crash-safe behaviour if enabled. See example for more details if you would like crash safe behaviour.

Log Messages Order

Quill creates a single worker backend thread which orders the messages in all queues by timestamp before printing them to the log file.

Number of Backend Threads

Quill focuses on low latency and not high throughput. Therefore, there is only one backend thread that will process all logs.

Quill Configuration

Quill offers a few customisation options which are also very well documented. Have a look at files Quil.h under the namespace quill::config. Also the file TweakMe.h offers some compile time customisations.

Quill is shipped with ideal configuration favouring performance.

Ideally each caller thread runs on an isolated CPU. Then the backend logging thread should also be pinned to an isolated CPU or a junk CPU core by calling quill::config::set_backend_thread_cpu_affinity(cpu).

In release builds LOG_TRACE or LOG_DEBUG statements can be compiled out to reduce the number of branches in the application. This can be done by editing TweakMe.h or invoking cmake

cmake .. -DCMAKE_CXX_FLAGS="-DQUILL_ACTIVE_LOG_LEVEL=QUILL_LOG_LEVEL_INFO"