27

Visual C ++CreateThreadpoolWorkQueueUserWorkItem使用std::async.std::launch::async

池中的线程数是有限的。如果创建多个长时间运行而不休眠的任务(包括做 I/O),队列中即将到来的任务将没有机会工作。

标准(我使用 N4140)说使用std::asyncwithstd::launch::async

...调用INVOKE(DECAY_COPY(std::forward<F>(f)), DECAY_COPY(std::forward<Args>(args))...)(20.9.2, 30.3.1.2)就像在一个由线程对象表示的新执行DECAY_COPY()线程中一样,调用async.

(§30.6.8p3,强调我的。)

std::thread的构造函数创建一个新线程等。

关于一般线程它说(§1.10p3):

实现应该确保所有未阻塞的线程最终都会取得进展。[注意:标准库函数可能会静默阻塞 I/O 或锁定。执行环境中的因素,包括外部强加的线程优先级,可能会阻止实现对前进进度做出某些保证。——<em>尾注]

如果我创建一堆 OS 线程或std::threads,它们都执行一些非常长的(可能是无限的)任务,它们都会被安排(至少在 Windows 上;不会弄乱优先级、亲缘关系等)。如果我们将相同的任务调度到 Windows 线程池(或使用std::async(std::launch::async, ...)哪个线程池),那么在较早的任务完成之前,稍后的调度任务将不会运行。

严格来说,这合法吗?“最终”是什么意思?


问题是,如果首先安排的任务实际上是无限的,那么其余的任务将不会运行。所以其他线程(不是操作系统线程,而是根据 as-if 规则的“C++ 线程”)不会取得进展。

有人可能会争辩说,如果代码具有无限循环,则行为是未定义的,因此它是合法的。

但我认为,我们不需要标准所说的那种有问题的无限循环会导致 UB 实现这一点。访问 volatile 对象、执行原子操作和同步操作都是“禁用”关于循环终止的假设的副作用。

(我有一堆异步调用执行以下 lambda

auto lambda = [&] {
    while (m.try_lock() == false) {
        for (size_t i = 0; i < (2 << 24); i++) {
            vi++;
        }
        vi = 0;
    }
};

并且仅在用户输入时才释放锁定。但是还有其他有效类型的合法无限循环。)

如果我安排了几个这样的任务,我安排在他们之后的任务就不会运行。

一个非常邪恶的例子是启动太多任务,这些任务在释放锁/引发标志之前一直运行,然后使用 `std::async(std::launch::async, ...) 调度引发标志的任务. 除非“最终”这个词意味着非常令人惊讶的东西,否则这个程序必须终止。但是在 VC++ 实现下它不会!

在我看来,这似乎违反了标准。让我想知道的是笔记中的第二句话。一些因素可能会阻止实现对前进的进展做出某些保证。那么这些实现如何符合要求呢?

这就像说可能有一些因素阻止实现提供内存排序、原子性甚至多个执行线程的存在的某些方面。很好,但符合要求的托管实现必须支持多线程。对他们和他们的因素来说太糟糕了。如果他们不能提供他们那不是 C++。

这是放宽要求吗?如果这样解释,则完全撤销了该要求,因为它没有指定因素是什么,更重要的是,实现可能不提供哪些保证。

如果不是——那张纸条甚至意味着什么?

我记得根据 ISO/IEC 指令,脚注是非规范性的,但我不确定注释。我确实在 ISO/IEC 指令中找到了以下内容:

24 个音符

24.1 目的或理由

注释用于提供旨在帮助理解或使用文档文本的附加信息。该文件应可以在没有注释的情况下使用。

强调我的。如果我考虑没有那个不清楚的注释的文档,在我看来,线程必须取得进展,std::async(std::launch::async, ...)效果好像仿函数是在一个新线程上执行的,好像它是使用创建的std::thread,因此使用std::async(std::launch::async, ...)must调度仿函数取得进展。在使用线程池的 VC++ 实现中,它们没有。所以VC++在这方面是违反标准的。


完整示例,在 i5-6440HQ 上的 Windows 10 Enterprise 1607 上使用 VS 2015U3 进行测试:

#include <iostream>
#include <future>
#include <atomic>

int main() {
    volatile int vi{};
    std::mutex m{};
    m.lock();

    auto lambda = [&] {
        while (m.try_lock() == false) {
            for (size_t i = 0; i < (2 << 10); i++) {
                vi++;
            }
            vi = 0;
        }
        m.unlock();
    };

    std::vector<decltype(std::async(std::launch::async, lambda))> v;

    int threadCount{};
    std::cin >> threadCount;
    for (int i = 0; i < threadCount; i++) {
        v.emplace_back(std::move(std::async(std::launch::async, lambda)));
    }

    auto release = std::async(std::launch::async, [&] {
        __asm int 3;
        std::cout << "foo" << std::endl;
        vi = 123;
        m.unlock();
    });
    
    return 0;
}

4个或更少它终止。超过 4 则不会。


类似的问题:

4

1 回答 1

4

P0296R2在 C++17 中已经稍微澄清了这种情况。除非 Visual C++ 实现证明其线程不提供并发向前进度保证(这通常是不可取的),否则有界线程池不符合要求(在 C++17 中)。

关于“外部强加的线程优先级”的注释已被删除,可能是因为环境已经总是有可能阻止 C++ 程序的进程(如果不是按优先级,则通过暂停,如果不是,则通过电源或硬件故障)。

该部分还有一个规范的“应该”,但它(如 conio提到的)仅适用于无锁操作,这可以通过其他线程对同一缓存行的频繁并发访问无限期延迟(不仅仅是相同的原子多变的)。(我认为在某些实现中,即使其他线程只是在读取,也会发生这种情况。)

于 2017-12-18T01:06:36.550 回答