繁体   English   中英

使用线程池合法的std :: async的Visual C ++实现

[英]Is the Visual C++ implementation of std::async using a thread pool legal

当使用std::launch::async调用std::async时,Visual C ++使用Windows线程池(如果可用,则使用Vista的CreateThreadpoolWork如果不可用,则使用QueueUserWorkItem )。

池中的线程数是有限的。 如果创建多个运行很长时间而没有休眠的任务(包括执行I / O),则队列中即将发生的任务将无法工作。

标准(我正在使用N4140)说使用std::asyncstd::launch::async

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

(§30.6.8p3,强调我的。)

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

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

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

如果我创建了一堆操作系统线程或者std::thread ,它们都执行一些非常长的(也许是无限的)任务,它们都将被安排(至少在Windows上;不会弄乱优先级,亲和力等)。 如果我们将相同的任务安排到Windows线程池(或使用std::async(std::launch::async, ...)这样做),以后的计划任务将不会运行,直到先前的任务完成。

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


问题是如果首先安排的任务事实上是无限的,那么剩下的任务就不会运行。 所以其他线程(不是OS线程,但根据as-if规则的“C ++ - 线程”)将无法取得进展。

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

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

(我有一堆执行以下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, ...)分派的仿函数必须取得进展。 而在使用线程池的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个没有。


类似的问题:

P0296R2在C ++ 17中对此情况进行了一些阐述 除非Visual C ++实现证明其线程不提供并发的前向进程保证 (这通常是不合需要的),否则有界线程池不符合(在C ++ 17中)。

关于“外部强加的线程优先级”的说明已被删除,可能是因为环境总是可能阻止C ++程序的进展(如果不是优先级,那么暂停,如果不是那样,那么通过权力或硬件故障)。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM