Multithreading in an expressive way with monadic expression

Hi, there is a long time I have not written anything, sorry for that! I planned to write several articles during these few next weeks. This article will deal with multithreading.

Introduction

In a lot of articles, we can read that multithreading is not easy, and other things like that. In this article, we will learn how to write multi-threaded application in a simple and a fun way.
The idea will be to write something like that

If we write a little scheme, it gives us that :

Multithreading in action
Multithreading in action

Obviously, when the function f3 starts, functions f1 and f2 must have complete. The same thing happens for the getResult. It must wait for f3, f4, f5 to complete.

Does it really exist?

Actually, it does, and it will even be in the standard in C++20. However, the feature from the C++20 suffers from different problems that are explained in the Vittorio Romeo article. Actually, this article could be a continuation of the series of articles from Vittorio.

Multithreading with Monadic Expression: Implementation

I hope you really love read ununderstandable C++ code because the code is full of template and metaprogramming. I recommend to have some basis in really modern C++ or read the series from Vittorio I was talking before. We are going to see a lot of things both in multithreading and metaprogramming

Why do we need a continuation of the series from Vittorio?

Firstly, the code provided by Vittorio does not work on Visual Studio, and I really want to have my codes work on all compilers and all operating systems. Second, in its article, he only provides a version using a function waitAndGet that prevents us to insert code into the main thread in an easy way.
The third issue is that the group of functions returns a std::tuple

For example, you must have to write something like that

But, in reality, some people will prefer the first version, other will prefer the second version. What will happen if a function does not return any arguments? According to me, the second version is more natural.

Forwarding

Because we are going to use a lot of metaprogramming, we may need to perfect forward some arguments. Here are two macro that defines a perfect forwarding. There is one macro for a normal variable and another one for auto. I was not able to use the same one for both cases because sometimes, use the form ::std::forward<decltype(variable)>(variable); is too difficult for Visual Studio, that is why I provide these both declarations.

This code does not really need any explanations.

A Latch

A latch is useful when you want to know if a job is finished or not. Let’s say you have two thread, which one of them is waiting the result coming from the second one. The first one need to wait the second to finish.

Latch
Latch

The following code shows how to implement a simple “bool_latch”

The code is based on condition_variable, mutex, and a boolean variable

The return type

Each function may return one value. However, what returns a group of functions? The better choice is to return a std::tuple. Thus, if you have three functions that return an int at the same level (running in 3 parallels threads), the result will be a std::tuple<int, int, int>.

Functions that return void.

The simple way for this case is to create an empty type that we will name nothing. Its declaration is straightforward.

The function, instead to return nothing, must return the type nothing.
Let’s say you have three functions, f1, f2, f3. f1 and f3 return an int and f2 returns nothing. You will get a std::tuple<int, nothing, int>
How to return nothing instead of void? This function explains that in a straightforward way!

Pass a tuple as an argument

Okay, now say you have a std::tuple<int, double, std::string> and, you want to give it to one function with a prototype like returnType function(int, double, std::string).
One way to do that is to use the function apply.

Here there is no problem, but assume that there is a nothing in your tuple like std::tuple<int, nothing, int>.
When you will apply this tuple to the function, you will also pass the nothing.

To avoid such problem, you must filter all the arguments and store the arguments inside a lambda function.

Architecture

This part will be the most difficult part of the article. We will see how the architecture work. It is an architecture with nodes. We will see three kinds of nodes. The root node that is the beginning of the chain of functions, the when_all node that can own one or several functions which will run in parallel and one node result_getter that represents the end of the chain of functions.

Overview

Overview of multithreading
Overview of multithreading

As you may have noticed, there is no type erasure. The type owned by the caller is the result_getter. However, the result_getter, when it executes all the functions, it must return to the root. And to do that, it must go through the when_all.

Now go to explain whole the code!

root

There is nothing difficult here. The when_all is a friend. There are two functions, the first one is called by the children and its purpose it’s only to reach the top. After, you execute the child functions through the scheduler.

when_all

This is the most difficult part.
However, everything is not difficult. There are two things that are difficult. The output_type and the execute function.

The input_type is straightforward, we take the output from the parent. The output_type is a bit tricky. The idea is to call all the function, and for each of them, add the result to a tuple.

There are several things to think about.
The exec lambda is the function that will be executed in a parallel thread. We store the value inside the good position of the tuple. If all functions have finished, we call the execute function for the directChild. This condition is checked by mLeft.

The executeOneFunction lambda is the function that computes if the function must be launched through the scheduler or not. (If there is only one function, there is no need to launch it through the scheduler because the root already did that).

The enumerate_args execute the function with each arguments, and give the index as an argument as well.

The enumerate_args is following :

result_getter

Once you are here, you may have already build your own result_getter

The idea here is to execute the function and wait for the latch to return the result. All the part for epurated tuple is done here :

The idea is to remove the nothings from the tuple, and if the tuple owns only one type, the tuple is removed and we get only the type.

Conclusion

In this article, you learned how to write safe and easy to read multi-threaded functions. There is still a lot of things to do, but it is really usable and easy to use. And if someone else has to read your code, he should be able to do so.

If you want to test it online, you can go on wandbox

Reference

Vittorio Romeo

Leave a Reply