C++ Concurrent Programming: Deep Exploration of std::future, std::async, std::packaged_task and std::promise

C++ Concurrent Programming: Deep Exploration of std::future, std::async, std::packaged_task and std::promise

1. Introduction

1.1 Concept of Concurrent Programming

Concurrent programming is a computer programming technique whose core lies in enabling a program to handle multiple tasks simultaneously. On a single-core processor, only one task can run at any given time, but through task switching, the effect of concurrent execution can be created. On a multi-core processor, however, multiple tasks can truly be processed simultaneously.

The goal of concurrent programming is to improve the efficiency of program execution. Especially in the case of processing a large amount of data or requiring a large amount of calculation, the running speed of the program can be significantly improved through concurrent execution. At the same time, it can also improve the responsiveness of the program, even if some tasks are blocked during the running process, it will not affect the execution of other tasks.

Concurrent programming is not a simple task, it involves many complex issues such as data synchronization, resource sharing and deadlock etc. Therefore, it is necessary to deeply understand the various concepts and techniques of concurrent programming in order to write correct and efficient concurrent programs.

As a widely used programming language, C++ provides a complete set of concurrent programming libraries. These libraries include multithreading support, synchronization tools such as mutexes and condition variables, and tools such as std::future, std::async, std::packaged_task, and std::promise that will be explored in depth in this article.

These tools provide powerful functions that can help us write concurrent programs more conveniently. However, getting the most out of them requires a solid understanding of how they work and how to use them. This article aims to help readers understand and master these complex but powerful tools.

Next, let's start to explore the world of concurrent programming in C++ in depth.

1.2 Importance of Concurrent Programming in C++

In today's computing environment, the number of processor cores is increasing rapidly, making concurrent programming more and more important. For many applications, their potential performance cannot be realized without taking full advantage of the parallel computing capabilities of multi-core processors.

Concurrent programming has a particularly important position in C++. C++ is a multi-paradigm programming language that supports procedural programming, object-oriented programming, and generic programming. Moreover, C++ also provides a set of powerful concurrent programming libraries, allowing us to write efficient and scalable concurrent programs.

The importance of C++ concurrent programming is reflected in the following aspects:

  • Performance improvement : By taking advantage of the parallel computing capabilities of multi-core processors, we can write more efficient programs. This performance boost can be significant in situations where large amounts of data are processed or where a large amount of computation is required.
  • Improving Responsiveness : In many applications, we need to remain responsive to user input while processing long-running tasks. Through concurrent programming, we can run long-running tasks in one thread while processing user input in another thread, thereby improving the responsiveness of the program.
  • Take advantage of modern hardware : As the number of processor cores increases, future programs must be able to handle parallelism and concurrency to take full advantage of the performance of modern hardware. Concurrent programming is the key to solving this problem.
  • Evolution of programming models : Concurrent programming is an important part of modern programming models. With the development of technologies such as cloud computing, big data, and the Internet of Things, the amount of data and computing tasks we need to process are increasing, so we need to use concurrent programming to meet these needs.

To sum up, C++ concurrent programming is an important skill that any C++ programmer should master. In this article, we will delve into C++'s concurrent programming tools, including std::future, std::async, std::packaged_task, and std::promise, and show how to use them to write efficient concurrent programs through examples.

1.3 关于std::future、std::async、std::packaged_task和std::promise的简介 (Introduction to std::future, std::async, std::packaged_task, and std::promise)

In C++ concurrent programming, std::future, std::async, std::packaged_task and std::promise are four very important tools. They are all part of the C++11 concurrent programming library, and have been further optimized and improved in C++14, 17, 20 and other later versions. Below, we briefly introduce these four tools:

  • std::future : This is a template class that represents the result of an asynchronous operation. This asynchronous operation can be a function running in another thread, or a computational task. std::future provides a mechanism to get the result after it is ready without blocking the current thread.
  • std::async : This is a function that starts an asynchronous task and returns a std::future object that will hold the result of this task in the future. std::async provides an easy way to run asynchronous tasks and get their results.
  • std::packaged_task : This is a template class that encapsulates a callable object (such as a function or lambda expression) and allows asynchronously obtaining the result of the call on that object. When a std::packaged_task object is called, it internally executes the packaged callable object and saves the result in a std::future object.
  • std::promise : This is a template class that provides a way to manually set the result of a std::future object. std::promise can be very useful when you have an asynchronous task whose result needs to be available in multiple places.

These four tools provide a powerful concurrent programming model, which allows us to distribute computing tasks among multiple threads, and then obtain the results of these tasks when needed. In the following chapters, we will introduce the working principles and usage methods of these four tools in detail, and show how to use them in actual programs through sample codes.

2. std::future: storage and acquisition of asynchronous results

2.1 The basic principle and structure of std::future

In concurrent programming, we often need to pass data between multiple threads. std::futureIt is a class used to represent the results of asynchronous operations in the C++ standard library, which provides a non-blocking (or asynchronous) way to obtain the results of calculations from other threads.

Fundamental

std::futureThe way it works is simple: you create an std::futureobject, then pass it to another thread, and that thread at some point std::promiseOR std::packaged_task's the result. Then, you can call at any point std::future::get()to get the result. If the result is not ready, get()the current thread is blocked until the result is available.

std::futureis a template class whose template parameter is the result type of the asynchronous operation it represents. For example, std::future<int>to represent an asynchronous operation whose result is an integer.

structure and method

