Welcome to the next lesson in our "High-Performance PDF Processing" module. In our previous session, we laid the essential groundwork by integrating the PDFium library into your CMake project. With the library now accessible, we can turn our attention to its primary purpose in your application: parsing PDF documents.
Your goal is to build a "blazingly fast" and "highly performative" whiteboard. A crucial part of this is ensuring the user interface remains fluid and responsive at all times. Heavy tasks like opening and analyzing a multi-page PDF can take several seconds, and if this work is done on the main application thread, your UI will freeze completely. This lesson directly tackles that challenge. You will learn how to use std::async and std::future to offload the entire PDF parsing process to a background thread, keeping your UI perfectly smooth and interactive.
The Problem: Blocking the Main Thread
Most GUI applications operate on a single main thread that is responsible for handling user input, updating application logic, and drawing to the screen. This is often called the "UI thread." If you execute a long-running function on this thread, it cannot do anything else until that function completes. The result is a frozen, unresponsive application—the enemy of high-performance software.

To solve this, we will use C++'s high-level concurrency tools to execute the PDF parsing task asynchronously.
std::async and std::future: Your Asynchronous Toolkit
While C++ offers std::thread for direct thread management, std::async provides a higher-level, task-based approach that is often simpler and safer for "fire-and-forget" background operations like this one.
When you call std::async, you provide it with a function to run. It launches this function (potentially on a new thread) and immediately returns a std::future.
std::async: A function template that runs a callable object (like a function or lambda) asynchronously.std::future: A class template that acts as a placeholder for a value that will be computed in the future. It is the communication channel for retrieving the result (or any exceptions) from the asynchronous task.
Let's look at the basic mechanism.
C++11 Concurrency Tutorial - Part 5: Futures
This article by Baptiste Wicht provides a clear introduction to std::async and std::future.
First, read the brief introduction explaining the concept. Then, study the two simple code examples that follow, down to the end of the second code block. Pay attention to how std::async is called with a lambda and how future.get() is used to retrieve the value.
As the article notes, future.get() is a blocking call. It will pause the thread that calls it until the background task is finished. If we call this on our main UI thread, we are right back where we started, with a frozen application. We need a way to check on the result without blocking. But before we get to that, let's address a critical subtlety of std::async.
The Destructor Trap: A Performance "Gotcha"
Your interest in low-level and performance systems means you'll appreciate this detail. The std::future returned by std::async has a special behavior: its destructor will block until the asynchronous task is complete. This means if you don't store the future in a variable, your std::async call effectively becomes synchronous.
This video demonstrates the issue perfectly.
The Trap of std::async - Learn Modern C++
The channel Code Blacksmith explains this "nasty surprise" in std::async.
Watch the entire short video. The introduction explains the basic premise and the trap. Pay close attention to the debugger demonstration from 1:28 to 2:39, where not assigning the future causes the main thread to block. See how assigning the future to a variable from 2:39 to 3:28 solves the problem, allowing true asynchronous execution. The key takeaway is simple: if you want a task to run in the background, you must store the std::future that std::async returns.
Building a Non-Blocking UI Loop
Now we can tackle the main problem: how to launch our PDF parsing and get the result later without freezing the UI. The pattern is to poll the std::future on each frame of our application's main loop.
We can check if the future has a result ready without blocking by using future.wait_for(). By passing it a zero-second duration, it returns immediately with the status of the future.
This is a common and critical mistake for developers new to std::future, and the following resource explains the problem and the solution perfectly.
Top 20 C++ multithreading mistakes and how to avoid them
This article highlights common pitfalls in C++ concurrency. We'll focus on the one most relevant to UI development.
Read section #15, titled "Calling .get() on a std::future in a time-sensitive code path". The "❌ Blocking Call in Render Loop" example shows exactly what we want to avoid. Then, carefully study the "✅ Solution: Use wait_for() with zero timeout to check readiness". This code snippet presents the canonical non-blocking pattern for checking a future in a tight loop, which is exactly what you need for your whiteboard's main application loop.
The polling logic looks like this:
// Somewhere in your application state
std::future<PdfParseResult> pdf_parsing_future;
// In your main application loop (e.g., while(window_is_open) { ... } )
void update() {
// Check if a task is running and if it's finished
if (pdf_parsing_future.valid()) { // .valid() checks if the future is usable
auto status = pdf_parsing_future.wait_for(std::chrono::seconds(0));
if (status == std::future_status::ready) {
// The result is ready! Get it (this won't block) and process it.
try {
PdfParseResult result = pdf_parsing_future.get();
// ... use the parsed PDF data ...
} catch (const std::exception& e) {
// ... handle any errors from the parsing task ...
}
}
}
// ... continue with other per-frame logic like input handling and rendering ...
}
This pattern is demonstrated very clearly in the following video, which uses a simulated file-loading task as an analogy for our PDF parsing.
std::async in cpp with background thread loading data example | Introduction to Concurrency in C++
This video from Mike Shah provides a complete, practical example of a non-blocking background task.
Watch from 1:36 to the end. The structure he builds is a model for your own application: The Goal (1:36-2:45): He outlines the architecture of a main thread running a loop while a background thread loads data. Launching the Task (2:45-5:20): He uses std::async to start the background task, storing the returned std::future. The Main Loop (6:17-9:50): This is the key part. He implements the main while(true) loop and, inside it, uses wait_for and checks for std::future_status::ready to poll for completion without blocking. The Result (12:33-14:45): He runs the code, showing the main thread and background thread printouts interleaved, proving both are running concurrently.
Guarantees and Error Handling
There are two final, important details for making this system robust.
-
Guaranteeing Asynchronous Execution: By default,
std::asyncis allowed to be deferred. This means it might not run the task on a new thread at all, but instead wait and run it synchronously the first time you call.get(). To prevent UI stalls, we must guarantee it runs on a background thread. We do this by passingstd::launch::asyncas the first argument.// This GUARANTEES the task runs on a new thread. m_pdf_future = std::async(std::launch::async, &MyClass::parsePdf, this, "path/to/doc.pdf");This is covered in Mistake #14 in the "Top 20 C++ multithreading mistakes" article.
-
Handling Exceptions: What happens if the PDF is corrupt or the file doesn't exist? Your parsing function might throw an exception. The beauty of
std::asyncis that it catches any exception thrown in the background task and stores it inside the future. The exception is then re-thrown on the calling thread when you call.get(). This makes error handling clean and simple—just wrap your.get()call in atry...catchblock, as shown in the code snippet above and detailed in Mistake #16 of the same article.

Conclusion
You now have a complete, robust pattern for executing long-running tasks in the background without compromising the responsiveness of your application. This is a fundamental technique for any high-performance, interactive software.
Key Takeaways:
- Long-running tasks like PDF parsing must be moved off the main UI thread to prevent the application from freezing.
std::asyncis a high-level tool for launching a function on a background thread. It returns astd::futurewhich holds the eventual result.- You must store the returned
std::futurein a variable to prevent its destructor from blocking and making the call synchronous. - To avoid blocking the UI, check for a result non-blockingly within your main loop using
future.wait_for(std::chrono::seconds(0)). - Only call
future.get()afterwait_forhas confirmed the status isstd::future_status::ready. - Use the
std::launch::asyncpolicy to guarantee the task runs on a new thread. - Wrap your call to
future.get()in atry...catchblock to gracefully handle exceptions from the background task.
In the next lesson, we will finally dive into the PDFium API. With the asynchronous framework in place, you will write the body of the parsePdf function itself, using PDFium to extract raw text blocks and their bounding boxes from a document page.