std::function在调用内部可变lambda函数时是否会锁定互斥量?

4
C++标准库提供以下类型保证(除非另有说明):
(1) 读操作(即对const对象的操作)是线程安全的。这意味着多个线程可以同时从一个对象中读取,而不会发生竞争条件,只要没有线程同时对该对象进行写入(应用非const操作)。
(2) 只要每个对象一次只被一个线程访问,多个线程可以同时读写任意对象。
标准库要求用户类型具有相同的保证。(您可以在GotW #95中阅读有关此内容的信息,或观看Herb at C++ and Beyond 2012解释此内容。)
现在我的问题是,以下结论是否正确:由于std::functionoperator()是一个const成员函数,因此需要确保线程安全。如果在构造函数中传递的函数对象具有const operator()成员函数,则std::function对象可以假定其为线程安全并直接转发调用。但是,如果在构造函数中传递给它的函数对象具有可变的operator(),则该操作不需要是线程安全的,但std::function仍然需要是线程安全的,因为调用运算符仍然是const。因此,std::function必须在外部同步调用存储的可变函数对象,并因此使用互斥锁。这意味着,在将可变lambda传递给std::function的构造函数时会产生性能开销。
这种推理是否正确?如果正确,那么当前的实现是否符合要求?

1
我不知道std::function是否实际上会获取互斥锁,但如果它这样做了,也不会有太大帮助,因为它无法控制它没有进行的调用。因此,如果它这样做了,我会感到惊讶。一般来说,没有自动使线程不安全的代码变得线程安全的方法。如果您希望您的代码是线程安全的,您需要自己处理同步。 - allyourcode
1个回答

5
Herb所提到的关于标准库在数据竞争安全方面的保证行为在C++11 §17.6.5.9中有所规定:
17.6.5.9 数据竞争避免 [res.on.data.races]
1 本节指定了实现应满足的要求以防止数据竞争(1.10)。除非另有规定,每个标准库函数都应满足每个要求。实现可以在除下文规定的情况之外的情况下防止数据竞争。
2 C++标准库函数不得直接或间接地访问线程之外的对象(1.10),除非这些对象是通过函数的参数直接或间接访问的,包括this。
3 C++标准库函数不得直接或间接修改线程之外的对象(1.10),除非这些对象是通过函数的非const参数直接或间接访问的,包括this。
4 [注意:这意味着,例如,实现不能在没有同步的情况下将静态对象用于内部目的,因为它可能会导致数据竞争,即使在不显式共享对象的程序中也是如此。—end note]
5 C++标准库函数不得通过调用其容器元素上所需的函数来访问其参数或其容器参数的元素间接访问的对象。
6 通过调用标准库容器或字符串成员函数获得的迭代器上的操作可能会访问底层容器,但不得修改它。[注意:特别是,使迭代器无效的容器操作与与该容器相关联的迭代器上的操作冲突。—end note]
7 如果对象对用户不可见且受到数据竞争的保护,则实现可以在线程之间共享其自己的内部对象。
8 除非另有规定,否则C++标准库函数应仅在当前线程中执行所有对用户可见(1.10)的操作。
9 [注意:如果没有可见的副作用,则这允许实现并行化操作。—end note]
假设您将lambda闭包传递给std::function - 例如,通过构造函数或赋值运算符 - 然后调用该函数的operator()。按照第1段的规定,operator()可以通过“直接或间接通过函数的参数,包括this,访问闭包对象。”根据第2段的规定,它不能更改std::function对象本身或闭包对象的状态,因为它们都是“通过函数的非const参数,包括this,直接或间接访问的”。这种行为很容易实现,而无需保护同时访问线程,即锁定。 operator ()会调用lambda闭包的operator()函数,规则也随之改变:您的lambda的operator ()不是标准库函数,因此不受指定标准库函数行为的规则的约束。您可以按照语言规则对闭包对象进行任何操作。
标准库保证其动作不会引入任何数据竞争,但您需要对代码可能引入的任何数据竞争负责。

我猜Herb是把某些东西解释成标准中没有的东西:即具有"const"参数的函数是线程安全的,除非另有说明。或者他没有这样做?我想我会发布一个新问题来了解这个问题。 - Ralph Tandetzky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接