std::futureIt mainly includes the following methods:

  • get(): Get the result of an asynchronous operation. This operation blocks until the result is ready. This method should only be called once, as it destroys the internal result state.
  • valid()std::future: Check if there is a shared state associated with this . If so, return true; otherwise, return false.
  • wait(): Block the current thread until the asynchronous operation completes.
  • wait_for(), wait_until(): These two functions can be used to set a timeout, they will return if the result is not ready within the specified time.

Here's std::futurethe basic structure:

method describe
get() Get the result of an asynchronous operation, if the result is not ready, block until the result is available.
valid() std::futureChecks to see if there is a shared state associated with this .
wait() Blocks the current thread until the asynchronous operation completes.
wait_for() Blocks the current thread until the asynchronous operation completes or until the specified wait time elapses.
wait_until() Blocks the current thread until the asynchronous operation completes or until the specified point in time is reached.

With the basic principles and structure understood std::future, we can begin to explore its practical application. In the next section, we will detail std::futurethe usage scenarios and sample code.

2.2 std::future usage scenarios and sample code

The main use case for std::future is to get the result of an asynchronous operation. It is usually used with std::async, std::packaged_task or std::promise to get the result when the asynchronous task completes.

Start an asynchronous task with std::async

In this case, std::async is used to start an asynchronous task and returns a std::future object that can be used to get the result of the asynchronous task. Here is an example:

#include <future>
#include <iostream>

int compute() {
    
    
    // 假设这里有一些复杂的计算
    return 42;
}

int main() {
    
    
    std::future<int> fut = std::async(std::launch::async, compute);

    // 在这里我们可以做其他的事情

    int result = fut.get(); // 获取异步任务的结果
    std::cout << "The answer is " << result << std::endl;

    return 0;
}

Wrap a callable object with std::packaged_task

std::packaged_task can wrap a callable object and allow you to get the result of the call on that object. Here is an example:

#include <future>
#include <iostream>

int compute() {
    
    
    // 假设这里有一些复杂的计算
    return 42;
}

int main() {
    
    
    std::packaged_task<int()> task(compute);
    std::future<int> fut = task.get_future();

    // 在另一个线程中执行任务
    std::thread(std::move(task)).detach();

    int result = fut.get(); // 获取异步任务的结果
    std::cout << "The answer is " << result << std::endl;

    return 0;
}

Use std::promise to explicitly set the result of an asynchronous operation

std::promise provides a way to manually set the result of an asynchronous operation. This is useful in situations where you need more control or when an asynchronous operation cannot return a result directly.

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

void compute(std::promise<int> prom) {
    
    
    // 假设这里有一些复杂的计算
    prom.set_value(42);
}

int main() {
    
    
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    // 在另一个线程中执行任务
    std::thread(compute, std::move(prom)).detach();

    int result = fut.get(); // 获取异步任务的结果
    std::cout << "The answer is " << result << std::endl;

    return 0;
}

The above are the main usage scenarios and some basic sample codes of std::future. In the next section, we'll explore the use of std::future in more advanced applications.

2.3 Application of std::future in advanced applications

std::future can not only be used for simple asynchronous task result acquisition, but more importantly, it provides a basis for writing complex concurrent and parallel codes. Below we will introduce several usages of std::future in advanced applications.

chain of asynchronous operations

We can create chains of asynchronous operations by using std::future and std::async. In this chain, the output of one operation is used as the input of the next operation, but these operations can be executed concurrently on different threads.

#include <future>
#include <iostream>

int multiply(int x) {
    
    
    return x * 2;
}

int add(int x, int y) {
    
    
    return x + y;
}

int main() {
    
    
    std::future<int> fut = std::async(std::launch::async, multiply, 21);
    
    // 启动另一个异步任务,该任务需要等待第一个异步任务的结果
    std::future<int> result = std::async(std::launch::async, add, fut.get(), 20);
    
    std::cout << "The answer is " << result.get() << std::endl;
    return 0;
}

Asynchronous Data Stream Pipeline

We can also use std::future to create asynchronous dataflow pipelines, where each stage can execute concurrently on a different thread.

#include <future>
#include <iostream>
#include <queue>

std::queue<std::future<int>> pipeline;

void stage1() {
    
    
    for (int i = 0; i < 10; ++i) {
    
    
        auto fut = std::async(std::launch::async, [](int x) {
    
     return x * 2; }, i);
        pipeline.push(std::move(fut));
    }
}

void stage2() {
    
    
    while (!pipeline.empty()) {
    
    
        auto fut = std::move(pipeline.front());
        pipeline.pop();
        int result = fut.get();
        std::cout << result << std::endl;
    }
}

int main() {
    
    
    std::thread producer(stage1);
    std::thread consumer(stage2);
    producer.join();
    consumer.join();
    return 0;
}

These are just some examples of std::future in advanced applications. In fact, std::future is a very important part of C++ concurrent programming, and it can be used to build various complex concurrent and parallel structures.

Dependencies between asynchronous tasks

When we have some tasks that need to be executed in a specific order, we can use std::future to implement this dependency.

#include <future>
#include <iostream>

int task1() {
    
    
    // 假设这是一个耗时的任务
    return 42;
}

int task2(int x) {
    
    
    // 这个任务依赖于task1的结果
    return x * 2;
}

int main() {
    
    
    std::future<int> fut1 = std::async(std::launch::async, task1);
    std::future<int> fut2 = std::async(std::launch::async, task2, fut1.get());
    
    std::cout << "The answer is " << fut2.get() << std::endl;
    return 0;
}

Cancel an async task after a timeout

