解锁C++异步编程:告别阻塞,拥抱高效

你在使用办公软件处理文档时,点击保存后,软件界面突然定格,只能眼巴巴地看着进度条缓慢前进,期间什么操作都做不了,是不是很恼人?这便是传统同步编程的阻塞问题在作祟。同步编程就像单行道,任务得一个接一个按顺序执行,一旦遇到文件读写、网络请求这类耗时操作,整个程序就会被卡住,CPU 资源也只能闲置浪费。

而异步编程,堪称程序世界的 “多车道高速公路”。当程序碰到耗时任务,无需原地等待,可立即切换到其他车道,继续执行别的任务。如此一来,CPU 得以充分利用,程序响应速度大幅提升,用户体验自然也更上一层楼。

C++ 作为一门以高性能著称的编程语言,在异步编程领域底蕴深厚,从基础的多线程技术,到 C++11 引入的 std::async、std::future 等高级特性,再到 C++20 推出的协程,为开发者解锁高效编程提供了丰富且强大的工具。接下来,就让我们一同深入 C++ 异步编程的奇妙世界,探索这些工具的用法 。

一、异步编程的简介

1.1什么是异步?

异步编程是一种编程范式,允许程序在等待某些操作时继续执行其它任务,而不是阻塞或等待这些操作完成;异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。

在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。

简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

常见的两种异步:回调函数、异步Ajax

(1)回调函数

回调函数最常见的是setTimeout,实例如下:

<script type="text/javascript">
	setTimeout(function() {
		console.log("First")
	}, 2000)
	console.log("Second")
</script>

正常情况下(同步)应该先输出First再输出Second,但结果刚好相反。因为延迟了2秒,所以在这2秒内先输出了Second,2秒后再输出了First。

(2)异步Ajax

<button>发送一个 HTTP GET 请求并获取返回结果</button>
<script>
	$(document).ready(function() {
		$("button").click(function() {
			$.get("data.json", function(data, status) {
				console.log("数据: " + data + "\n状态: " + status);
			});
			console.log("1111")
		});
	});
</script>

1.2同步 VS 异步:编程世界的龟兔赛跑

在编程的奇妙世界里,同步与异步是两种重要的任务执行方式,就如同龟兔赛跑中的乌龟和兔子,有着截然不同的行事风格。

先来说说同步编程,它就像一只稳扎稳打的乌龟 ,代码按照顺序,一个任务接着一个任务地执行。只有当前一个任务彻底完成,拿到了它的返回结果,程序才会继续向下执行下一个任务。在这个过程中,如果某个任务因为等待资源(比如进行网络请求、读取大文件等)而花费了大量时间,整个程序就只能干等着,其他任务也都被阻塞,无法推进 ,就像排队买票,必须等前面的人买完,下一个人才能上前买票。

而异步编程呢,则像是那只灵活的兔子。当程序遇到一个可能会耗时的任务时,它不会傻等这个任务完成,而是先把这个任务扔到一边,自己继续去执行后续的代码。等那个异步任务完成了,再通过特定的机制(比如回调函数、事件监听、Promise 等)来通知程序处理结果。这就好比你点了外卖,下单之后不需要一直盯着手机等待外卖送达,你可以继续做自己的事情,等外卖到了,手机会收到通知(回调)。

1.3异步编程为何在 C++ 中如此重要

C++ 作为一门强大的编程语言,在系统级开发、游戏开发、嵌入式系统、高性能计算等众多领域都有着广泛的应用 。在这些场景中,异步编程发挥着举足轻重的作用。

以系统级开发为例,操作系统需要同时处理多个任务,如文件读写、网络通信、用户输入等。如果采用同步编程,当进行文件读写时,整个系统可能会被阻塞,无法及时响应其他任务,导致系统性能大幅下降。而异步编程可以让操作系统在等待文件读写完成的过程中,继续处理其他任务,大大提高了系统的响应速度和吞吐量。

在游戏开发中,为了实现流畅的画面和实时交互,游戏需要在同一时间内处理图形渲染、用户操作、网络同步等多个任务。异步编程能够使这些任务并行执行,避免了因某个任务的阻塞而影响整个游戏的运行,从而提升了游戏的性能和用户体验。

在高性能计算领域,C++ 常用于处理大规模数据和复杂算法。异步编程可以充分利用多核处理器的优势,将不同的计算任务分配到不同的核心上并行执行,大大缩短了计算时间,提高了计算效率。

二、探索 C++中的异步编程工具

了解了异步编程的重要性之后,接下来就来看看 C++ 中用于实现异步编程的强大工具。

2.1 std::async:异步编程的得力助手

std::async 是 C++ 标准库提供的一个函数模板,用于异步执行任务。它的基本语法如下:

