【问题标题】:Safe way to pass parameters into a thread将参数传递到线程的安全方法
【发布时间】:2014-12-29 08:00:50
【问题描述】:

您能否澄清一下,为什么下面的代码是一种将参数传递到新线程的安全方式:

//Listing 5.3 Passing a Value into a Created Thread
for ( int i=0; i<10; i++ )
    pthread_create( &thread, 0, &thread_code, (void *)i );

下面的代码不是:

//Listing 5.4 Erroneous Way of Passing Data to a New Thread
for ( int i=0; i<10; i++ )
    pthread_create( &thread, 0, &thread_code, (void *)&i );

从书中引用,关于代码:

重要的是要意识到子线程可以在调用后的任何时候开始执行,所以指针必须指向仍然存在并且仍然保持相同值的东西。这排除了传递指向变化变量的指针以及指向堆栈上保存的信息的指针(除非堆栈肯定存在,直到子线程读取该值之后)。

【问题讨论】:

  • 书中说第二种方法是“将数据传递到新线程的错误方式”
  • 是否保证第一种方法中的i即使在循环完成后也能保持活动状态?
  • 我们说是的。启动所有线程后,加入所有线程是合理的。
  • 这不是关于让变量是否存在将由“C”规范指导。在您的情况下,它应该是 C99 或 C11。你的书本身说unless the stack is certain to exist
  • @MohitJain:在第一种方法中,您根本不依赖变量是否仍然存在。

标签: c pthreads


【解决方案1】:

第三种方法很好,如下所示:

static int args[10];
for ( int i=0; i<10; i++ ) {
    args[i] = i;
    pthread_create( &thread, 0, &thread_code, (void *)&args[i] );
}

如果您希望在所有线程之间共享相同的变量,请在主变量或最好的静态变量或全局变量中创建一个局部变量。

方法一和方法二的问题:

方法 1 您将int 转换为void *,然后再转换回int,这很糟糕,因为intvoid * 的大小可能不同。如果您打算将void * 转换为int *,那就更糟了,而且是一个UB。另请阅读this post

方法 2 您将相同的地址传递给所有线程。当 i 从 10 个工作线程中的任何一个的主线程更改时,相同的值将在各处反映出来,这可能不是您的意图。 此外,i 的范围在 for 循环之后结束,您最终可能会访问线程中的悬空指针。并且会导致 UB。 (未定义的行为)

【讨论】:

  • 显然,您的回答与我在书中看到的完全不同。
  • 我从逻辑上得出了这个答案。如果我希望每个线程中的不同参数与主线程通信并且不想进行可能会咬我的类型转换,这符合我的目的。
  • 请注意,使用第三种方法,您仍然必须确保 args 保持在范围内,直到新线程全部完成。
  • @immibis 是的,你是对的。如果 args 的寿命可以小于线程,则应考虑制作这些 static
【解决方案2】:

为什么第二个例子错了?

正如你的引文所说,你不能将指针传递给 interation 变量,因为它会很快改变。你永远不知道并发线程何时会使用指针并取消引用它。

// Listing 5.4 Erroneous Way of Passing Data to a New Thread
for ( int i=0; i<10; i++ )
    pthread_create( &thread, 0, &thread_code, (void *)&i );

想象一下第一次打电话给pthread_create()。它接收一个指向i 的指针,并且可能会取消引用该指针并读取该值。您当时的价值应该是0。但是您的主线程(带有 for 循环的线程)可能已经将 i0 更改为 1。这称为竞争条件,因为您的程序取决于一个线程更快地更改值还是另一个线程更快地获取它。

还有第二个竞争条件,因为您的 i 变量将在循环结束时超出范围。如果线程启动或读取指针目标的速度很慢,则堆栈上的地址可能已经分配给其他东西。不得取消引用指向不再存在的变量的指针。

为什么第一个没有同样的问题?

第一个示例使用i 的值,而不是地址。这很好,因为pthread_create() 只会保存该值并将其传递给线程。

// Listing 5.3 Passing a Value into a Created Thread
for ( int i=0; i<10; i++ )
    pthread_create( &thread, 0, &thread_code, (void *)i );

但是pthread_create() 只接受void *(一个通用指针)。该示例使用了一个特殊技巧,您可以将整数值转换为指针值。预计线程函数将执行相反的操作(将指针转换回整数)。

技巧通常用于在预期对象的位置存储整数值,因为它避免了分配和解除分配对象。这种技术是好的还是坏的做法超出了事实答案的范围。它被用于 GLib 等框架中,但我想很多程序员会鄙视它。

最后的笔记

书中的例子显然不是解决实际问题的方法,而只是激励的例子。在实际代码中,您很少只传递一个整数值,并且您可能希望在某个时间点加入线程。因此,在一个简单的场景中,您必须分配线程参数、填写它们、启动工作人员、加入工作人员、检索结果并释放分配。

在更复杂的场景中,您将与线程进行通信,因此您不会被限制在创建它们时提供它们并在加入它们后检索结果。您甚至可以让工作人员运行并在需要时重复使用它们。

【讨论】:

  • 帕维尔,感谢您的精彩回答!我被(void *) 部分欺骗了。现在我似乎很清楚,它只是i 实际值的容器。但是,(void *) i 不适用于非整数 i,对吧?
  • @Konstantin:序数类型,基本上可以转换为int,例如enum。如果编译器抱怨,你总是可以双重类型转换,首先是例如intptr_t,然后到void *
猜你喜欢
  • 2020-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-18
  • 2014-08-04
  • 2017-07-29
  • 1970-01-01
相关资源
最近更新 更多