We can use std::future::wait_for to implement the function of canceling an asynchronous task after a timeout.

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

void task() {
    
    
    // 假设这是一个可能会超时的任务
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "Task completed" << std::endl;
}

int main() {
    
    
    std::future<void> fut = std::async(std::launch::async, task);
    
    std::future_status status = fut.wait_for(std::chrono::seconds(2));
    if (status == std::future_status::timeout) {
    
    
        std::cout << "Task cancelled due to timeout" << std::endl;
    } else {
    
    
        std::cout << "Task completed within timeout" << std::endl;
    }
    
    return 0;
}

Note that this example doesn't actually cancel the async task, it just stops waiting after the task times out. True task cancellation is a more complex problem in C++ and requires the use of other techniques.

These examples only show part of the usage of std::future. In fact, std::future can be used to deal with various complex concurrency and parallel problems.

3. std::async: Launching and Managing Asynchronous Tasks (std::async: Launching and Managing Asynchronous Tasks)

3.1 Basic Principles and Structure of std::async (Basic Principles and Structure of std::async)

Introduced by C++11 std::async, this is a very convenient asynchronous execution mechanism that allows us to more easily implement concurrency and parallel operations. std::asyncAn asynchronous task can be started immediately, or its execution can be delayed, depending on the start policy passed to it. This asynchronous task can be a function, a function pointer, a function object, a lambda expression, or a member function.

In C++, std::asynca function returns an std::futureobject that can be used to get the result of an asynchronous task. After the asynchronous task is completed, the result can std::future::get()be obtained through the function. If the result is not ready, this call will block until the result is ready.

Here is std::asyncthe basic prototype of:

template< class Function, class... Args >
std::future<std::invoke_result_t<std::decay_t<Function>, std::decay_t<Args>...>>
    async( Function&& f, Args&&... args );

The parameter here Functionis an asynchronous task, Argswhich is the parameter passed to the task. std::asyncReturns a std::futurewhose template parameter is Functionthe return type of .

std::asyncThere are two modes:

  1. Asynchronous mode ( std::launch::async): The new thread is started immediately and executes the task.
  2. Deferred mode ( std::launch::deferred): The task executes when future::get()orfuture::wait()

The default mode is std::launch::async | std::launch::deferredthat the system decides whether to start a new thread immediately, or to delay execution.

std::asyncThe main advantage of is that it allows you to decouple the need for results (passes std::future) from the execution of tasks, allowing you to organize your code more flexibly without having to worry about the details of thread management.

3.2 Use Cases and Example Code for std::async of std::async

std::asyncIt is a powerful tool for asynchronous programming, which can handle various scenarios, such as: parallel computing, background tasks, delayed computing, etc.

Below, we'll demonstrate the use of it with some sample code std::async.

3.2.1 Basic asynchronous tasks

Here is an example of a simple asynchronous task where we calculate an element of the Fibonacci sequence:

#include <future>
#include <iostream>

int fibonacci(int n) {
    
    
    if (n < 3) return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}

int main() {
    
    
    std::future<int> fut = std::async(fibonacci, 10);
    // 执行其他任务...
    int res = fut.get();  // 获取异步任务的结果
    std::cout << "The 10th Fibonacci number is " << res << "\n";
    return 0;
}

In the above code, we create an asynchronous task to calculate the 10th element of the Fibonacci sequence, and then continue to execute other tasks. When we need the result of the Fibonacci number, we call it fut.get(). If the asynchronous task has been completed at this time, we can get the result immediately; if the asynchronous task has not been completed, the call will block until the result is available.

3.2.2 Asynchronous execution mode and delayed execution mode

std::asyncCan accept an additional argument specifying the launch strategy. Here is an example:

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

void do_something() {
    
    
    std::cout << "Doing something...\n";
}

int main() {
    
    
    // 异步模式
    std::future<void> fut1 = std::async(std::launch::async, do_something);
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 让主线程睡眠1秒
    fut1.get();

    // 延迟模式
    std::future<void> fut2 = std::async(std::launch::deferred, do_something);
    std::this_thread::sleep_for(std::chrono::seconds(1));  // 让主线程睡眠1秒
    fut2.get();

    return 0;
}

In the above code, we first start a task in asynchronous mode, and this task will immediately start executing in a new thread. Then, we start another task in deferred mode, and this task will not fut2.get()start executing until we call it.

3.2.3 Error Handling

If an exception is thrown in an asynchronous task, the exception will be propagated to the caller std::future::get(). This makes error handling easy:

#include <future>
#include <iostream>

int calculate() {
    
    
    throw std::runtime_error("Calculation error!");
     return 0;  // 这行代码永远不会被执行
}

int main() {
    
    
    std::future<int> fut = std::async(calculate);
    try {
    
    
        int res = fut.get();
    } catch (const std::exception& e) {
    
    
        std::cout << "Caught exception: " << e.what() << "\n";
    }
    return 0;
}

In the above code, the async task calculate()throws an std::runtime_errorexception. This exception was propagated to the main thread, where we caught the exception and printed the exception message.

std::asyncProvides a simple and safe way to deal with concurrent and parallel programming, it hides the details of thread management and result acquisition, so that we can focus on the actual task.

3.3 Applications of std::async in Advanced Use Cases

std::asyncNot only can it be used for simple asynchronous tasks, but it can also play a role in some advanced application scenarios. These applications usually involve a large number of calculations or scenarios that require parallel processing.

3.3.1 Parallel Algorithms

