【问题标题】:What happens to a detached thread inside a forked process when the process dies?当进程死亡时,分叉进程中的分离线程会发生什么?
【发布时间】:2016-09-04 17:21:13
【问题描述】:

我的程序每次必须处理某些事情时都会分叉,并且在每个分叉的进程中,我都会分离一个线程以便从分叉的进程中记录统计信息:这个线程循环收集数据,但它没有实际条件来停止这个循环.

我在"What happens to a detached thread when main() exits?" 中读到:

如前所述,任何线程,无论是否分离,在大多数操作系统上都会随着它的进程而死。

在我的程序中,我没有为循环线程提供停止条件,因为当产生它的进程将终止时,分离的线程将随之终止。无论如何,我觉得我认为某些东西是理所当然的,所以我编写了以下代码来简化我的疑问并从我的原始程序中排除多余的部分。

在这段代码中,每个分叉的进程都会产生一个线程,该线程将打印一些数字。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <thread>

void threadFoo(int id) {
    int i=0;

    // this loop will simulate some stas collecting
    while (i<1000000) {
        printf("[%d]%d \t", id, i);
        ++i;
    }
    printf("\n\n\n");
    return;
}

void forkFoo(int id) {
    std::thread t(threadFoo, id);
    t.detach();
    printf("PID %d detached thread\n", getpid());
    return;
}


int main(void) {

    int i;
    pid_t pid;

    for (i=0; i<3; i++) {
        pid = fork();
        if (pid == 0) {
            forkFoo(i);
            // this sleep will simulate some work
            sleep(1);
            printf("Proc %d about to terminate...even its detached thread?\n");
            _exit(EXIT_SUCCESS);
        }
        else if(pid > 0) {
            // wait for all children to terminate
            wait(NULL);
        }
    }

    printf("main() about to terminate...\n");
}

程序的输出证实每个线程都随着它的进程而死

PID 13476 detached thread
[0]0    [0]1    [0]2    [0]3  ...
... [0]48940    [0]48941    Proc 13476 about to terminate...even its detached thread?
PID 13478 detached thread
[1]0    [1]1    [1]2    [1]3 ... [1]42395   [1]42396    Proc 13478 about to terminate...even its detached thread?
PID 13480 detached thread
[2]0    [2]1    [2]2    [2]3 ...
... [2]41664    [2]41665    Proc 13480 about to terminate...even its detached thread?
main() about to terminate...

当我用valgrind --leak-check=full --show-leak-kinds=all 运行这个程序时,有人提出了一些疑问:当每个分叉的进程都死掉时,valgrind 会显示一些令人毛骨悚然的输出(13534 是分叉的进程 PID):

