代码之家  ›  专栏  ›  技术社区  ›  thb

块范围内的线程本地

  •  0
  • thb  · 技术社区  · 7 年前

    什么是使用 thread_local 块范围变量?

    如果一个可编译的示例有助于说明问题,这里是:

    #include <thread>
    #include <iostream>
    
    namespace My {
        void f(int *const p) {++*p;}
    }
    
    int main()
    {
        thread_local int n {42};
        std::thread t(My::f, &n);
        t.join();
        std::cout << n << "\n";
        return 0;
    }
    

    输出: 43

    在示例中,新线程有自己的线程 n 但(据我所知)对此毫无兴趣,何必费心呢?新的线是自己的吗 n个 有用吗?如果没有用,那又有什么意义呢?

    当然,我认为 一点。我只是不知道重点是什么。这就是我问的原因。

    如果新的线索是自己的 n个 希望(如我所想)CPU在运行时进行特殊处理,可能是因为在机器代码级别,不能访问自己的 n个 以通常的方式,通过预先计算的与新线程堆栈的基指针的偏移量,那么我们不只是浪费机器周期和电力而毫无收获吗?然而,即使不需要特别处理,仍然没有收获!我看不出来。

    所以为什么 线程本地 请在街区范围内?

    工具书类

    1 回复  |  直到 7 年前
        1
  •  4
  •   marc_s MisterSmith    7 年前

    我发现 thread_local 仅在以下三种情况下有用:

    1. 如果您需要每个线程都有一个唯一的资源,这样它们就不必共享、互斥等来使用所述资源。即使如此,只有当资源很大和/或创建成本很高或需要跨函数调用持久化时(即函数中的局部变量不够),这才有用。

    2. (1)的分支-当调用线程最终终止时,可能需要运行特殊逻辑。为此,可以使用 线程本地 在函数中创建的对象。毁灭者 线程本地 对于使用 线程本地 声明(在线程生命周期结束时)。

    3. 可能需要为每个调用它的唯一线程执行一些其他逻辑,但只能执行一次。例如,可以编写一个函数来注册调用函数的每个唯一线程。这听起来可能有点奇怪,但我发现在我正在开发的库中管理垃圾收集资源时使用了这种方法。这种用法与(1)密切相关,但在其构造完成后就不再使用了。在一条线的整个生命周期中有效的哨兵对象。

        2
  •  1
  •   Michael Kenzel    7 年前

    抛开克鲁兹·让(我想我不能再加上这些)已经给出的好例子,还可以考虑以下几点:没有理由禁止它。我想你不会怀疑 thread_local 或者问为什么它应该用一般的语言。对于 线程本地 块范围变量是存储类和作用域如何在C++中工作的结果。仅仅因为人们无法想到与每种可能的语言特性组合有关的“有趣”的事情,并不意味着必须明确禁止所有没有至少一个已知的“有趣”应用程序的语言特性组合。按照这种逻辑,我们还必须继续下去,禁止没有私人成员的班级交朋友等等。至少对我来说,C++特别是遵循这样一种哲学:“如果没有特殊的技术原因,为什么X不能在Y状态下工作,那么就没有理由禁止它”,我认为这是一种相当健康的方法。无缘无故地禁止事情意味着增加复杂性没有好的理由。我相信每个人都会同意C++已经足够复杂了。它还防止了一些令人愉快的意外,比如,仅仅在多年之后,某个语言特性突然被发现之前没有考虑过应用程序。这种情况最突出的例子可能是模板(至少据我所知),它最初的构思并没有考虑到元编程的目的;后来才发现它们也可以用于此

        3
  •  1
  •   Philipp Claßen    7 年前

    首先注意一个块本地线程 is implicitly static thread_local . 换句话说,示例代码相当于:

    int main()
    {
        static thread_local int n {42};
        std::thread t(My::f, &n);
        t.join();
        std::cout << n << "\n"; // prints 43
        return 0;
    }
    

    用声明的变量 thread_local 函数内部与全局定义的线程本地变量没有太大区别。在这两种情况下,都会创建每个线程唯一的对象,其生存期绑定到线程的生存期。

    区别只是全局定义的线程本地变量将被初始化 when the new thread is run before you enter any thread-specific functions . 相反,块局部线程局部变量被初始化 the first time control passes through its declaration .

    一个用例是通过定义在线程生存期内重用的本地缓存来加速函数:

    void foo() {
      static thread_local MyCache cache;
      // ...
    }
    

    (我用过 static thread_local 这里要明确的是,如果函数在同一个线程中多次执行,缓存将被重用,但这是一个品味问题。如果你放下 static ,不会有任何影响。)


    关于示例代码的注释。可能是有意的,但线程并不是真正访问线程本地的 n . 相反,它对指针的副本进行操作,指针是由运行 main . 因为这两个线程都引用同一个内存。

    换言之,更详细的方式应该是:

    int main()
    {
        thread_local int n {42};
        int* n_ = &n;
        std::thread t(My::f, n_);
        t.join();
        std::cout << n << "\n"; // prints 43
        return 0;
    }
    

    如果更改代码,则线程将访问 n个 ,它将运行自己的版本,并且 n个 属于主线程的将不会被修改:

    int main()
    {
        thread_local int n {42};
        std::thread t([&] { My::f(&n); });
        t.join();
        std::cout << n << "\n"; // prints 42 (not 43)
        return 0;
    }
    

    这里有一个更复杂的例子。它调用函数两次,以显示调用之间的状态保持不变。它的输出还显示线程在自己的状态下运行:

    #include <iostream>
    #include <thread>
    
    void foo() {
      thread_local int n = 1;
      std::cout << "n=" << n << " (main)" << std::endl;
      n = 100;
      std::cout << "n=" << n << " (main)" << std::endl;
      int& n_ = n;
      std::thread t([&] {
              std::cout << "t executing...\n";
              std::cout << "n=" << n << " (thread 1)\n";
              std::cout << "n_=" << n_ << " (thread 1)\n";
              n += 1;
              std::cout << "n=" << n << " (thread 1)\n";
              std::cout << "n_=" << n_ << " (thread 1)\n";
              std::cout << "t executing...DONE" << std::endl;
            });
      t.join();
      std::cout << "n=" << n << " (main, after t.join())\n";
      n = 200;
      std::cout << "n=" << n << " (main)" << std::endl;
    
      std::thread t2([&] {
              std::cout << "t2 executing...\n";
              std::cout << "n=" << n << " (thread 2)\n";
              std::cout << "n_=" << n_ << " (thread 2)\n";
              n += 1;
              std::cout << "n=" << n << " (thread 2)\n";
              std::cout << "n_=" << n_ << " (thread 2)\n";
              std::cout << "t2 executing...DONE" << std::endl;
            });
      t2.join();
      std::cout << "n=" << n << " (main, after t2.join())" << std::endl;
    }
    
    int main() {
      foo();
      std::cout << "---\n";
      foo();
      return 0;
    }
    

    输出:

    n=1 (main)
    n=100 (main)
    t executing...
    n=1 (thread 1)      # the thread used the "n = 1" init code
    n_=100 (thread 1)   # the passed reference, not the thread_local
    n=2 (thread 1)      # write to the thread_local
    n_=100 (thread 1)   # did not change the passed reference
    t executing...DONE
    n=100 (main, after t.join())
    n=200 (main)
    t2 executing...
    n=1 (thread 2)
    n_=200 (thread 2)
    n=2 (thread 2)
    n_=200 (thread 2)
    t2 executing...DONE
    n=200 (main, after t2.join())
    ---
    n=200 (main)        # second execution: old state is reused
    n=100 (main)
    t executing...
    n=1 (thread 1)
    n_=100 (thread 1)
    n=2 (thread 1)
    n_=100 (thread 1)
    t executing...DONE
    n=100 (main, after t.join())
    n=200 (main)
    t2 executing...
    n=1 (thread 2)
    n_=200 (thread 2)
    n=2 (thread 2)
    n_=200 (thread 2)
    t2 executing...DONE
    n=200 (main, after t2.join())