以下一段代码是否会在多线程场景中按预期工作?
int getUniqueID()
{
static int ID=0;
return ++ID;
}
ID不必连续 - 即使它跳过一个值,也没关系。 可以说当这个函数返回时,返回的值在所有线程中都是唯一的吗?
答案 0 :(得分:18)
答案 1 :(得分:5)
不,比赛仍有潜力,因为增量不一定是原子的。如果你使用原子操作来增加ID,这应该可行。
答案 2 :(得分:4)
++
不一定是原子的,所以不,这不是线程安全的。但是,很多C运行时提供了原子版本,例如,对于gcc __sync_add_and_fetch()
和Windows上的InterlockedIncrement()
。
答案 3 :(得分:3)
如果您只需要在N个线程中单调增加(或非常接近)数字,请考虑这个(k是某个数字,使得2 ^ k> N):
int getUniqueIDBase()
{
static int ID=0;
return ++ID;
}
int getUniqueID()
{
return getUniqueIDBase() << k + thread_id;
}
答案 4 :(得分:2)
getUniqueID
至少有两种竞争条件。在初始化ID
和增加ID
时。我已经重写了函数以更清楚地显示数据竞争。
int getUniqueID()
{
static bool initialized = false;
static int ID;
if( !initialized )
{
sleep(1);
initialized = true;
sleep(1);
ID = 1;
}
sleep(1);
int tmp = ID;
sleep(1);
tmp += 1;
sleep(1);
ID = tmp;
sleep(1);
return tmp;
}
增量具有欺骗性,它看起来很小,以至于假设它是原子的。但它是一个加载 - 修改 - 存储操作。将值从内存加载到CPU寄存器。 inc
登记册。将寄存器存回内存。
使用新的c ++ 0x,您只需使用std::atomic
类型。
int getUniqueID()
{
static std::atomic<int> ID{0};
return ++ID;
}
注意:技术上我说谎了。零初始化全局变量(包括函数静态)可以存储在bss内存中,一旦程序启动就不需要初始化。但是,增量仍然是一个问题。
答案 5 :(得分:0)
注意:使用几乎这个词,因为全局变量将在进程启动时初始化(即在进入main
之前将调用其构造函数)而内部的静态变量函数将在第一次执行语句时初始化。
你的问题从一开始就是错误的:
具有本地静态变量的ID生成器 - 线程安全吗?
在C / C ++中,函数内部或类/结构声明内部的静态变量(几乎)表现为全局变量,而不是基于本地堆栈的变量。
以下代码:
int getUniqueID()
{
static int ID=0;
return ++ID;
}
将(几乎)与伪代码相似:
private_to_the_next_function int ID = 0 ;
int getUniqueID()
{
return ++ID;
}
使用伪关键字private_to_the_next_function
使变量对所有其他函数不可见但getUniqueId ...
在这里,static
只隐藏变量,使其无法从其他函数访问...
但即使隐藏,变量ID仍然是全局的:多个线程应该调用getUniqueId, ID将与其他全局变量一样是线程安全的,也就是说,根本不是线程安全的 的
在看完评论后,我觉得我的答案不够清楚。我没有使用全球/本地概念来表示其范围,但是对于它们的终身意义:
只要进程正在运行,全局就会生效,并且在堆栈上分配的本地将在进入范围/函数时开始其生命,并且在范围/函数退出时将停止存在。这意味着全球将保留其价值,而本地则不会。这也意味着线程之间将共享全局,而本地则不会。
添加static
关键字,它具有不同的含义,具体取决于上下文(这就是为什么在全局变量和C ++中的函数上使用static
而不赞成使用匿名命名空间,但是我disgress)。
在限定局部变量时,此局部不再像本地一样。它成为一个隐藏在函数内部的全局。所以它的行为好像在函数调用之间神奇地记住了局部变量的值,但是没有魔法:变量是全局变量,并且在程序结束之前将保持“活着”。
您可以通过记录在函数内声明为static的对象的创建和销毁来“看到”这一点。构造将在执行声明语句时发生,并且破坏将在过程结束时发生:
bool isObjectToBeConstructed = false ;
int iteration = 0 ;
struct MyObject
{
MyObject() { std::cout << "*** MyObject::MyObject() ***" << std::endl ; }
~MyObject() { std::cout << "*** MyObject::~MyObject() ***" << std::endl ; }
};
void myFunction()
{
std::cout << " myFunction() : begin with iteration " << iteration << std::endl ;
if(iteration < 3)
{
++iteration ;
myFunction() ;
--iteration ;
}
else if(isObjectToBeConstructed)
{
static MyObject myObject ;
}
std::cout << " myFunction() : end with iteration " << iteration << std::endl ;
}
int main(int argc, char* argv[])
{
if(argc > 1)
{
std::cout << "main() : begin WITH static object construction." << std::endl ;
isObjectToBeConstructed = true ;
}
else
{
std::cout << "main() : begin WITHOUT static object construction." << std::endl ;
isObjectToBeConstructed = false ;
}
myFunction() ;
std::cout << "main() : end." << std::endl ;
return 0 ;
}
如果你启动没有参数的可执行文件,执行将永远通过静态对象声明,因此,它永远不会被构造也不会被破坏,如日志所示:
main() : begin WITHOUT static object construction.
myFunction() : begin with iteration 0
myFunction() : begin with iteration 1
myFunction() : begin with iteration 2
myFunction() : begin with iteration 3
myFunction() : end with iteration 3
myFunction() : end with iteration 2
myFunction() : end with iteration 1
myFunction() : end with iteration 0
main() : end.
但是如果使用参数启动它,那么该对象将在myFunction的第三次递归调用中构造,并且仅在进程结束时销毁,如日志所示:
main() : begin WITH static object construction.
myFunction() : begin with iteration 0
myFunction() : begin with iteration 1
myFunction() : begin with iteration 2
myFunction() : begin with iteration 3
*** MyObject::MyObject() ***
myFunction() : end with iteration 3
myFunction() : end with iteration 2
myFunction() : end with iteration 1
myFunction() : end with iteration 0
main() : end.
*** MyObject::~MyObject() ***
现在,如果你使用相同的代码,但通过多个线程调用myFunction,你将在myObject的构造函数上有竞争条件。如果你调用这个myObject方法或者在多个线程调用的myFunction中使用这个myObject变量,你也会有竞争条件。
因此,静态局部变量myObject只是隐藏在函数内的全局对象。