Home Is const reference always thread safe when used as function parameter?
Post
Cancel

Is const reference always thread safe when used as function parameter?

Many people has a fear of thread safety when using const reference as parameter to a function. As usual lets start with some examples and understand the thread safety of const reference

Lets consider simple Test object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Test
{
    Test()
    {
    }

    void name(std::string t)
    {
        m_name = t;
    }

    std::string name() const
    {
        return m_name;
    }

    std::string m_name{};
};

Let’s create a function foo which has void foo(const Test& testObj, const int threadNr); as signature with thread safety using lock_guard and create two thread in main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
std::mutex g_mtx;

void foo(const Test& testObj, const int threadNr)
{
    std::lock_guard<std::mutex> lock(g_mtx);

    if (threadNr == 1) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    std::cout << "Thread " << threadNr << " name is " << testObj.name() << "\n";
}

int main()
{
    Test t;
    t.name("test1");
		// Start thread 1
    std::thread t1 {foo, t, 1};

    // update the name
    t.name("test2");
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

		// Start thread 2
    std::thread t2 {foo, t, 2};

    if (t1.joinable()) {
        t1.join();
    }

    if (t2.joinable()) {
        t2.join();
    }

    return 0;
}

Output:

1
2
3
bhavith@bhavith:~/testing$ ./a.out
Thread 1 name is test1
Thread 2 name is test2

Output looks fine right?

Lets make some modification, instead of reference let’s update to pointer

1
2
3
4
5
6
7
8
9
10
void foo(const Test* testObj, const int threadNr)
{
    std::lock_guard<std::mutex> lock(g_mtx);

    if (threadNr == 1) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    std::cout << "Thread " << threadNr << " name is " << testObj->name() << "\n";
}
1
2
3
// Update t to &t since we need to pass address
std::thread t1 {foo, &t, 1}; 
std::thread t2 {foo, &t, 2};

Output:

1
2
3
bhavith@bhavith:~/testing$ ./a.out
Thread 1 name is test2
Thread 2 name is test2

Expected:

Thread 1 name is test1

Actual:

Thread 1 name is test2

Why is behavior so? I know you people have already guessed it. Yes, pointer in both the thread points to the same Test object.

But the question is, why reference did not have same behavior as pointer?

In order to understand why reference does not have same behavior as pointer, let’s add copy constructor to Test class and get back foo to use reference signature

void foo(const Test& testObj, const int threadNr)

std::thread t1 {foo, t, 1};

std::thread t2 {foo, t, 2};

1
2
3
4
5
    Test(const Test& t)
    {
				std::cout << "Copy ctor \n";
        m_name = t.m_name;
    }

Now let’s re-execute the code.

Output:

1
2
3
4
5
bhavith@bhavith:~/testing$ ./a.out
Copy Ctor
Copy Ctor
Thread 1 name is test1
Thread 2 name is test2

Can you observe here, every time, when function is called, a brand new Test object is created by invoking copy constructor, but this is not the case with pointer.

Let’s prove it by adding some cout’s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void foo(const Test& testObj, const int threadNr)
{
    std::lock_guard<std::mutex> lock(g_mtx);

    std::cout << "Object address in thread " << threadNr << " is " << &testObj << "\n";

    if (threadNr == 1) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    std::cout << "Thread " << threadNr << " name is " << testObj.name() << "\n";
}

int main()
{
    Test t;
    std::cout << "Original object address is " << &t << "\n";
    t.name("test1");
    std::thread t1 {foo, t, 1};

    // update the name
    t.name("test2");
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    std::thread t2 {foo, t, 2};

    if (t1.joinable()) {
        t1.join();
    }

    if (t2.joinable()) {
        t2.join();
    }

    return 0;
}

Output:

1
2
3
4
5
6
7
8
bhavith@bhavith:~/testing$ ./a.out
Original object address is 0x7ffc05d84690
Copy Ctor
Object address in thread 1 is 0x5610595982d0
Copy Ctor
Thread 1 name is test1
Object address in thread 2 is 0x561059598440
Thread 2 name is test2

Here you can see that Test object is local to the function foo. From this it is understood that, we can use const reference without any fear of thread safety.

What about using shared_ptr as a parameter to function foo. Please let me know in comment section.

This post is licensed under CC BY 4.0 by the author.

Do you know int main takes three arguments

Design Patterns Slides