std::future<返回类型> future = std::async(启动策略, 函数名, 参数1, 参数2, ...);

其中,启动策略 是一个可选参数,用于指定任务的执行方式,有以下几种取值:

  1. std::launch::async:表示任务将在一个新线程中异步执行。就像你在餐厅点餐,服务员接到你的订单后,立即把订单交给厨房的厨师,厨师在厨房(新线程)里开始为你烹饪美食。

  2. std::launch::deferred:任务会被延迟执行,直到调用 future.get() 或 future.wait() 时才会在调用线程中执行。这就好比服务员先把你的订单放在一边,等你催促(调用 get 或 wait)的时候,才开始让厨师做菜。

  3. std::launch::async | std::launch::deferred:这是默认策略,由系统决定是立即异步执行还是延迟执行。

如果不指定启动策略,默认使用 std::launch::async | std::launch::deferred。

std::async 的返回值是一个 std::future 对象,通过它可以获取异步任务的执行结果 。例如:

#include <iostream>
#include <future>
#include <chrono>

// 模拟一个耗时任务
int heavyTask() {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时2秒
    return 42;
}

int main() {
    // 启动异步任务
    std::future<int> futureResult = std::async(std::launch::async, heavyTask);

    std::cout << "Doing other things while waiting for the task to complete..." << std::endl;

    // 获取异步任务的结果,如果任务还未完成,get() 会阻塞等待
    int result = futureResult.get();

    std::cout << "The result of the heavy task is: " << result << std::endl;

    return 0;
}

在上面的代码中,std::async 启动了一个异步任务 heavyTask,主线程在等待任务完成的过程中可以继续执行其他操作,当调用 futureResult.get() 时,如果任务尚未完成,主线程会被阻塞,直到任务完成并返回结果。

2.2 std::future:获取异步操作结果的窗口

std::future 是一个模板类,用于获取异步操作的结果。它就像是一个窗口,通过这个窗口可以窥视异步任务的执行状态和获取最终的结果。std::future 提供了以下几个重要的成员函数:

①get():获取异步操作的结果。如果异步任务还未完成,调用 get() 会阻塞当前线程,直到任务完成并返回结果。如果任务在执行过程中抛出了异常,get() 会重新抛出该异常。例如:

std::future<int> future = std::async([]() {
    return 1 + 2;
});
int result = future.get(); // 阻塞等待任务完成并获取结果
std::cout << "Result: " << result << std::endl;

②wait():阻塞当前线程,直到异步任务完成,但不返回结果。常用于在不关心结果,只需要等待任务完成的场景。比如:

std::future<void> future = std::async([]() {
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(3));
});
std::cout << "Waiting for the task to finish..." << std::endl;
future.wait();
std::cout << "Task finished." << std::endl;

wait_for():阻塞当前线程一段时间,等待异步任务完成。返回一个 std::future_status 枚举值,表示等待的结果,可能的值有:

  1. std::future_status::ready:任务已完成。

  2. std::future_status::timeout:等待超时,任务还未完成。

  3. std::future_status::deferred:任务是延迟执行的,还未开始执行。

std::future<int> future = std::async([]() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 42;
});

std::this_thread::sleep_for(std::chrono::seconds(1)); // 主线程先干点别的

auto status = future.wait_for(std::chrono::seconds(1));
if (status == std::future_status::ready) {
    int result = future.get();
    std::cout << "Task completed, result: " << result << std::endl;
} else if (status == std::future_status::timeout) {
    std::cout << "Task is still in progress." << std::endl;
} else if (status == std::future_status::deferred) {
    std::cout << "Task is deferred." << std::endl;
}

wait_until():阻塞当前线程直到指定的时间点,等待异步任务完成。用法与 wait_for() 类似,只是等待的结束条件是时间点而不是时间段。

2.3 std::promise:线程间通信的桥梁

std::promise 也是一个模板类,它通常与 std::future 搭配使用,用于在不同线程之间传递数据 。std::promise 可以看作是一个承诺,它承诺在未来的某个时刻会提供一个值,而 std::future 则可以获取这个承诺的值。

std::promise 的基本原理是:在一个线程中创建一个 std::promise 对象,然后将与该 std::promise 关联的 std::future 对象传递给其他线程。在创建 std::promise 的线程中,通过调用 std::promise 的 set_value() 成员函数来设置一个值(或者调用 set_exception() 来设置一个异常),而在其他持有 std::future 的线程中,可以通过 std::future 的 get() 方法来获取这个值(如果设置的是异常,get() 会抛出该异常)。

下面是一个简单的示例,展示了如何使用 std::promise 和 std::future 在线程间传递数据:

#include <iostream>
#include <thread>
#include <future>