In cases where large amounts of data need to be processed, we can use std::asyncto parallelize the algorithm. For example, suppose we need to sort an array, we can split the array in half, sort the halves separately in two asynchronous tasks, and finally combine the results.

Here's an example of a parallel sort:

#include <algorithm>
#include <future>
#include <vector>

template <typename T>
void parallel_sort(std::vector<T>& v) {
    
    
    if (v.size() <= 10000) {
    
      // 对于小数组,直接排序
        std::sort(v.begin(), v.end());
    } else {
    
      // 对于大数组,分成两半并行排序
        std::vector<T> v1(v.begin(), v.begin() + v.size() / 2);
        std::vector<T> v2(v.begin() + v.size() / 2, v.end());

        std::future<void> fut = std::async([&v1] {
    
     parallel_sort(v1); });
        parallel_sort(v2);
        fut.get();

        std::merge(v1.begin(), v1.end(), v2.begin(), v2.end(), v.begin());
    }
}

In this example, we define a parallel sort function parallel_sort. If the size of the array is less than 10000, we directly sort the array; if the size of the array is greater than 10000, we split the array in half, then sort the first half in one asynchronous task, sort the second half in the main thread, and finally merge result.

3.3.2 Background tasks

In some cases, we may need to perform some tasks in the background, which may take a long time to complete. For example, we may need to download a file in the background, or perform some complex calculations. std::asyncProvides an easy way to handle this situation.

Here's an example that downloads a file in the background:

#include <future>
#include <iostream>
#include <string>

std::string download_file(const std::string& url) {
    
    
    // 用你的下载库下载文件...
    return "file content";
}

int main() {
    
    
    std::future<std::string> fut = std::async(download_file, "http://example.com/file");
    // 在此处执行其他任务...
    std::string file_content = fut.get();
    std::cout << "Downloaded file content: " << file_content << "\n";
    return 0;
}

In this example, we download a file in an asynchronous task and then continue with other tasks. When we need the contents of a file, we call fut.get()getresult.

3.3.3 Asynchronous log system

In many systems, the logging system is a key component used to record the operation of the program. However, writing to the log can be a time-consuming operation, especially when we need to write a large number of logs. Using std::async, we can put the log write operation in a separate thread, thus avoiding blocking the main thread.

Here is a simple example of an asynchronous logging system:

#include <future>
#include <iostream>
#include <string>

void write_log(const std::string& log) {
    
    
    // 在这里写入日志...
}

void log_async(const std::string& log) {
    
    
    std::async(std::launch::async, write_log, log);
}

int main() {
    
    
    log_async("Start program");
    // 执行其他任务...
    log_async("End program");
    return 0;
}

In this example, we write to the log in an asynchronous task, then return immediately without waiting for the log write to complete. This way, we can write to the log without blocking the main thread.

3.3.4 Real-time computing system

In some real-time computing systems, we may need to complete some tasks within a certain period of time, otherwise we need to abort these tasks. std::asyncand std::futureprovide a simple way to achieve this requirement.

Here is an example of a real-time computing system:

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

int calculate() {
    
    
    // 在这里执行一些复杂的计算...
    return 42;
}

int main() {
    
    
    std::future<int> fut = std::async(std::launch::async, calculate);
    std::chrono::milliseconds span(100);  // 最多等待100毫秒
    if (fut.wait_for(span) == std::future_status::ready) {
    
    
        int result = fut.get();
        std::cout << "Result is " << result << "\n";
    } else {
    
    
        std::cout << "Calculation did not finish in time\n";
    }
    return 0;
}

In this example, we perform calculations in an asynchronous task, then wait for up to 100 milliseconds. If the computation completed within this time, we fetch the result; otherwise, we print a message that the computation did not complete within the time.

Four, std::packaged_task: encapsulates the function of the callable target

4.1 The basic principle and structure of std::packaged_task

std::packaged_taskIt is a tool introduced by C++11. Its main function is to encapsulate callable objects, such as functions, lambda expressions, function pointers or function objects, which allows us to perform these tasks in different contexts or threads. std::packaged_taskTo abstract asynchronous operations, it can be regarded as a "package" that contains all the necessary information for asynchronous operations.

Fundamental

Internally , it associates std::packaged_taskthe encapsulated callable with an object . std::futureWhen we call std::packaged_taskan object, it performs the task it encapsulates, and then stores the result in std::future. In this way, we can std::futureget the result of the task through this, no matter which thread the task is completed in.

// 创建一个 packaged_task,它将 std::plus<int>() 封装起来
std::packaged_task<int(int, int)> task(std::plus<int>());

// 获取与 task 关联的 future
std::future<int> result_future = task.get_future();

// 在另一个线程中执行 task
std::thread(std::move(task), 5, 10).detach();

// 在原线程中,我们可以从 future 中获取结果
int result = result_future.get();  // result == 15

structure

std::packaged_taskis a template class whose template parameter is the type of the callable object. For example, if we have a function that returns voidand takes one intparameter, then we can create an std::packaged_task<void(int)>object of .

std::packaged_taskIt mainly includes the following public member functions:

  • Constructor: Used to construct std::packaged_taskobjects and encapsulate callable objects inside.
  • operator(): Used to call the encapsulated task.
  • valid(): Used to check std::packaged_taskif there is a packaged task.
  • get_future(): Used to get the object std::packaged_taskassociated with std::future.
  • swap(): Used to exchange the contents of two std::packaged_taskobjects.

By judicious use std::packaged_task, we can better manage asynchronous tasks and get the results of tasks from anywhere. This is a very useful tool in C++ concurrent programming.

