va_start(等)是否可以重入?

时间:2010-10-05 16:30:37

标签: c++ variadic-functions

在对具有悠久历史的课程进行编辑时,我被包裹他的va_start的建筑师的特殊习惯所困扰 - > va_end在互斥锁中的序列。该添加的更改日志(大约在15年前制作,之后未修订)注意到这是因为va_start等。一切都不是可重入的。

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

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

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

这是从多个线程调用的。

3 个答案:

答案 0 :(得分:5)

至于连续可重入(即,如果foo()使用va_startfoo()可以安全地拨打使用bar()的{​​{1}} va_start ),答案是没关系 - 只要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的不同实例。

我认为在你的例子中,互斥体对于varargs的使用是不必要的。但是,如果是write(),那么对write()调用进行序列化肯定是有意义的,这样就不会有多个write()线程搞砸了彼此的输出。

答案 1 :(得分:2)

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

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

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

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

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

答案 2 :(得分:0)

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