【问题标题】:Is va_start (etc.) reentrant?va_start (等)是可重入的吗?
【发布时间】:2011-04-21 09:52:24
【问题描述】:

在对具有悠久历史的类进行编辑时,我被架构师的一个特殊习惯所阻碍,即把他的 va_start -> va_end 序列包装在互斥体中。该添加的更改日志(大约 15 年前制作,此后未修订)指出这是因为 va_start 等。 all 都不是可重入的。

我不知道 va_start 有任何此类问题,因为我一直认为它只是一些堆栈指针数学的宏。这里有什么我不知道的吗?如果有副作用,我不想更改此代码。

具体来说,有问题的函数看起来很像这样:

void write(const char *format, ...)
{
    mutex.Lock();
    va_list args;
    va_start(args, format);
    _write(format, args);
    va_end(args);
    mutex.Unlock();
}

这是从多个线程调用的。

【问题讨论】:

  • 如果 _write 命令将串行 IO 锁定在字节级别或缓冲级别,您仍可能希望拥有更高级别的锁定以使 write() 调用更具原子性。有 2 个线程调用 printf("foo")printf("bar") 并在您的输出上获得 "fboaor" 而不是 "foobar""barfoo",没有什么比这更烦人的了。

标签: c++ variadic-functions


【解决方案1】:

就连续可重入而言(即,如果foo() 使用va_startfoo() 调用同样使用va_startbar() 是否安全),答案是没问题 - 只要因为va_list 实例不一样。标准说,

va_start 和 va_copy 宏都不应被调用来重新初始化 ap,而无需为同一个 ap 调用 va_end 宏。

所以,只要使用不同的va_list(上面称为ap)就可以了。

如果您所说的可重入是指线程安全的(我假设您是线程安全的,因为涉及互斥体),那么您需要查看具体的实现。由于 C 标准没有谈论多线程,所以这个问题实际上是由实现来确保的。我可以想象在一些古怪或小型架构上使va_start 线程安全可能很困难,但我认为如果您在现代主流平台上工作,您可能不会遇到任何问题。

在更主流的平台上,只要将不同的va_list 参数传递给va_start 宏,您应该不会有多个线程通过“相同”va_start 的问题。由于va_list 参数通常在堆栈上(因此不同的线程会有不同的实例),您通常会处理va_list 的不同实例。

我认为在您的示例中,可变参数使用不需要互斥锁。但是,如果是 write(),那么对 write() 调用进行序列化肯定是有意义的,这样您就不会有多个 write() 线程将彼此的输出搞砸了。

【讨论】:

  • 关于序列化调用 - 是的,互斥体属于这里,我只是想把它放在较低的级别。它最初处于较低级别,但 15 年前的这个提交将它移到了你看到它的位置,声称互斥锁也需要保护 va_list,我不相信!
【解决方案2】:

嗯,变量参数访问在 C 中实现的方式很明显,va_list 对象存储了一些内部状态。这使得它不能重入,这意味着在va_list 对象上调用va_start 将使之前的va_start 的效果无效。但更准确地说,C 明确禁止在“关闭”之前用va_end 调用的va_start 会话之前再次在va_list 对象上调用va_start

va_list 对象应该以“非重叠”方式使用:va_start...va_end。之后,您可以在同一个 va_list 对象上执行另一个 va_start。但尝试重叠同一va_list 对象上的va_start...va_end 会话将不起作用。

P.S. 实际上,理论上,当然可以在任何基于会话的迭代器中实现一些基于 LIFO 的内部状态。 IE。理论上可以允许在同一个va_list 对象上嵌套va_start...va_end 会话(从而使其在这个意义上是可重入的)。但是 C 库规范没有提供类似的东西。

请注意,尽管在 C99 中 va_list 对象可以被 va_copy 复制。因此,如果您需要通过多个重叠的va_start...va_end 会话浏览相同的参数列表,您始终可以通过创建原始va_list 的多个独立副本来实现。

P.P.S. 查看您提供的代码示例...在这种情况下绝对不需要任何互斥锁(就va_list 的完整性而言)。并且不需要可重入的va_list 对象。您的代码非常好,没有任何互斥锁。它可以在多线程环境中正常工作。 va_... 组中的宏不会对实际的“堆栈指针”进行操作。相反,它们对完全独立的va_list 对象进行操作,该对象可用于迭代存储在堆栈中的值。您可以将其视为您自己的私有本地堆栈指针。每个调用你的函数的线程都将获得它自己的va_list 的副本,并在它自己的堆栈上进行迭代。线程之间不会有冲突。

【讨论】:

  • 是的,在单个 va_list 上两次调用 va_start 会使该列表无效。因此,对于静态 va_list(或全局,或类的成员......),它可能会失效,但未来对同一函数的调用会生成一个新的 va_list。
  • @Nate:是的,但不一定需要静态的va_list 来解决问题。当一个人在同一个本地 va_list 上进行操作时,通常会出现这样的问题,这是在对同一个可变参数函数的一次调用中进行的。这没有什么违法的。
【解决方案3】:

在某些平台上 va_list 会出现重入问题,但在同一平台上,所有局部变量都有这样的问题。不过,我很好奇:您的 _write 函数期望做什么?如果它使用的是在调用 write 之前设置的参数,那么它本身可能会导致线程问题,除非(1)包含 _write 的对象的任何特定实例一次只能由一个线程使用,或者(2)所有使用对象进行 _write 的线程将需要相同的设置参数。

【讨论】:

  • _write 仍将受到保护。我只是想将保护降低到那个级别,而不必担心所有 va_list 指针。
猜你喜欢
  • 2015-08-13
  • 1970-01-01
  • 2022-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多