4.2 Usage scenarios and sample codes of std::packaged_task

std::packaged_taskIt has a wide range of applications in multi-threaded programming, and is mainly suitable for scenarios that need to perform tasks asynchronously and obtain results. Here are a few std::packaged_tasktypical scenarios of use:

  1. Asynchronous task execution : You can use it when you need to execute a task in another thread and want to get the result in the current thread std::packaged_task.
  2. Task queue : You can create a std::packaged_taskqueue, put tasks into the queue, and have one or more worker threads execute these tasks.
  3. Future/Promise model : You can use std::packaged_taskFuture/Promise model, which std::futureis used to get the result, std::packaged_taskused to execute the task and store the result.

Here is std::packaged_taskan example usage of one:

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

// 一个要在子线程中执行的函数
int calculate(int x, int y) {
    
    
    return x + y;
}

int main() {
    
    
    // 创建一个packaged_task,将calculate函数封装起来
    std::packaged_task<int(int, int)> task(calculate);

    // 获取与task关联的future
    std::future<int> result = task.get_future();

    // 创建一个新线程并执行task
    std::thread task_thread(std::move(task), 5, 10);
    
    // 在主线程中,我们可以从future中获取结果
    int result_value = result.get();

    std::cout << "Result: " << result_value << std::endl;  // 输出: Result: 15

    task_thread.join();

    return 0;
}

In this example, we create an std::packaged_taskobject taskthat calculatewraps the function. Then we execute in a new thread task, and get the result in the main thread std::future. This way we can execute tasks asynchronously and get results when needed.

4.3 Application of std::packaged_task in advanced applications

std::packaged_taskThere are many advanced applications in complex multi-threaded environments, such as task queues, thread pools, and asynchronous task chains. A few application cases are briefly introduced below.

task queue

A task queue is a common multithreaded design pattern that allows multiple producer threads to submit tasks that are then executed by one or more consumer threads. std::packaged_taskIt is very suitable for implementing task queues, because it can encapsulate arbitrary callable objects into a unified interface.

#include <queue>
#include <future>
#include <mutex>

// 任务队列
std::queue<std::packaged_task<int()>> tasks;
std::mutex tasks_mutex;

// 生产者线程
void producer() {
    
    
    // 创建一个packaged_task
    std::packaged_task<int()> task([]() {
    
     return 7 * 7; });

    // 将task添加到任务队列中
    std::lock_guard<std::mutex> lock(tasks_mutex);
    tasks.push(std::move(task));
}

// 消费者线程
void consumer() {
    
    
    // 从任务队列中取出一个task并执行
    std::lock_guard<std::mutex> lock(tasks_mutex);
    if (!tasks.empty()) {
    
    
        std::packaged_task<int()> task = std::move(tasks.front());
        tasks.pop();
        task();
    }
}

Thread Pool

Thread pool is a common multithreading design pattern, which creates a certain number of threads and reuses these threads to perform tasks. std::packaged_taskCan be used to implement tasks in a thread pool, as it can execute a task in one thread and get the result in another thread.

asynchronous task chain

Asynchronous task chaining is a design pattern where the result of one task is used as input for the next task. std::packaged_taskCan be used to implement asynchronous task chains, as it can store the result in a task when it completes std::future, and this std::futurecan then be used by the next task to get the result.

// 第一个任务
std::packaged_task<int()> task1([]() {
    
     return 7 * 7; });
std::future<int> future1 = task1.get_future();

// 第二个任务,它的输入是第一个任务的结果
std::packaged_task<int(int)> task2([](int x) {
    
     return x + 1; });
std::future<int> future2 = task2.get_future();

// 在一个线程中执行第一个任务
std::thread(std::move(task1)).detach();

// 在另一个线程中执行第二个任务
std::thread([&]() {
    
    
    task2(future1.get());
}).detach();

// 获取第二个任务的结果
int result = future2.get();

In this example, we create a chain of asynchronous tasks where the first task calculates 7 * 7and the second task increments the result. We execute these two tasks in two different threads, and then

Then get the final result in the main thread. This demonstrates std::packaged_taskgreat power in advanced concurrent programming.

Features\Models task queue Thread Pool asynchronous task chain
Applicable scene Need to distribute and execute tasks in multiple threads, suitable for producer-consumer model It is necessary to optimize task execution performance and avoid frequent creation and destruction of threads, which is suitable for scenarios with high concurrency and a large amount of tasks There are dependencies between tasks, and downstream tasks need to use the results of upstream tasks, suitable for data processing and computationally intensive tasks
resource usage The number of threads can be dynamically adjusted according to the length of the task queue, and resource usage is flexible A fixed number of threads, created and reused in advance, stable resource usage Each task may be executed in a different thread, resource usage is flexible, but more inter-thread synchronization may be required
task management Tasks are managed through queues, and tasks can be scheduled according to policies such as first-in-first-out or priority Tasks are usually managed by the task queue inside the thread pool, and the thread pool is responsible for task scheduling and execution The management of tasks needs to be carried out according to the dependencies between tasks, usually requiring more complex logic
result acquisition By std::futuregetting the result, you can get it asynchronously, or block and wait for the result By std::futuregetting the result, you can get it asynchronously, or block and wait for the result By std::futuregetting the result, you can get it asynchronously, or block and wait for the result, and the downstream task can directly use the result of the upstream task
error handling Errors usually need to be caught in the thread that executes the task and std::futurepassed to the thread that gets the result via Errors usually need to be caught in the thread that executes the task and std::futurepassed to the thread that gets the result via Errors usually need to be caught in the thread that executes the task and std::futurepassed to the thread that gets the result. The error may cause the interruption of the entire task chain