==13534== HEAP SUMMARY:
==13534==     in use at exit: 352 bytes in 2 blocks
==13534==   total heap usage: 2 allocs, 0 frees, 352 bytes allocated
==13534== 
==13534== 64 bytes in 1 blocks are still reachable in loss record 1 of 2
==13534==    at 0x4C2B145: operator new(unsigned long) (vg_replace_malloc.c:333)
==13534==    by 0x401DB5: __gnu_cxx::new_allocator<std::_Sp_counted_ptr_inplace<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (new_allocator.h:104)
==13534==    by 0x401CE1: std::allocator_traits<std::allocator<std::_Sp_counted_ptr_inplace<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_ptr_inplace<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >, (__gnu_cxx::_Lock_policy)2> >&, unsigned long) (alloc_traits.h:351)
==13534==    by 0x401B41: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >, std::_Bind_simple<void (*(int))(int)> >(std::_Sp_make_shared_tag, std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >*, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > > const&, std::_Bind_simple<void (*(int))(int)>&&) (shared_ptr_base.h:499)
==13534==    by 0x401A8B: std::__shared_ptr<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >, std::_Bind_simple<void (*(int))(int)> >(std::_Sp_make_shared_tag, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > > const&, std::_Bind_simple<void (*(int))(int)>&&) (shared_ptr_base.h:957)
==13534==    by 0x401A35: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >::shared_ptr<std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >, std::_Bind_simple<void (*(int))(int)> >(std::_Sp_make_shared_tag, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > > const&, std::_Bind_simple<void (*(int))(int)>&&) (shared_ptr.h:316)
==13534==    by 0x4019A9: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > > std::allocate_shared<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >, std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > >, std::_Bind_simple<void (*(int))(int)> >(std::allocator<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > > const&, std::_Bind_simple<void (*(int))(int)>&&) (shared_ptr.h:598)
==13534==    by 0x401847: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > > std::make_shared<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> >, std::_Bind_simple<void (*(int))(int)> >(std::_Bind_simple<void (*(int))(int)>&&) (shared_ptr.h:614)
==13534==    by 0x401621: std::shared_ptr<std::thread::_Impl<std::_Bind_simple<void (*(int))(int)> > > std::thread::_M_make_routine<std::_Bind_simple<void (*(int))(int)> >(std::_Bind_simple<void (*(int))(int)>&&) (thread:193)
==13534==    by 0x4012AB: std::thread::thread<void (&)(int), int&>(void (&)(int), int&) (thread:135)
==13534==    by 0x400F42: forkFoo(int) (funwiththreadinsidefork.cpp:21)
==13534==    by 0x400FBD: main (funwiththreadinsidefork.cpp:36)
==13534== 
==13534== 288 bytes in 1 blocks are possibly lost in loss record 2 of 2
==13534==    at 0x4C2C9B4: calloc (vg_replace_malloc.c:711)
==13534==    by 0x4012E14: allocate_dtv (dl-tls.c:296)
==13534==    by 0x4012E14: _dl_allocate_tls (dl-tls.c:460)
==13534==    by 0x5359D92: allocate_stack (allocatestack.c:589)
==13534==    by 0x5359D92: pthread_create@@GLIBC_2.2.5 (pthread_create.c:500)
==13534==    by 0x4EE8CAE: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)
==13534==    by 0x4012D1: std::thread::thread<void (&)(int), int&>(void (&)(int), int&) (thread:135)
==13534==    by 0x400F42: forkFoo(int) (funwiththreadinsidefork.cpp:21)
==13534==    by 0x400FBD: main (funwiththreadinsidefork.cpp:36)
==13534== 
==13534== LEAK SUMMARY:
==13534==    definitely lost: 0 bytes in 0 blocks
==13534==    indirectly lost: 0 bytes in 0 blocks
==13534==      possibly lost: 288 bytes in 1 blocks
==13534==    still reachable: 64 bytes in 1 blocks
==13534==         suppressed: 0 bytes in 0 blocks

每个分叉的进程死亡时都会出现相同的错误(警告?)消息。

最终输出是关于main()进程,PID 13533:

==13533== HEAP SUMMARY:
==13533==     in use at exit: 0 bytes in 0 blocks
==13533==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==13533== 
==13533== All heap blocks were freed -- no leaks are possible
==13533== 
==13533== For counts of detected and suppressed errors, rerun with: -v
==13533== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

我不知道如何阅读所有这些 valgrind 输出,也不知道我处理分离线程的方式是否正确:我使用的是 C++11,因为它没有提供垃圾收集器, 我不知道那些possibly loststill reachable 字节是否会降低我程序的性能;我经常 fork() (即使每个分叉的进程都有几秒钟的生命周期)并且每个分叉的进程都会产生一个分离的线程来记录一些统计信息。当分叉的进程死亡时,线程也随之死亡,但我不知道从长远来看,我的程序是否会因为 valgrind 向我显示的那些字节而变慢。

在您看来,我的担心是否合理?我是否正确处理了分叉进程中分离线程的死亡?

