了解循环中 std::async 中引用的范围和生存期

Understanding Scope and Lifetime of References in std::async within a Loop

提问人:Sami 提问时间:7/26/2023 最后编辑:DailyLearnerSami 更新时间:7/28/2023 访问量:53

问:

我的问题围绕着 listDirs 函数中的 for 循环,我在其中启动异步任务。我通过引用传递路径,然后在单独的线程中调用 listDir 函数。std::async

我知道一旦 for 循环移动到下一次迭代,路径变量(即对路径向量中实例的常量引用)就会超出范围。但是,listDir 函数的参数是应绑定到 path 的引用。std::filesystem::path

我的理解是,即使 path 在 listDirs 函数中超出了范围,但路径向量中的实际实例会在 listDirs 函数的整个持续时间内持续存在,正如我们经过的那样。但我不确定这种理解是否正确。std::filesystem::pathstd::ref

有人可以澄清一下这是如何工作的吗?具体说来:

std::ref 是否确保 listDir 获得有效的引用,即使路径超出 listDirs 函数的范围? 在这种情况下,是否存在悬空引用的风险?std::async

#include <filesystem>
using Iterator = std::filesystem::directory_iterator;
// The caller of this function is the thread runtime
std::vector<std::string> listDir(const std::filesystem::path& directory)
{
    
    
    std::vector<std::string> files;
    for (Iterator it(directory); it != Iterator(); ++it)
    {
        
        if (it->is_regular_file())
        {
            files.emplace_back(it->path().filename().string());
            
        }
        
    }
    // When we return this vector as the final action in the function, Return Value Optimization(RVO) takes place to
    // eliminate any extra copying of the vector
    return files;

}

std::vector<std::string> listDirs(const std::vector<std::filesystem::path>& paths)
{
    using Iterator = std::filesystem::directory_iterator;
    std::vector<std::future<std::vector<std::string>>> futures; // listDir returns std::vector<std::string> type
    // iterate over all the directory paths
    for (const std::filesystem::path& path : paths)
    {
    // start each thread using std::async
        futures.emplace_back(std::async(listDir, std::ref(path)));
    }
    std::vector<std::string> allFiles;
    for (std::future<std::vector<std::string>>& fut : futures)
    {

        std::vector<std::string> files = fut.get(); // RVO
        std::move(files.begin(), files.end(), std::back_inserter(allFiles));

    }
    // When we return this vector as the final action in the function, Return Value Optimization(RVO) takes place to
    // eliminate any extra copying of the vector
    return allFiles;
}
int main()
{
    std::filesystem::path currentPath("G:\\lesson4");
    std::vector<std::filesystem::path> paths;

    for (Iterator it(currentPath); it!= Iterator(); ++it)
    {
        if (it->is_directory())
        {
            std::cout << it->path() << '\n';
            paths.emplace_back(it->path());
        }
        
    }

    for (const auto& fileName : listDirs(paths))
    {
        std::cout << fileName << std::endl;
    }

}
C++ 多线程异 stdasync

评论

1赞 paddy 7/26/2023
path并没有真正“超出范围”,因为它是对 的元素的引用。这是整个功能的范围。pathslistDirs
0赞 Sami 7/26/2023
它是否甚至在每次 for-each 迭代结束时都不会超出范围,因为它是临时变量并在每次迭代中创建的?
1赞 paxdiablo 7/26/2023
同意那里的@paddy,是对继续存在的其他事物的引用。我什至不确定您在添加时是否需要这里,尽管我可能是错的。对“符号”本身的访问有时可能会消失,但它所指的东西在你使用它的任何时候都是“活的”。pathstd::ref(path)futurespath
0赞 paddy 7/26/2023
这不是一个临时变量。这是一个参考。从字面上看,它指的是矢量内部的东西。如果将该引用传递到其他内容,则它是相同的引用。
1赞 paxdiablo 7/26/2023
顺便说一句,@paddy,这确实应该是一个答案,而不是一个评论。

答:

1赞 paddy 7/26/2023 #1

在循环中,变量是一个引用。你可以把它想象成一个指针,但事实并非如此。path

for (const std::filesystem::path& path : paths)
{
    // start each thread using std::async
    futures.emplace_back(std::async(listDir, std::ref(path)));
}

在循环的第一次迭代中,指向量的第一个元素。在第二次迭代中,它引用向量的第二个元素。等等......pathpaths

因为在其元素的任何引用(即使是 中使用的元素)的生存期内都不会改变,所以这是安全的。当您使用 时,该引用包装器将封装当前引用。pathsfuturespathstd::asyncstd::ref(path)

事实上,引用包装器通常是使用后台的指针实现的,因为这是将引用作为值传递的唯一实用方法。

即使在调用第一个异步方法之前循环移动到第二次迭代,引用绑定仍然保持不变,并且仍引用 的第一个元素。paths

评论

0赞 Sami 7/26/2023
感谢您的解释。这是有道理的。你为什么说“你可以把它想象成一个指针,但它不是”引用不是常量指针吗?
1赞 paddy 7/26/2023
不,引用就是引用。在某些情况下,编译器可以使用指针来实现引用,但引用不是指针。引用我的回答中链接的文档:引用变量是“已存在的对象或函数的别名”。另外:“引用不是对象;它们不一定占用存储空间”。这是一个微妙的区别,这就是为什么我说你可以把它们想象指针,但重要的是要避免相信它们实际上是指针的陷阱......因为他们不是。它们是参考。我希望我现在已经把这一点说清楚了。
0赞 Sami 7/26/2023
是的。这是非常清楚的。感谢您抽出宝贵时间并做出解释。欣赏它。