并发编程(二):线程安全检查工具

对于验证代码是否是线程安全,往往是十分困难的,有一些工具可以帮我们简化这项任务,以尽可能保证并发的正确性。

ThreadSanitizer

谷歌出品,内置于编译器的一种线程安全分析工具,使用方法就是在编译时加上-fsanitize=thread 配置项即可。

现在来简单尝试分析如下代码:

#include <thread>
#include <iostream>


bool flag = false;
void producer() {
    printf("profucer\n");
    flag = true;
}
void consumer() {
    while(!flag);
    printf("consumer\n");
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    printf("\n");
}

如果你使用gcc -g 模式编译上述代码,并执行,你大概率会得到正确的输出顺序。

profucer
consumer

profucer
consumer

profucer
consumer

profucer
consumer

但是这是一个经典的线程不安全代码, 现在我们来加上配置项,分析一下线程不安全的原因。

==================
WARNING: ThreadSanitizer: data race (pid=19240)
  Read of size 1 at 0xaaaaaaab4020 by thread T2:
    #0 consumer() /home/taskflow/demo/simple.cpp:19 (simple+0x1258)
    #1 void std::__invoke_impl<void, void (*)()>(std::__invoke_other, void (*&&)()) /usr/include/c++/10/bits/invoke.h:60 (simple+0x2238)
    #2 std::__invoke_result<void (*)()>::type std::__invoke<void (*)()>(void (*&&)()) /usr/include/c++/10/bits/invoke.h:95 (simple+0x215c)
    #3 void std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/include/c++/10/thread:264 (simple+0x209c)
    #4 std::thread::_Invoker<std::tuple<void (*)()> >::operator()() /usr/include/c++/10/thread:271 (simple+0x203c)
    #5 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run() /usr/include/c++/10/thread:215 (simple+0x1fe8)
    #6 <null> <null> (libstdc++.so.6+0xccf98)

  Previous write of size 1 at 0xaaaaaaab4020 by thread T1:
    #0 producer() /home/taskflow/demo/simple.cpp:16 (simple+0x11f8)
    #1 void std::__invoke_impl<void, void (*)()>(std::__invoke_other, void (*&&)()) /usr/include/c++/10/bits/invoke.h:60 (simple+0x2238)
    #2 std::__invoke_result<void (*)()>::type std::__invoke<void (*)()>(void (*&&)()) /usr/include/c++/10/bits/invoke.h:95 (simple+0x215c)
    #3 void std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/include/c++/10/thread:264 (simple+0x209c)
    #4 std::thread::_Invoker<std::tuple<void (*)()> >::operator()() /usr/include/c++/10/thread:271 (simple+0x203c)
    #5 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run() /usr/include/c++/10/thread:215 (simple+0x1fe8)
    #6 <null> <null> (libstdc++.so.6+0xccf98)

  Location is global 'flag' of size 1 at 0xaaaaaaab4020 (simple+0x000000014020)

  Thread T2 (tid=19243, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:962 (libtsan.so.0+0x61880)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xcd2bc)
    #2 main /home/taskflow/demo/simple.cpp:25 (simple+0x12fc)

  Thread T1 (tid=19242, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:962 (libtsan.so.0+0x61880)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xcd2bc)
    #2 main /home/taskflow/demo/simple.cpp:24 (simple+0x12e8)

SUMMARY: ThreadSanitizer: data race /home/taskflow/demo/simple.cpp:19 in consumer()
==================

由上述的异常日志看出,设计数据竞争的部分(临界区),是全局变量flag。

然后是经典的生产消费模型:

template<typename T>
class Queue {
private:
    T* m_queue;
    std::size_t front, back; // front指向下一个要push的位置,back指向下一个要pop的位置
    static constexpr std::size_t Maxm = 1024;
public:
    Queue(): m_queue{new T[Maxm]}, front{0}, back{0} {} // 先不考虑扩容的事情
    ~Queue() {delete[] m_queue;}
    // 先不考虑扩容问题 
    void push(const T& item) {
        m_queue[front] = item;
        front++; // 先不考虑容量,保证每次push次数少于Maxm
    }

    T pop() {
        while(back >= front) {
        }
        T item = m_queue[back];
        ++back;
        return item;
    }

};


void Producer(Queue<int>& queue, int num_items) {
    for (int i = 0; i < num_items; ++i) {
        queue.push(i);
    }
}

void Consumer(Queue<int>& queue, int num_items) {
    int cur = 0;
    for (int i = 0; i < num_items; ++i) {
        int item = queue.pop(); // 阻塞式等待
        // std::cout << "Consumed: " << item << std::endl;
        assert(cur == item);
        cur++;
    }
    std::cout<<"done\n";
}