5. std::promise: Promise of Asynchronous Operation Results (std::promise: Promise of Asynchronous Operation Results)

5.1 Basic Principles and Structure of std::promise (Basic Principles and Structure of std::promise)

std::promise is a concurrent programming tool introduced in C++11 and later versions, which allows us to set a value or exception in one thread, and then get this value or exception in another thread. Such features make std::promise a powerful means of inter-thread communication.

Imagine that you are throwing a big party and you need to promise your guests that they will get delicious food. In this case, you might hire a chef to prepare the food. You promise your guests that there will be good food, and the chef works behind the scenes, doing his best to fulfill your promise. In C++, this process is like one thread (the chef) doing the work while another thread (you) waits for the result.

Now, let's dive into the underlying principles and structure of std::promise.

Fundamental

The rationale behind std::promise is simple. When you create a std::promise object, you can give it a value or an exception. The value or exception can be retrieved by a std::future object associated with the promise. This is a standard "producer-consumer" model, where promises are producers and futures are consumers.

structure

std::promise is a template class that takes a template parameter T, representing the type of the promised value. A std::promise object can set a value through its member function set_value, or set an exception through its member function set_exception. These values ​​or exceptions can be accessed through the std::future object associated with them.

Here is a simple usage example of std::promise:

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

void my_promise(std::promise<int>& p) {
    
    
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
    p.set_value(42); // 设置值
}

int main() {
    
    
    std::promise<int> p;
    std::future<int> f = p.get_future(); // 获取 future
    std::thread t(my_promise, std::ref(p)); // 在新线程中运行函数
    std::cout << "Waiting for the answer...\n";
    std::cout << "The answer is " << f.get() << '\n'; // 获取值
    t.join();
    return 0;
}

In this example, the my_promise function runs in a new thread and sets the promise value to 42. The main thread waits for the value of the future, then prints it. Note that the main thread blocks at f.get() until the value of the promise is set.

In the next section, we will introduce the usage scenarios and sample code of std::promise in detail.

5.2 Use Cases and Example Code for std::promise of std::promise

scenes to be used

The most common use case for std::promise is inter-thread communication in a multi-threaded environment, especially when you need to set a value (or an exception) in one thread and get that value (or an exception) in another thread )hour.

Additionally, std::promise can be used in the following scenarios:

  1. Asynchronous tasks: When you need to run a task that might take a long time, and you don't want to wait for the task to complete, you can use std::promise to run the task in a new thread and get the result in the main thread.
  2. Dataflow pipeline: You can use a series of std::promise and std::future objects to create a dataflow pipeline, where each thread is part of the pipeline, and each thread provides data through the std::promise object , and then get the data through the std::future object.

sample code

Let's go through an example to show how to use std::promise. In this example, we'll use a promise to deliver the result of a calculation from a new thread.

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

// 这个函数将会在一个新线程中被运行
void compute(std::promise<int>& p) {
    
    
    int result = 0;
    // 做一些计算...
    for (int i = 0; i < 1000000; ++i) {
    
    
        result += i;
    }
    // 计算完成,设置 promise 的值
    p.set_value(result);
}

int main() {
    
    
    // 创建一个 promise 对象
    std::promise<int> p;
    // 获取与 promise 关联的 future 对象
    std::future<int> f = p.get_future();
    // 在新线程中运行 compute 函数
    std::thread t(compute, std::ref(p));
    // 在主线程中获取结果
    std::cout << "The result is " << f.get() << std::endl;
    // 等待新线程完成
    t.join();
    return 0;
}

In this example, we run a potentially long computation in a new thread and use a promise to deliver the computation result. In the main thread, we get this result through the future object. f.get()When we call , the main thread blocks until the new thread finishes computing and sets the value of the promise.

5.3 Applications of std::promise in Advanced Use Cases

std::promise can not only be used in basic multi-threaded programming, but also has some advanced application scenarios, such as being used in combination with other concurrency tools to improve program performance and efficiency. Here are two advanced use cases for using std::promise:

Advanced Application 1: Chained Asynchronous Tasks

In some cases, you may need to perform a series of asynchronous tasks, where the input of each task depends on the output of the previous task. In this case, you can create a chain of promises and futures, each task has an input future and an output promise, so that the order of execution of tasks can be guaranteed, and the result of each task can be easily obtained.

For example, the following code shows how to use a chain of promises and futures to perform a series of asynchronous tasks:

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

void chain_task(std::future<int>& f, std::promise<int>& p) {
    
    
    int input = f.get(); // 获取输入
    int output = input * 2; // 执行一些计算
    p.set_value(output); // 设置输出
}

int main() {
    
    
    // 创建 promise 和 future 的链
    std::promise<int> p1;
    std::future<int> f1 = p1.get_future();
    std::promise<int> p2;
    std::future<int> f2 = p2.get_future();

    // 在新线程中运行异步任务
    std::thread t1(chain_task, std::ref(f1), std::ref(p2));
    std::thread t2(chain_task, std::ref(f2), std::ref(p1));

    // 设置初始输入
    p1.set_value(42);

    // 获取最终结果
    std::cout << "The final result is " << f1.get() << std::endl;

    // 等待新线程完成
    t1.join();
    t2.join();

    return 0;
}

Advanced application two: combined with other concurrency tools

