专业编程基础技术教程

网站首页 > 基础教程 正文

C++ 使用std::async实现并发 c++ std async

ccvgpt 2024-11-11 11:23:48 基础教程 7 ℃


std::async()函数异步地运行目标函数,并返回一个std::future对象来携带目标函数的返回值。通过这种方式,async()的工作方式与std::thread非常相似,但允许返回值。

C++ 使用std::async实现并发 c++ std async

让我们通过几个示例来考虑std::async()的使用。

如何做……

在最简单的形式中,std::async()函数执行的任务与std::thread非常相似,无需调用join()或detach(),同时允许通过std::future对象返回值。

在这个示例中,我们将使用一个函数来计算一定范围内的素数数量。我们将使用chrono::steady_clock来计时每个线程的执行时间。

我们首先定义几个方便的别名:

using launch = std::launch;  
using secs = std::chrono::duration<double>;


std::launch具有用于async()调用的执行策略常量。secs别名是一个持续时间类,用于计算我们的素数计算时间。

我们的目标函数计算一定范围内的素数数量。这本质上是通过消耗一些时钟周期来理解执行策略的一种方式:

struct prime_time {  
    secs dur{};  
    uint64_t count{};  
};  
  
prime_time count_primes(const uint64_t& max) {  
    // ... 函数实现 ...  
}


prime_time结构用于返回值,包含持续时间和计数元素。这允许我们计算循环本身的执行时间。isprime lambda函数在值是素数时返回true。我们使用steady_clock来计算计算素数的循环的持续时间。

在main()中,我们调用我们的函数并报告其计时:

int main() {  
    constexpr uint64_t MAX_PRIME{ 0x1FFFF };  
    auto pt = count_primes(MAX_PRIME);  
    cout << format("primes: {} {:.3}\n", pt.count, pt.dur);  
}


输出:

primes: 12252 1.88008s


现在,我们可以使用std::async()异步运行count_primes():

int main() {  
    constexpr uint64_t MAX_PRIME{ 0x1FFFF };  
    auto primes1 = async(count_primes, MAX_PRIME);  
    auto pt = primes1.get();  
    cout << format("primes: {} {:.3}\n", pt.count, pt.dur);  
}


在这里,我们使用count_primes函数和MAX_PRIME参数调用async()。这将在后台运行count_primes()。

async()返回一个std::future对象,该对象携带异步操作的返回值。future对象的get()方法会阻塞,直到异步函数完成,然后返回函数的返回对象。

这与不使用async()时得到的计时几乎相同:

primes: 12252 1.97245s


async()函数可以选择性地将其第一个参数作为执行策略标志:

auto primes1 = async(launch::async, count_primes, MAX_PRIME);


选择是async或deferred。这些标志位于std::launch命名空间中。

async标志启用异步操作,而deferred标志启用延迟求值。这些标志是位映射的,可以使用按位或|运算符组合。

默认设置是两个位都被设置,就好像指定了async | deferred一样。

我们可以使用async()同时运行我们函数的多个实例:

int main() {  
    // ... 初始化并启动多个异步线程 ...  
    for(auto& f : swarm) {  
        auto pt = f.get();  
        // ... 输出结果 ...  
    }  
    // ... 输出总时间 ...  
}


我们知道async返回一个future对象。因此,我们可以通过将future对象存储在容器中来运行15个线程。这是我们在运行Windows的6核i7上的输出:

start parallel primes  
primes(01): 12252 4.1696s  
// ... 更多输出 ...  
total duration: 5.9461s


尽管6核i7无法在所有核心上单独运行所有进程,但它仍然在不到6秒的时间内完成了15个实例。

看起来它在大约4秒内完成了前13个线程,然后又花了2秒来完成最后2个线程。它似乎利用了Intel的Hyper-Threading技术,该技术允许在某些情况下在一个核心上运行两个线程。

当我们在12核Xeon上运行相同的代码时,我们得到了这样的结果:

start parallel primes  
primes(01): 12252 0.96221s  
// ... 更多输出 ...  
total duration: 0.98166s


12核Xeon在不到一秒的时间内完成了所有15个进程。

它是如何工作的……

理解std::async的关键在于它使用了std::promise和std::future。

promise类允许一个线程存储一个对象,该对象稍后可以由future对象异步检索。

例如,假设我们有一个这样的函数:

void f() {  
    cout << "this is f()\n";  
}


我们可以使用std::thread来运行它,如下所示:

int main() {  
    std::thread t1(f);  
    t1.join();  
    cout << "end of main()\n";  
}


这对于一个简单的、没有返回值的函数来说工作得很好。当我们想从f()返回一个值时,我们可以使用promise和future。

我们在main()线程中设置promise和future对象:

int main() {  
    std::promise<int> value_promise;  
    std::future<int> value_future = value_promise.get_future();  
    std::thread t1(f, std::move(value_promise));  
    t1.detach();  
    cout << format("value is {}\n", value_future.get());  
    cout << "end of main()\n";  
}


然后我们将promise对象传递给我们的函数:

void f(std::promise<int> value) {  
    cout << "this is f()\n";  
    value.set_value(47);  
}


请注意,promise对象不能被复制,所以我们需要使用std::move来传递它。

promise对象作为连接future对象的桥梁,允许我们在值可用时检索它。

std::async()只是一个帮助函数,用于简化promise和future对象的创建。使用async(),我们可以这样做:

int f() {  
    cout << "this is f()\n";  
    return 47;  
}  
  
int main() {  
    auto value_future = std::async(f);  
    cout << format("value is {}\n", value_future.get());  
    cout << "end of main()\n";  
}


这就是async()的价值所在。对于许多目的来说,它使promise和future的使用变得更加容易。

最近发表
标签列表