// 线程函数,设置 promise 的值
void setPromiseValue(std::promise<int>& promise) {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
    promise.set_value(42); // 设置 promise 的值
}

int main() {
    std::promise<int> promise;
    std::future<int> future = promise.get_future(); // 获取与 promise 关联的 future

    std::thread thread(setPromiseValue, std::ref(promise)); // 创建线程并传递 promise

    std::cout << "Waiting for the result..." << std::endl;
    int result = future.get(); // 阻塞等待,直到 promise 设置值
    std::cout << "The result is: " << result << std::endl;

    thread.join(); // 等待线程结束

    return 0;
}

在这个例子中,主线程创建了一个 std::promise<int> 对象和与之关联的 std::future<int> 对象。然后创建了一个新线程 thread,并将 std::promise 对象传递给新线程的函数 setPromiseValue。在新线程中,经过一段时间的模拟耗时操作后,调用 promise.set_value(42) 设置了 std::promise 的值。而主线程在调用 future.get() 时会被阻塞,直到新线程设置了 std::promise 的值,然后获取到这个值并输出。

2.4 std::packaged_task:封装可调用对象的利器

std::packaged_task 是一个模板类,用于封装一个可调用对象(如函数、lambda 表达式、函数对象等),以便异步执行该任务,并通过 std::future 获取结果。它的作用是将任务的执行和结果的获取分离开来,使得任务可以在不同的线程中异步执行 。

std::packaged_task 的基本原理是:将一个可调用对象封装在 std::packaged_task 对象中,当调用 std::packaged_task 对象时,它会在后台线程中执行封装的可调用对象,并将执行结果存储在一个共享状态中。通过调用 std::packaged_task 的 get_future() 方法,可以获取一个与该共享状态关联的 std::future 对象,从而在其他线程中获取任务的执行结果。

例如,假设有一个计算两个整数之和的函数,现在想要异步执行这个计算任务并获取结果,可以使用 std::packaged_task 来实现:

#include <iostream>
#include <future>
#include <thread>

// 计算两个整数之和的函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 创建一个 packaged_task 对象,封装 add 函数
    std::packaged_task<int(int, int)> task(add);

    // 获取与 task 关联的 future 对象,用于获取任务结果
    std::future<int> futureResult = task.get_future();

    // 在新线程中执行任务
    std::thread thread(std::move(task), 3, 5);

    // 主线程可以继续执行其他操作
    std::cout << "Doing other things while the task is running..." << std::endl;

    // 获取异步任务的结果
    int result = futureResult.get();

    std::cout << "The result of the addition is: " << result << std::endl;

    thread.join(); // 等待线程结束

    return 0;
}

在上述代码中,首先创建了一个 std::packaged_task<int(int, int)> 对象 task,并将 add 函数封装在其中。然后通过 task.get_future() 获取了一个 std::future<int> 对象 futureResult,用于获取任务的结果。接着,创建了一个新线程 thread,并将 task 移动到新线程中执行,同时传递了 add 函数所需的参数 3 和 5。在主线程中,可以继续执行其他操作,最后通过 futureResult.get() 获取异步任务的结果并输出。

三、C++ 异步编程案例分析

3.1案例一:异步文件读取

在实际应用中,文件读取是一个常见的操作,尤其是在处理大文件时,如果采用同步读取方式,可能会导致程序长时间阻塞,影响用户体验。下面通过一个示例来展示如何使用 std::async 和 std::future 实现异步文件读取,并分析其性能优势。

假设我们有一个大小为 1GB 的大文件 large_file.txt,需要读取其内容。首先,使用同步方式读取文件的代码如下:

#include <iostream>
#include <fstream>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();

    std::ifstream file("large_file.txt");
    std::string content;
    file.seekg(0, std::ios::end);
    content.resize(file.tellg());
    file.seekg(0, std::ios::beg);
    file.read(&content[0], content.size());

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    std::cout << "Synchronous read time: " << duration << " ms" << std::endl;

    return 0;
}

在上述代码中,std::ifstream 用于打开文件,通过 seekg 和 tellg 函数获取文件大小并分配相应的内存空间,然后使用 read 函数将文件内容读取到字符串 content 中。整个过程是同步的,程序会阻塞直到文件读取完成。

接下来,使用异步方式读取文件:

#include <iostream>
#include <fstream>
#include <future>
#include <chrono>

std::string readFileAsync(const std::string& filename) {
    std::ifstream file(filename);
    std::string content;
    file.seekg(0, std::ios::end);
    content.resize(file.tellg());
    file.seekg(0, std::ios::beg);
    file.read(&content[0], content.size());
    return content;
}