【问题讨论】:

    标签: c++ linux multithreading c++11


    【解决方案1】:

    当您调用std::thread::detach 时,它不会将线程 与您的进程 分离,它只是将std::thread 实例与线程分离。它的堆栈是从进程的内存中分配的,它仍然与进程共享内存和资源:当进程停止时,它会将线程带出。

    而且它没有优雅地完成,它没有调用任何析构函数,甚至没有释放堆栈(这就是您看到泄漏的原因)。

    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <chrono>
    #include <atomic>
    
    struct OnExit
    {
        const char* id = "none";
        ~OnExit()
        {
            std::cout << "Exiting " << id << std::endl;
        }
    };
    
    thread_local OnExit onExit;
    
    void threadFn1()
    {
        onExit.id = "threadFn1";
        for (size_t i = 0; i < 100000; ++i) {
            std::cout << onExit.id << std::endl;
            std::this_thread::sleep_for(std::chrono::microseconds(50));
        }
    }
    
    std::atomic<bool> g_running { true };
    
    void threadFn2()
    {
        onExit.id = "threadFn2";
        while (g_running) {
            std::cout << onExit.id << std::endl;
            std::this_thread::sleep_for(std::chrono::microseconds(50));
        }
    }
    
    int main()
    {
        std::thread t1(threadFn1);
        std::cout << "started t1\n";
        t1.detach();
        std::cout << "detached t1\n";
    
        std::thread t2(threadFn2);
        std::cout << "started t2\n";
        std::this_thread::sleep_for(std::chrono::microseconds(500));
    
        std::cout << "ending\n";
        g_running = false;
        t2.join();
    }
    

    现场演示:http://coliru.stacked-crooked.com/a/aa775a2960db09db

    输出

    started t1
    detached t1
    started t2
    threadFn2
    threadFn1
    threadFn2
    threadFn2
    ending
    Exiting threadFn2
    

    因为我们自行终止了threadFn2,它调用了OnExit dtor,但是threadFn1被粗暴地终止了。

    【讨论】:

    • 它甚至可以使用分离的线程:coliru.stacked-crooked.com/a/bfa2b9bd4ddda01b 但是,连续检查标志不是让 CPU 总是忙、忙等待吗?
    • @elmazzun 您只需在代码运行的任何循环开始时检查它,因此它只是向您现有的工作负载添加一条指令。
    【解决方案2】:

    “当进程死亡时,分叉进程中的分离线程会发生什么?” - 线程蒸发。线程存在于进程中。当进程终止时,其中的线程也会终止。

    【讨论】:

      【解决方案3】:

      进程是保持你的线程的元素。当进程进行时,线程随之而来。唯一的问题是您的线程是否获得了在任何情况下都需要释放的系统范围的资源。

      【讨论】:

      • 每个线程启动一个pcap会话:在这个会话中,系统范围的资源没有被修改:所以,在我的真实程序中,泄漏将由所有pcap文件描述符和结构,一些sockaddr_in 和其他结构;他们看似无辜,但在大量分离的线程被残酷杀害后会发生什么?大量泄漏和浪费内存?
      • 正确方法:不要分离。让主线程访问所有 pcap 句柄。当 main 想要停止时,它会关闭所有 pcaps 并加入所有线程。当线程收到 pcap 错误时,它们会退出。如果处理得当,您将不会有泄漏和干净整洁的端接。
      • @Shloim 破坏手柄的人应该是创建它的人。如果是主线程,他应该先加入然后关闭句柄,而不是相反。
      • 不一定。假设 thread1 创建了一个套接字,连接它并从中读取数据。当主线程想要杀死它时,它应该在 thread1 的套接字上调用shoutdown()。这将唤醒 thread1 并让它优雅地结束。当然,thread1 应该创建一个接口来正确“中断”它。
      【解决方案4】:

      线程死了。泄漏是由于线程没有完成它的运行并且没有释放它自己的资源。如果你在主线程中的睡眠时间会更长,那么就不会有任何泄漏。

      在阅读有关 pcap 的 cmets 后进行编辑:

      正确方法:不要分离。让主线程访问所有 pcap 句柄。当 main 想要停止时,它会关闭所有 pcaps 并加入所有线程。当线程收到 pcap 错误时,它们会退出。如果处理得当,您将不会有任何泄漏和干净整洁的端接。

      【讨论】:

      • 问题是:被分离的线程永远不会知道它什么时候完成它的循环,什么时候资源将被释放/释放,因为当分离它的进程死亡时,循环将被残酷地停止。
      • 没错。如果你认为你的线程在进程中被杀死是可以的,并且如果它没有做任何可能危及你的系统的事情,如果它在运行中停止,那么你就可以了。但这是一个糟糕的设计。正确的方法是通知线程以某种方式停止(引发标志)并等待它遵守(通过不分离并在其上调用 join)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-02-14
      • 1970-01-01
      • 2019-07-16
      • 1970-01-01
      • 2011-01-24
      • 2018-02-22
      • 1970-01-01
      相关资源
      最近更新 更多