我想在某处可以得到答案,但我无法找到它,因为有很多线程问题,相比之下,我很简单。
我没有尝试制作线程安全副本或赋值构造函数或类似的东西。
我想知道的是,如果我有一个表示互斥锁的类,并且我从一个实例化它的函数返回,这首先发生,我的互斥体的析构函数(因此解锁它)或复制构造函数返回值。这是我的榜样:
string blah::get_data(void)
{
MutexLock ml(shared_somewhere_else); // so this locks two threads from calling get_data at the same time
string x = "return data";
return x;
}
在其他地方,我们调用get_data ...
string result = get_data();
回想一下C,你永远不会返回指向全局变量的指针,因为你返回后局部变量超出了范围。
C ++没有这个问题,因为x会被复制到结果中。我想知道的是什么时候发生的。在复制之前我的锁是免费的吗?
在这个简单的例子中,"返回数据"是静态信息,但我正在使用它,它的数据可以被另一个线程更改(也锁定在同一个MutexLock上),所以如果锁在释放复制到结果之前释放,副本可能会被破坏。
我不确定我是否能很好地解释这个问题,所以我会尝试澄清这是否有意义。
答案 0 :(得分:16)
对于以前的标准(这里我将使用C ++ 03),最接近标准来声明返回中的操作顺序来自 6.6
6.6 跳转声明
- 从范围退出(无论多么已完成)时,将为所有具有自动存储持续时间(3.7.2)(命名对象或临时对象)的构造对象调用析构函数(12.4),该范围在该范围内声明, 相反的声明顺序。从一个循环中转出一个循环,或者通过一个具有自动存储持续时间的初始化变量返回过去涉及销毁具有自动存储持续时间的变量,这些变量在从...转移的点的范围内。
醇>
必须完成return语句才能退出[function]范围,这意味着复制初始化也必须完成。此订单不明确。来自 3.7.2 和 12.8 的各种其他引用简明扼要地陈述与上述相同而未提供明确的顺序。工作修订(2014年11月之后)包括以下引用以解决该问题。 defect report澄清了变化。
从该问题日期所见的标准的当前工作草案(N4527)
6.6.3 退货声明
- 在结束时临时销毁之前,对返回实体的复制初始化进行排序 返回语句的操作数建立的完整表达式,然后按顺序排序 包含return语句的块的局部变量(6.6)的销毁。
醇>
请注意,此引用直接指向 6.6 。所以我认为可以安全地假设在返回表达式复制初始化返回值之后,Mutex对象将永远被销毁。
答案 1 :(得分:1)
记住破坏顺序的最简单方法是在离开块时以相反的创建顺序完成,然后在返回后离开块。
如果你考虑一下,最新构建的是在堆栈顶部,即。返回语句所需的临时值,然后是自动序列,它们的顺序相反。
在这种情况下,return语句可能是RVO或NRVO(命名返回值优化),实际上是一个移动。但即使这样也不确定,因为SSO(小字符串优化)可能会导致它成为一种新结构。
返回值在销毁之前返回到返回结束时的“返回堆栈”。 最初它被放置在堆栈上,然后被复制,可能几次被分配到它也是预期的var。 (N)RVO使它更加模糊,因为如果可能的话,它会将它放在最终目的地。
如果我们使用as-if
查看创作和破坏的顺序Mutex -> stack +mutex
string x -> stack +string x base ie. length, capacity and data pointer
-> heap +string x data
return x -> stack +string r base (this is a copy)
-> heap +string r data (this is a copy)
end block -> start destruction
destroy x -> heap -string x data
stack -string x base
mutex -> stack -mutex
return to main
-> destroy old result data
copy return value to result
-> copy return base to result base
-> heap +new result data
-> copy return data to result data
destroy r -> heap -return data
-> stack -return base
这显然无效让我们使用斜体打开-O3来表示更改后的代码
Mutex -> stack +mutex
string x -> stack +string x base ie. length, capacity and data pointer
-> heap +string x data
return x -> *no need to copy, x is where we want it*
end block -> start destruction
destroy x -> *no need to destroy x as we need it*
mutex -> stack -mutex
return to main
-> destroy old result data
copy return value to result
-> copy return base to result base
-> *no need to copy the data as its the same*
destroy r -> heap -return data
-> stack *only data need to be destroyed so base is destroyed by adjusting stack pointer*
现在我们可以通过在函数参数中添加返回地址来添加(N)RVO作弊,因此get_data()变为get_data(string& result)
*place result on stack
-> +stack &result*
Mutex -> stack +mutex
string x -> *string x is not needed as we use result& *
*if new data is longer than result.capacity
-> destroy old data
-> heap +string x data
else -> just copy it*
end block -> start destruction
mutex -> stack -mutex
return to main
-> *there is no old result data to destroy*
*data is already in correct position so no copy return value to result*
*there is no return value on stack so don'tdestroy it*
离开我们
place result on stack
-> +stack &result
Mutex -> stack +mutex
if new data is longer than result.capacity
-> destroy old data
-> heap +string x data
else -> just copy it
end block -> start destruction
mutex -> stack -mutex
return to main
答案 2 :(得分:1)
Rollen D'Souza's answer的实用补充。
所以现在我们有一个标准的引用。 现在,它在实际代码中看起来如何?
此代码的反汇编(VS2015,调试模式):
#include <thread>
#include <mutex>
#include <iostream>
std::mutex g_i_mutex;
std::string get_data() {
std::lock_guard<std::mutex> lock(g_i_mutex);
std::string s = "Hello";
return s;
}
int main() {
std::string s = get_data();
}
...表示:
8: std::string get_data() {
push ebp
mov ebp,esp
push 0FFFFFFFFh
push 0A1B6F8h
mov eax,dword ptr fs:[00000000h]
push eax
sub esp,100h
push ebx
push esi
push edi
lea edi,[ebp-10Ch]
mov ecx,40h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
mov eax,dword ptr ds:[00A21008h]
xor eax,ebp
mov dword ptr [ebp-10h],eax
push eax
lea eax,[ebp-0Ch]
mov dword ptr fs:[00000000h],eax
mov dword ptr [ebp-108h],0
9: std::lock_guard<std::mutex> lock(g_i_mutex);
push 0A212D0h
lea ecx,[lock]
call std::lock_guard<std::mutex>::lock_guard<std::mutex> (0A11064h)
mov dword ptr [ebp-4],0
10: std::string s = "Hello";
push 0A1EC30h
lea ecx,[s]
call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (0A112A8h)
11: return s;
lea eax,[s]
push eax
mov ecx,dword ptr [ebp+8]
call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (0A110CDh)
mov ecx,dword ptr [ebp-108h]
or ecx,1
mov dword ptr [ebp-108h],ecx
lea ecx,[s]
call std::basic_string<char,std::char_traits<char>,std::allocator<char> >::~basic_string<char,std::char_traits<char>,std::allocator<char> > (0A11433h)
mov dword ptr [ebp-4],0FFFFFFFFh
lea ecx,[lock]
call std::lock_guard<std::mutex>::~lock_guard<std::mutex> (0A114D8h)
mov eax,dword ptr [ebp+8]
12: }
push edx
mov ecx,ebp
push eax
lea edx,ds:[0A1642Ch]
call @_RTC_CheckStackVars@8 (0A114BFh)
pop eax
pop edx
mov ecx,dword ptr [ebp-0Ch]
mov dword ptr fs:[0],ecx
pop ecx
pop edi
pop esi
pop ebx
mov ecx,dword ptr [ebp-10h]
xor ecx,ebp
call @__security_check_cookie@4 (0A114E7h)
add esp,10Ch
cmp ebp,esp
call __RTC_CheckEsp (0A1125Dh)
mov esp,ebp
pop ebp
ret
感兴趣的复制构造函数似乎是call
之后的第一个11: return s;
。我们可以看到这个调用是在任何析构函数之前执行的(并且反过来按照顺序颠倒构造顺序)。
答案 3 :(得分:0)
虽然我不是标准的大师,但是很明显在复制之后应该调用析构函数 - 否则你复制的对象在复制之前就会被销毁......:)