Today I discovered that there are some interesting behaviors when you use a function that takes a reference as an entry point for a thread.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>

struct container {
  int numThings;
};

void setThings(container& cont)
{
  cont.numThings = 3;
}

int main()
{
  container c;
  std::thread t(setThings, c);

  t.join();

  std::cout << c.numThings;
}

The code above works with some compilers but not with others. When it works, the result that is printed is 0. What happens is that when the thread is started the arguments are first copied into the thread and then passed into the function. This copy is done to avoid threads from reading memory addresses that are not valid (The memory address could become invalid if the caller of the thread has returned already).

The intent of this code was to have the thread modify the container. This is possible by using std::ref:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>

struct container {
  int numThings;
};

void setThings(container& cont)
{
  cont.numThings = 3;
}

int main()
{
  container c;
  std::thread t(setThings, std::ref(c));

  t.join();

  std::cout << c.numThings;
}

In this case the value printed is 3, as expected. std::ref wraps c in a reference_wrapper<container>. The reason we are able to pass a reference_wrapper<container> to setThings instead of a container& is because it defines an implicit conversion from reference_wrapper<T> to T&.

The behavior above is good, because it makes you explicitly decide when you want to pass by reference to a thread, which could be dangerous in some situations.

There are other cases, where you won’t get a compiler error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <thread>

void talk(const std::string& words)
{
  std::cout << words;
}

void doInThread(int param) {
  char buffer[10];
  sprintf(buffer, "%i", param);
  std::thread t(talk, buffer);
  t.detach();
}

int main()
{
  doInThread(1);
  doInThread(2);
  doInThread(3);
}

The output of this program varies depending on the order in which threads are executed, but there are some scenarios where something totally unexpected happens. The output comes like this: 333. This shouldn’t be possible, because only one thread is told to print 3.

The problem comes from the buffer variable. This variable is a char*. When doInThread exits, the memory from that pointer is released and is free to use again. Because we call doInThread 3 times in a row, the same memory is being assigned and released on each call. The last value it is assigned is 3.

The tricky thing is when we create our thread, all arguments are copied to the thread context. In this case, the char* is copied to the thread. In the thread context, the char* is converted to std::string and used by the talk function. The copying of the char* happens right away, but the conversion to std::string can happen some time in the future. In the case where we get 333 the conversion is not done until after the buffer is set to 3, so all threads end up converting to the same value.

This scenario feels to me like hard to avoid, so it is probably a good idea to keep and eye on this automatic conversions when using threads. It’s safer to do the explicit conversion before to be sure:

1
std::thread t(talk, std::string(buffer));
[ design_patterns  programming  c++  ]
Configuring ESP32 to act as Access Point
Aligned and packed data in C and C++
ESP32 Non-Volatile Storage (NVS)
Making HTTP / HTTPS requests with ESP32
Modularizing ESP32 Software