【问题标题】:C++, threads, and pointersC++、线程和指针
【发布时间】:2013-10-10 21:32:21
【问题描述】:

我正在使用 std::thread 来执行多个线程。我将指向数组的指针作为参数传递,类似于:

my_type* rest[count];
//Fill rest array
std::thread(fnc, rest, count);

我似乎遇到的问题是,在某个地方,“rest”中的指针值被破坏了。我在调用 std::thread 之前打印出指针值,以及 std::thread 代表我调用的函数中的第一件事,并且值不匹配。它看起来相当随机,有时它们会匹配,有时不匹配(当后者发生时会导致段错误)。

我知道(从我能在该主题上找到的一点点)std::thread 复制了参数,我认为我的问题源于此,并且有一个特殊的函数 std::ref()允许它传递引用,但没有一个特别提到指针。我尝试了各种技术来尝试使用 std::ref() 传递这个数组,但我还没有解决这个问题。

我认为这可能是我的问题的原因是正确的,还是我找错了树?

【问题讨论】:

  • rest 是局部变量吗?在fnc 开始执行之前它是否可能超出范围? std::thread 只会将指针复制到数组的第一个元素 - 而不是元素本身。只要fnc 正在运行,它们就最好保持活力。
  • @IgorTandetnik 没有休息是一个永远不应该超出范围的指针。它传递给我的“创建线程”函数,在 std::thread 调用之前有效,在它之后有效。实际的指针(由 %p 用 printf 打印)在新线程内部是不同的。现在,当我在线程创建之后放置一个 sleep(1) (允许线程在调用函数之前完成)它(似乎)可以工作。我一直在努力寻找可能发生这种情况的任何原因,因为“休息”早已超越了功能。
  • "并且在 std::thread 调用之前有效,在它之后有效" 它在线程的整个生命周期内都有效吗?当您在 C++ 中传递数组时,它会自动转换为指针(不幸的是,这是从 C 继承的不良行为),因此如果数组是局部变量,那么您将指针传递给局部变量;只要使用该指针,局部变量就必须保持活动状态。
  • 由于 C++ 中原始数组的不良行为,我建议您使用std::arraystd::vector。两者都有值语义,这比原始数组的怪异要明智得多。
  • 请发SSCCE

标签: c++ multithreading pointers c++11


【解决方案1】:

如果以某种方式转换(数组指针,而不是内容),那么我会遇到问题。

是的,就是这样。

人们经常错误地认为数组只是指针。事情的真相是,每当你声明一个接受数组的函数时:

void foo(int x[10]);

声明被“调整”,使得参数是一个指针:

void foo(int *x); // C++ can't tell the difference between this and the first declaration

当你调用函数时:

int x[10];
foo(x);

有一个隐式转换等价于:

int x[10];

int *tmp = &x[0];

foo(tmp);

所以发生的情况是,您有一块内存,其中包含指向长寿命对象的指针:

my_type *rest[count] = {new my_type, new my_type, new my_type};

您将指向该内存块的指针传递给线程:

thread(fnc, &rest[0], count);

然后当函数返回 rest 超出范围时,该内存块不再有效。

然后线程跟随指向内存块的指针并读取垃圾。如果它确实读取了正确的数组内容,那么它可以很好地访问长寿命的对象。问题是从 rest 曾经在堆栈上的损坏的内存块中获取指向长寿命对象的指针。

有没有办法抑制这种行为?

在大多数情况下,唯一有意义的就是不使用原始数组作为函数参数。您可以将原始数组包装在结构中并获得合理的行为:

struct int_array {
  int x[10];
};

void foo(int_array x);

int main() {
  int_array x = {1,2,3,4,5,6,7,8,9,0};
  foo(x); // the array is copied rather than getting strangely converted
}

这几乎正是 std::array 所做的,所以你最好使用它。

如果您不想要数组的副本,您可以引用该数组:

int foo(int (&x)[10]);

这为您提供了与使用int foo(int x[10]); foo(x); 在您背后完成的怪异“调整”和隐式转换基本相同的行为。这里的好处是它是明确的,并且您可以对数组的大小进行类型检查。也就是说,由于“调整”,以下内容不会导致编译器错误:

int foo(int x[10]);

int x[3];
foo(x);

而这将:

int foo(int (&x)[10]);

int x[3];
foo(x); // the implicit conversion to &x[0] does not get happen when the function takes a reference to array

【讨论】:

  • 这可能是我面临的问题。我不知道 C++ 是在幕后做到这一点的,并假设如果我以这种方式创建一个数组,它将超出创建它的范围。在我开始使用多个线程之前,这在我的应用程序中不是问题,所以我以前从未有机会亲眼目睹。感谢您提供有用的解释。
  • 虽然使用 std::vectors 或 std::lists 的解决方案似乎确实解决了我的问题(所有测试用例都可以在没有单个段错误的情况下工作),但它对性能的影响很大。以前需要大约 1100 毫秒才能完成的大型操作,现在使用 std::list 需要大约 13400 毫秒,这在 std::vector 中更加明显。我还没有尝试过 std::array,因为它似乎无法将任何大小的 std::array 传递给函数,因为它通过模板获取它的大小。
  • 我必须查看程序的详细信息才能知道最佳解决方案。例如,解决方案可能是移动向量,而不是复制或返回数组,但具有适当的生命周期。
  • 阵列的生命周期是一个未知因素。我不会复制向量(至少据我所知),因为当向量遍历我的函数时,我将指针传递给向量。当然,我不确定 C/C++ 在幕后做了什么小技巧。我正在努力用 struct 技巧来解决它,希望它能与原始速度更接近。
  • 我尝试了你的结构技巧@bames53,虽然它比向量快得多,但它仍然比使用“纯”数组慢了近 3 倍(~1100 到~2780)。你知道为什么会这样吗?我知道它很可能取决于特定于我的应用程序的任何数量的因素,但是如果直接传递/访问指向数组的结构指针与数组之间存在任何“已知”惩罚,它将帮助我分析问题。
【解决方案2】:

为了让您意识到代码的风险,请尝试执行以下操作:

#include <thread>
#include <iostream>

void f() { std::cout << "hello" << std::endl; }

int main()
{
    {
        auto t = std::thread(f);
        std::cout << "0" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        std::cout << "1" << std::endl;
    }
    std::cout << "2" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(400));
    std::cout << "3" << std::endl;
}

您会看到23 永远无法输出,因为应用程序已被终止。

事实上,它更微妙,因为在我的示例中,我已将线程移至t。类似于您的原始样本并且没有将线程分配给任何变量,没有提前终止,但"hello" 永远不会输出。 (可能有一个优化来消除临时的,因为它从未使用过;它只是在可加入之前被破坏了;或者谁知道......)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-14
    • 1970-01-01
    相关资源
    最近更新 更多