std::promise can be combined with other concurrency tools in the C++ standard library, such as std::async, std::packaged_task, std::thread, etc., to create more complex concurrency patterns.

For example, you can use std::async to start an asynchronous task, and std::promise to deliver the result of the task. You can also use std::packaged_task to wrap a task that can be run in a new thread, and use std::promise to set the result of the task. In this case, std::promise can provide a more advanced inter-thread communication mechanism, allowing you to share data and state among different threads.

The above are some usage scenarios of std::promise in advanced applications, hoping to help you better understand and use this tool. In the next chapter, we'll cover the comparison and choice of std::future, std::async, std::packaged_task, and std::promise.

6. Parallel classes and thread pools

parallel library

std::futureis part of the C++ standard library and represents a value that may be computed on other threads in the future. std::futureitself does not directly involve the thread pool. However, it is often std::asyncused in conjunction with mechanisms such as , which can utilize thread pools to perform asynchronous tasks.

In fact, std::asyncthe behavior of a depends on the parameters given to it. If passed an argument std::launch::async, it will execute the task in a new thread. If passed an argument std::launch::deferred, the task will run synchronously std::future::get()when . In any case, std::asyncimplementations may use thread pools, depending on standard library implementations and system limitations.

In summary, std::futurenot directly related to thread pools, but it can be used with asynchronous execution mechanisms using thread pools.

In the C++ standard library, the thread pool function is not directly provided. std::futureand std::asynconly provide a basic asynchronous execution method, so in the C++ standard library, you cannot directly control the details of the thread pool, such as the number of worker threads, adjustable parameters, etc. To achieve this control, you can create a custom thread pool, or use an existing open source thread pool library.

std::packaged_taskCan also be used with thread pools, but is not itself a thread pool implementation. std::packaged_taskis a class template that wraps a callable object in C++, which allows functions to be used in std::futureconjunction . When this callable (function, lambda expression, or function object) is invoked, std::packaged_taskthe result is stored and the associated std::futurebecomes ready.

You can std::packaged_taskcreate tasks with and submit those tasks to a thread pool. This enables a task executed in the thread pool to return a std::futureobject for asynchronous access to the task result.

choose tradeoffs

Thread pools are generally more suitable for long-running tasks, because thread pools mean that thread resources can be reused when executing long-running tasks. In this way, the performance loss caused by frequently creating and destroying threads can be avoided. Thread pools also allow you to control the number of concurrent threads to meet specific performance needs or system constraints.

For short-term and infrequent tasks, std::asyncit may be more appropriate to use parallel libraries (such as those in the C++ standard library, Intel TBB, Microsoft PPL, and C++ Boost.Asio libraries). These libraries can provide a simple interface when only a small number of tasks need to be performed, and avoid the additional complexity of managing thread pools. Parallel libraries usually take care of the resource management issues of thread creation and destruction, so are a good choice for these rare tasks.

Please note that when choosing how to execute tasks concurrently, the nature of the task (such as whether the task has priority, whether it needs to be synchronized, etc.) and the library used (they will have different functions and optimizations) should also be considered factors.

custom thread pool

Here is an example of a simple custom thread pool:

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>

class ThreadPool {
    
    
public:
    ThreadPool(size_t num_threads);
    ~ThreadPool();
    void enqueue(std::function<void()> task);

private:
    void worker();

    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex tasks_mutex;
    std::condition_variable tasks_cv;
    bool stop;
};

ThreadPool::ThreadPool(size_t num_threads) : stop(false) {
    
    
    for (size_t i = 0; i < num_threads; ++i) {
    
    
        workers.emplace_back(&ThreadPool::worker, this);
    }
}

ThreadPool::~ThreadPool() {
    
    
    {
    
    
        std::unique_lock<std::mutex> lock(tasks_mutex);
        stop = true;
    }
    tasks_cv.notify_all();
    for (auto &worker : workers) {
    
    
        worker.join();
    }
}

void ThreadPool::enqueue(std::function<void()> task) {
    
    
    {
    
    
        std::unique_lock<std::mutex> lock(tasks_mutex);
        tasks.push(task);
    }
    tasks_cv.notify_one();
}

void ThreadPool::worker() {
    
    
    while (true) {
    
    
        std::function<void()> task;

        {
    
    
            std::unique_lock<std::mutex> lock(tasks_mutex);
            tasks_cv.wait(lock, [this]() {
    
     return !tasks.empty() || stop; });

            if (stop && tasks.empty()) {
    
    
                return;
            }

            task = tasks.front();
            tasks.pop();
        }

        task();
    }
}

Through the implementation of the above custom thread pool, you can freely control the size of the thread pool and manage the task queue. In addition, there are many open source thread pool libraries to choose from, such as Intel TBB, Microsoft PPL and C++ Boost.Asio library. These libraries provide more optimizations and advanced control for multithreaded programming.

Parallel library help with thread pool

Parallel classes in C++, including std::thread, std::future, std::async, std::packaged_task, and std::promise, etc., can be used to implement thread pools, which can improve the utilization of multi-core processors. It is of great help to reduce the overhead of thread creation and destruction, and to improve the responsiveness of the program. Below we discuss in detail how these classes assist in implementing the thread pool.

1. std::thread

std::thread is the foundation of C++'s thread library, which can be used to create and manage threads. When implementing a thread pool, we usually create a set of threads and save them in a container (such as std::vector). When these threads are created, they will start to execute a specific function. This function is usually an infinite loop, which continuously fetches tasks from the task queue and executes them.

2. std::future和std::promise