void TestSingleProducerSingleConsumer() {
    Queue<int> queue;
    const int num_items = 1000;
    std::thread consumer_thread(Consumer, std::ref(queue), num_items);
    std::thread producer_thread(Producer, std::ref(queue), num_items);
    

    producer_thread.join();
    consumer_thread.join();
}

int main() {
    int n = 1;

    auto start = std::chrono::high_resolution_clock::now();

    while(n--) {
        TestSingleProducerSingleConsumer();
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Time taken: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << " ms" << std::endl;
    
}
==================
WARNING: ThreadSanitizer: data race (pid=42564)
  Write of size 8 at 0xfffffffff248 by thread T2:
    #0 Queue<int>::push(int const&) /home/taskflow/demo/banckmark.cpp:28 (banckmark+0x1c08)
    #1 Producer(Queue<int>&, int) /home/taskflow/demo/banckmark.cpp:48 (banckmark+0x14d0)
    #2 void std::__invoke_impl<void, void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(std::__invoke_other, void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:60 (banckmark+0x33d4)
    #3 std::__invoke_result<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>::type std::__invoke<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:95 (banckmark+0x31ec)
    #4 void std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::_M_invoke<0ul, 1ul, 2ul>(std::_Index_tuple<0ul, 1ul, 2ul>) /usr/include/c++/10/thread:264 (banckmark+0x3068)
    #5 std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::operator()() /usr/include/c++/10/thread:271 (banckmark+0x2fdc)
    #6 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> > >::_M_run() /usr/include/c++/10/thread:215 (banckmark+0x2f88)
    #7 <null> <null> (libstdc++.so.6+0xccf98)

  Previous read of size 8 at 0xfffffffff248 by thread T1:
    #0 Queue<int>::pop() /home/taskflow/demo/banckmark.cpp:32 (banckmark+0x1c68)
    #1 Consumer(Queue<int>&, int) /home/taskflow/demo/banckmark.cpp:55 (banckmark+0x156c)
    #2 void std::__invoke_impl<void, void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(std::__invoke_other, void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:60 (banckmark+0x33d4)
    #3 std::__invoke_result<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>::type std::__invoke<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:95 (banckmark+0x31ec)
    #4 void std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::_M_invoke<0ul, 1ul, 2ul>(std::_Index_tuple<0ul, 1ul, 2ul>) /usr/include/c++/10/thread:264 (banckmark+0x3068)
    #5 std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::operator()() /usr/include/c++/10/thread:271 (banckmark+0x2fdc)
    #6 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> > >::_M_run() /usr/include/c++/10/thread:215 (banckmark+0x2f88)
    #7 <null> <null> (libstdc++.so.6+0xccf98)

  Location is stack of main thread.

  Location is global '<null>' at 0x000000000000 ([stack]+0x000000020248)

  Thread T2 (tid=42567, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:962 (libtsan.so.0+0x61880)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xcd2bc)
    #2 TestSingleProducerSingleConsumer() /home/taskflow/demo/banckmark.cpp:67 (banckmark+0x1698)
    #3 main /home/taskflow/demo/banckmark.cpp:80 (banckmark+0x17a0)

  Thread T1 (tid=42566, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:962 (libtsan.so.0+0x61880)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xcd2bc)
    #2 TestSingleProducerSingleConsumer() /home/taskflow/demo/banckmark.cpp:66 (banckmark+0x1668)
    #3 main /home/taskflow/demo/banckmark.cpp:80 (banckmark+0x17a0)

SUMMARY: ThreadSanitizer: data race /home/taskflow/demo/banckmark.cpp:28 in Queue<int>::push(int const&)
==================
==================
WARNING: ThreadSanitizer: data race (pid=42564)
  Read of size 4 at 0xfffff4103000 by thread T1:
    #0 Queue<int>::pop() /home/taskflow/demo/banckmark.cpp:37 (banckmark+0x1cb0)
    #1 Consumer(Queue<int>&, int) /home/taskflow/demo/banckmark.cpp:55 (banckmark+0x156c)
    #2 void std::__invoke_impl<void, void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(std::__invoke_other, void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:60 (banckmark+0x33d4)
    #3 std::__invoke_result<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>::type std::__invoke<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:95 (banckmark+0x31ec)
    #4 void std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::_M_invoke<0ul, 1ul, 2ul>(std::_Index_tuple<0ul, 1ul, 2ul>) /usr/include/c++/10/thread:264 (banckmark+0x3068)
    #5 std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::operator()() /usr/include/c++/10/thread:271 (banckmark+0x2fdc)
    #6 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> > >::_M_run() /usr/include/c++/10/thread:215 (banckmark+0x2f88)
    #7 <null> <null> (libstdc++.so.6+0xccf98)

  Previous write of size 4 at 0xfffff4103000 by thread T2:
    #0 Queue<int>::push(int const&) /home/taskflow/demo/banckmark.cpp:27 (banckmark+0x1be0)
    #1 Producer(Queue<int>&, int) /home/taskflow/demo/banckmark.cpp:48 (banckmark+0x14d0)
    #2 void std::__invoke_impl<void, void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(std::__invoke_other, void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:60 (banckmark+0x33d4)
    #3 std::__invoke_result<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>::type std::__invoke<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int>(void (*&&)(Queue<int>&, int), std::reference_wrapper<Queue<int> >&&, int&&) /usr/include/c++/10/bits/invoke.h:95 (banckmark+0x31ec)
    #4 void std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::_M_invoke<0ul, 1ul, 2ul>(std::_Index_tuple<0ul, 1ul, 2ul>) /usr/include/c++/10/thread:264 (banckmark+0x3068)
    #5 std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> >::operator()() /usr/include/c++/10/thread:271 (banckmark+0x2fdc)
    #6 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(Queue<int>&, int), std::reference_wrapper<Queue<int> >, int> > >::_M_run() /usr/include/c++/10/thread:215 (banckmark+0x2f88)
    #7 <null> <null> (libstdc++.so.6+0xccf98)

  Location is heap block of size 4096 at 0xfffff4103000 allocated by main thread:
    #0 operator new[](unsigned long) ../../../../src/libsanitizer/tsan/tsan_new_delete.cpp:70 (libtsan.so.0+0x8fe9c)
    #1 Queue<int>::Queue() /home/taskflow/demo/banckmark.cpp:23 (banckmark+0x1d28)
    #2 TestSingleProducerSingleConsumer() /home/taskflow/demo/banckmark.cpp:64 (banckmark+0x1630)
    #3 main /home/taskflow/demo/banckmark.cpp:80 (banckmark+0x17a0)

  Thread T1 (tid=42566, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:962 (libtsan.so.0+0x61880)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xcd2bc)
    #2 TestSingleProducerSingleConsumer() /home/taskflow/demo/banckmark.cpp:66 (banckmark+0x1668)
    #3 main /home/taskflow/demo/banckmark.cpp:80 (banckmark+0x17a0)

  Thread T2 (tid=42567, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:962 (libtsan.so.0+0x61880)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xcd2bc)
    #2 TestSingleProducerSingleConsumer() /home/taskflow/demo/banckmark.cpp:67 (banckmark+0x1698)
    #3 main /home/taskflow/demo/banckmark.cpp:80 (banckmark+0x17a0)

SUMMARY: ThreadSanitizer: data race /home/taskflow/demo/banckmark.cpp:37 in Queue<int>::pop()
==================

数据竞争 1

涉及的线程:线程 T1 和线程 T2。 涉及的变量:Queue 对象中的成员变量(具体位置未明确给出),被解释为栈上的地址 0xfffffffff248 或 。 读写操作:

线程 T2 在 Producer(Queue&, int) 函数内调用 Queue::push(int const&)(第 28 行)对队列进行写入操作。
线程 T1 在 Consumer(Queue&, int) 函数内调用 Queue::pop()(第 32 行)对队列进行读取操作。 数据竞争:线程 T1 和线程 T2 并发地对 Queue 对象进行读写操作,且没有采用适当的同步机制(如互斥锁、条件变量等)来保护对队列的访问,导致数据竞争。

数据竞争 2

涉及的线程:线程 T1 和线程 T2。 涉及的变量:Queue 对象在堆上分配的内存区域(地址 0xfffff4103000,大小为 4096 字节)。 读写操作:

线程 T2 在 Producer(Queue&, int) 函数内调用 Queue::push(int const&)(第 27 行)对队列进行写入操作。
线程 T1 在 Consumer(Queue&, int) 函数内调用 Queue::pop()(第 37 行)对队列进行读取操作。 数据竞争:线程 T1 和线程 T2 并发地对 Queue 对象在堆上分配的内存区域进行读写操作,且没有采用适当的同步机制来保护对队列的访问,导致数据竞争。

对于一般的并发问题,都可以使用该工具进行排查,并根据建议修改代码。

参考

https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual

  • 58
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值