Skip to content
/ lum Public

Write predictable tests for multi-threaded C++. Inspired by Tokio's loom.

Notifications You must be signed in to change notification settings

podusowski/lum

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This library is highly experimental and incomplete! It's OK to play around with the examples, but please don't use it in any of your projects!

Tokio's loom inspired utility to test concurrency in your software.

What does it do?

There are various thread sanitizers which check whether your structures are synchronized, lum check your concurrent algorithms logic. Pretty vague, I know, let's see some examples.

Consider following code. Program has two threads. One (commented as supplementary) writes a value, second one (main thread) reads it. Is this program correct? Well, no. Writes and reads aren't synchronized. In fact, we can even see a data race in both ThreadSanitizer and helgrind:

WARNING: ThreadSanitizer: data race (pid=20480)
  Read of size 4 at 0x7ffeaa08cebc by main thread:
    #0 main <null> (nosync+0xd04d3) (BuildId: bdf7a3f9f6d85eec41db285485f6aa4944621de9)

  Previous write of size 4 at 0x7ffeaa08cebc by thread T1:
    #0 main::$_0::operator()() const nosync.cpp (nosync+0xd0af9) (BuildId: bdf7a3f9f6d85eec41db285485f6aa4944621de9)

This is pretty easy to fix, right? Just put some mutexes here and there. ThreadSanitizer doesn't complain anymore, nor is helgrind. However, if we run the program couple of times, we'll see that it doesn't give predictable results:

$ while true; do ./sync; done
value: 42
value: 42
value: 42
value: 0
value: 42
value: 42
value: 42
value: 42
value: 0
value: 42
value: 42

The behavior is not deterministic because there is no way to tell which thread will acquire the mutex first. Sometimes it's acquired by the writer and we get 42, but sometimes the variable id read by the main thread, resulting with still default value of 0.

lum lets you write tests which will use different combinations of whom gets the lock first. In above example, test would run two times (though there is a small caveat here). In the first iteration, "calculating thread" will get the lock first, in the second iteration, the "reading thread" will get it first. Both will happen in a deterministic way.

The test version behaves kind of like a Schrödinger's cat, taking both probability paths at once. If we run it, in fact we will see both cases:

value: 0
value: 42

Limitations and future plans

  • Only single multithreading synchronization primitive is supported right now - a mutex.
  • lum works only with a single mutex.
  • It's VERY intrusive right now. Not only you have to swap std::mutex with lum::mutex, but it also has additional constructor parameter. There are couple of ways to solve it though.

Enabling logging

To enable lum's internal debug prints, define a LUM_TRACE environment variable. Its value doesn't matter.

Cheat sheet

Building with clang's ThreadSanitizer:

cmake -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-fsanitize=thread" -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -B build-tsan
cmake --build build-tsan

About

Write predictable tests for multi-threaded C++. Inspired by Tokio's loom.

Topics

Resources

Stars

Watchers

Forks