int main() {
    auto start = std::chrono::high_resolution_clock::now();

    // 启动异步任务读取文件
    std::future<std::string> futureContent = std::async(std::launch::async, readFileAsync, "large_file.txt");

    // 主线程可以在等待文件读取的过程中执行其他操作
    std::cout << "Doing other things while reading the file..." << std::endl;

    // 获取异步任务的结果
    std::string content = futureContent.get();

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    std::cout << "Asynchronous read time: " << duration << " ms" << std::endl;

    return 0;
}

在这个异步读取的示例中,定义了一个 readFileAsync 函数,用于读取文件内容。通过 std::async 启动一个异步任务来执行这个函数,并返回一个 std::future<std::string> 对象,用于获取异步任务的结果。在主线程中,调用 futureContent.get() 之前,可以执行其他操作,当调用 get() 时,如果文件尚未读取完成,主线程会被阻塞,直到读取完成并返回结果。

通过多次测试,对比同步和异步读取大文件的时间,发现异步读取方式在读取大文件时,虽然整体耗时可能不会有明显的减少(因为文件读取本身的 I/O 操作是耗时的主要因素),但它可以让主线程在等待文件读取的过程中继续执行其他任务,提高了程序的响应性和整体效率 。例如,在一个图形界面应用中,使用异步文件读取可以避免界面在读取大文件时出现卡顿现象,用户可以继续进行其他操作,如点击按钮、切换界面等。

3.2案例二:多线程数据处理

在多线程数据处理场景中,经常会遇到需要多个线程协同工作,共同处理一批数据的情况。同时,也会面临共享数据竞争和线程同步等问题。下面通过一个示例来展示如何使用 std::thread 和 std::mutex 实现多线程数据处理,并解决共享数据竞争和线程同步问题。

假设我们有一个包含 10000 个整数的数组,需要对每个元素进行平方运算,然后将结果存储到另一个数组中。首先,使用单线程处理的代码如下:

#include <iostream>
#include <vector>
#include <chrono>

void squareArraySingleThread(std::vector<int>& input, std::vector<int>& output) {
    for (size_t i = 0; i < input.size(); ++i) {
        output[i] = input[i] * input[i];
    }
}

int main() {
    std::vector<int> input(10000);
    std::vector<int> output(10000);

    for (int i = 0; i < 10000; ++i) {
        input[i] = i + 1;
    }

    auto start = std::chrono::high_resolution_clock::now();

    squareArraySingleThread(input, output);

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    std::cout << "Single - thread processing time: " << duration << " ms" << std::endl;

    return 0;
}

上述代码中,squareArraySingleThread 函数使用单线程遍历输入数组,对每个元素进行平方运算,并将结果存储到输出数组中。

接下来,使用多线程处理:

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mutex_;

void squareArrayMultiThread(std::vector<int>& input, std::vector<int>& output, int start, int end) {
    for (int i = start; i < end; ++i) {
        std::lock_guard<std::mutex> lock(mutex_);
        output[i] = input[i] * input[i];
    }
}

int main() {
    std::vector<int> input(10000);
    std::vector<int> output(10000);

    for (int i = 0; i < 10000; ++i) {
        input[i] = i + 1;
    }

    auto start = std::chrono::high_resolution_clock::now();

    const int numThreads = 4;
    std::vector<std::thread> threads;

    int chunkSize = input.size() / numThreads;
    for (int i = 0; i < numThreads; ++i) {
        int startIndex = i * chunkSize;
        int endIndex = (i == numThreads - 1)? input.size() : (i + 1) * chunkSize;
        threads.emplace_back(squareArrayMultiThread, std::ref(input), std::ref(output), startIndex, endIndex);
    }

    for (auto& thread : threads) {
        thread.join();
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

    std::cout << "Multi - thread processing time: " << duration << " ms" << std::endl;

    return 0;
}

在多线程处理的代码中,定义了 squareArrayMultiThread 函数,该函数负责处理数组的一部分元素。通过 std::thread 创建多个线程,每个线程处理数组的一个分块。为了避免多个线程同时访问和修改输出数组时出现数据竞争问题,使用了 std::mutex 互斥锁。std::lock_guard<std::mutex> lock(mutex_); 语句使用了 RAII(Resource Acquisition Is Initialization)机制,在构造时自动锁定互斥锁,在析构时自动解锁,确保了在访问共享数据时的线程安全性。

通过多次测试,对比单线程和多线程处理数据的时间,发现多线程处理在处理大量数据时具有明显的性能优势。因为多线程可以充分利用多核处理器的优势,将任务分配到不同的核心上并行执行,从而大大缩短了处理时间 。但需要注意的是,线程的创建和管理也会消耗一定的资源,当数据量较小时,多线程处理可能会因为线程调度等开销而导致性能反而不如单线程。此外,合理地划分任务分块大小也会对性能产生影响,需要根据实际情况进行优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值