std::future and std::promise can be used to deliver and get the results of tasks. When implementing a thread pool, we usually create a std::promise object for each task, and return the corresponding std::future object to the caller. When the task is completed, the worker thread sets the result to the std::promise object, and the caller can get the result through the std::future object.

3. std::async

std::async is a simple asynchronous programming tool that can be used to start an asynchronous task and return a std::future object. Although std::async itself is not suitable for implementing thread pools (because it always creates new threads), we can learn from its design to simplify the interface of thread pools. Specifically, we can provide a function similar to std::async, which accepts a callable object and a set of parameters, encapsulates them into tasks and adds them to the task queue, and then returns a std::future object.

4. std::packaged_task

std::packaged_task can be seen as a class that wraps a callable object, which binds the callable object to a std::promise object. When a std::packaged_task object is called, it calls the internal callable object and saves the result in a std::promise object. When implementing a thread pool, we can use std::packaged_task to package tasks, so that any callable object can be converted into a uniform type that can be put into the task queue.

These parallel classes provide basic functions such as creating threads, executing tasks asynchronously, and delivering task results, so that we can implement efficient thread pools in C++. The use of the thread pool can better control the number of threads, avoid the overhead caused by excessive thread creation and destruction, improve the utilization of multi-core processors, and thus improve the performance of the program.

class name Functional description Realize the role of thread pool user programming perspective Practicality
std::thread Used to create and manage threads The basis of the thread pool, responsible for executing tasks Simple and easy to use, but requires manual thread lifecycle management high
std::future Used to get the result of an asynchronous task Provides a way to obtain task results, so that the caller can wait for the task to complete and obtain the result Provides a safe and easy way to get the result of an asynchronous task high
std::promise Used to set the result of an asynchronous task Provides the way to set the task result, so that the worker thread can set the result of the task It needs to be used in conjunction with std::future, which is a little more complicated to use middle
std::async Used to start asynchronous tasks You can learn from its design to simplify the interface of the thread pool Very simple and easy to use, but not suitable for implementing thread pools middle
std::packaged_task for packaging tasks Any callable object can be encapsulated as a task, so that the task can be put into the queue Simplifies task creation and delivery of results, but requires manual management of its lifecycle high

Combination of parallel library and thread pool

The following is a custom thread pool implementation using std::thread, std::future, std::promise, andstd::async .std::packaged_task

#include <iostream>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>

class ThreadPool {
    
    
public:
    // 构造函数: 创建指定数量的工作线程
    // Constructor: creates the specified number of worker threads
    ThreadPool(size_t num_threads);
    
    // 析构函数: 关闭所有线程并释放资源
    // Destructor: stops all threads and releases resources
    ~ThreadPool();

    // 任务入队函数: 将任务添加到任务队列中
    // Enqueue function: adds a task to the task queue
    template <typename F, typename... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;

private:
    // 工作线程执行函数
    // Worker thread execution function
    void worker();

    std::vector<std::thread> workers;             // 工作线程
    std::queue<std::function<void()>> tasks;      // 任务队列
    std::mutex tasks_mutex;                       // 保护任务队列的互斥锁
    std::condition_variable tasks_cv;             // 通知工作线程的条件变量
    bool stop;                                    // 标记线程池是否停止
};

ThreadPool::ThreadPool(size_t num_threads) : stop(false) {
    
    
    for (size_t i = 0; i < num_threads; ++i) {
    
    
        workers.emplace_back(&ThreadPool::worker, this);
    }
}

ThreadPool::~ThreadPool() {
    
    
    {
    
    
        std::unique_lock<std::mutex> lock(tasks_mutex);
        stop = true;
    }
    tasks_cv.notify_all();
    for (auto &worker : workers) {
    
    
        worker.join();
    }
}

template <typename F, typename... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
    
    
    using return_type = typename std::result_of<F(Args...)>::type;

    // 创建 packaged_task,包装任务,将任务与 future 关联
    auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

    std::future<return_type> result = task->get_future();
    {
    
    
        // 将任务包装为 std::function,并添加到任务队列
        std::unique_lock<std::mutex> lock(tasks_mutex);
        tasks.emplace([task](){
    
     (*task)(); });
    }
    tasks_cv.notify_one();                        // 通知一个工作线程

    return result;
}

void ThreadPool::worker() {
    
    
    while (true) {
    
    
        std::function<void()> task;

        // 从任务队列中获取任务
        {
    
    
            std::unique_lock<std::mutex> lock(tasks_mutex);
            tasks_cv.wait(lock, [this]() {
    
     return !tasks.empty() || stop; });

            // 如果线程池已停止且没有剩余任务,则退出
            if (stop && tasks.empty()) {
    
    
                return;
            }

            task = tasks.front();
            tasks.pop();
        }

        // 执行任务
        task();
    }
}

In this implementation, note the following key sections:

  • The constructor initializes the thread pool and creates the specified number of worker threads.
  • enqueue()The function is a task enqueue method, which can add tasks to the task queue. It creates a std::packaged_taskand std::futureassociates the task with the associated object. This method returns a std::futureobject that the caller can use to get the result of the asynchronous task.
  • The worker threads in the thread pool will wait and get tasks from the task queue. After the task is executed, std::futurethe object will become ready, and the task result can be obtained.
  • The destructor stops all worker threads and releases resources.

This thread pool provides basic thread management functionality, and you can extend it to support other functionality as needed, such as controlling the number of threads or providing task priorities.

Guess you like

Origin blog.csdn.net/qq_21438461/article/details/130716531