
Chào các 'dev-er' Gen Z, lại là anh Creyt đây! Hôm nay chúng ta sẽ 'bóc phốt' một khái niệm nghe có vẻ 'hack não' nhưng lại 'cool ngầu' cực kỳ trong C++: std::future. Nghe cái tên thôi đã thấy 'tương lai' rồi, đúng không? Nhưng thực ra nó là gì mà dân lập trình cứ 'wow' lên vậy?
1. std::future: 'Vé Số' cho kết quả của tương lai
Nói một cách 'Gen Z' nhất, std::future trong C++ giống như một cái 'vé số' hoặc một cái 'biên nhận đặt hàng online' vậy. Bạn không nhận được món hàng (kết quả) ngay lập tức, nhưng bạn có một cái biên nhận (cái future này nè) để sau này, khi nào món hàng được giao (khi tác vụ hoàn thành), bạn sẽ dùng cái biên nhận đó để lấy món hàng. Nó là một lời hứa về một kết quả sẽ có trong tương lai.
Để làm gì? Đơn giản là để chương trình của bạn không bị 'đơ' khi phải làm những việc nặng nhọc, tốn thời gian. Tưởng tượng bạn đang lướt TikTok mà app tự nhiên 'standing still' vì nó đang tải một cái video 8K siêu to khổng lồ ở background. Khó chịu đúng không? std::future cho phép bạn 'giao việc nặng' đó cho một 'trợ lý' (một luồng khác - thread khác) làm, còn bạn (luồng chính) thì cứ tiếp tục lướt TikTok hoặc làm việc khác. Khi nào 'trợ lý' làm xong, nó sẽ 'báo cáo kết quả' thông qua cái future bạn đã nhận.
Nói theo ngôn ngữ 'học thuật Harvard' một chút, std::future là một đối tượng cung cấp cơ chế để truy xuất kết quả của một tác vụ được thực thi bất đồng bộ (asynchronously). Nó đóng vai trò là một kênh giao tiếp một chiều, cho phép luồng chính (hoặc bất kỳ luồng nào khác) chờ đợi và nhận giá trị trả về hoặc ngoại lệ từ một tác vụ đã được khởi tạo trên một luồng riêng biệt.
2. Code Ví Dụ Minh Hoạ: 'Đặt hàng online' một hàm tính toán
Cách dễ nhất để tạo ra một std::future là sử dụng std::async. Coi như std::async là bạn 'đặt hàng' một hàm nào đó chạy bất đồng bộ.
#include <iostream>
#include <future> // Thư viện chứa std::future, std::async
#include <chrono> // Để dùng std::chrono::seconds
#include <thread> // Để dùng std::this_thread::sleep_for
// Hàm 'nặng nhọc' mô phỏng một tác vụ tốn thời gian (ví dụ: tính toán phức tạp, tải dữ liệu)
long long calculate_sum_of_squares(int n) {
std::cout << "[Worker Thread]: Bắt đầu tính tổng bình phương cho " << n << " số... \n";
long long sum = 0;
for (int i = 1; i <= n; ++i) {
sum += static_cast<long long>(i) * i;
// Giả lập công việc nặng nhọc
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Ngủ 10ms mỗi lần lặp
}
std::cout << "[Worker Thread]: Tính toán hoàn tất! \n";
return sum;
}
int main() {
std::cout << "[Main Thread]: Chuẩn bị giao việc nặng... \n";
// 'Đặt hàng' hàm calculate_sum_of_squares chạy bất đồng bộ
// std::async sẽ trả về một std::future<long long>
// future_result là cái 'biên nhận' của bạn
std::future<long long> future_result = std::async(std::launch::async, calculate_sum_of_squares, 100);
std::cout << "[Main Thread]: Vừa giao việc xong, giờ tôi đi lướt web đây (làm việc khác)... \n";
// Main thread có thể làm các công việc khác trong lúc worker thread đang tính toán
for (int i = 0; i < 3; ++i) {
std::cout << "[Main Thread]: Đang làm việc khác... " << i + 1 << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "[Main Thread]: Okay, tôi xong việc nhẹ rồi, giờ đi 'lấy hàng' đây! \n";
// Gọi .get() trên future để lấy kết quả.
// Nếu tác vụ chưa hoàn thành, .get() sẽ block (chờ) cho đến khi có kết quả.
try {
long long result = future_result.get(); // Đây là lúc 'shipper giao hàng' và bạn 'nhận hàng'
std::cout << "[Main Thread]: Kết quả tổng bình phương là: " << result << "\n";
} catch (const std::exception& e) {
std::cerr << "[Main Thread]: Có lỗi xảy ra trong quá trình tính toán: " << e.what() << "\n";
}
std::cout << "[Main Thread]: Chương trình kết thúc. \n";
return 0;
}
Trong ví dụ trên:
calculate_sum_of_squareslà tác vụ 'nặng nhọc'.std::async(std::launch::async, ...)khởi chạy tác vụ này trên một luồng mới, và trả về mộtstd::future<long long>.std::launch::asyncđảm bảo nó chạy trên một luồng riêng biệt.- Luồng chính (
main) tiếp tục chạy các công việc khác (in ra "Đang làm việc khác..."). - Khi luồng chính cần kết quả, nó gọi
future_result.get(). Nếu tác vụ chưa xong,get()sẽ chờ. Nếu xong rồi, nó trả về kết quả.

3. Mẹo (Best Practices) để 'xài' future cho 'pro'
.get()chỉ gọi được MỘT LẦN: Giống như bạn chỉ có thể dùng một cái vé số để đổi giải một lần thôi. Lần thứ hai gọiget()sẽ gây ra undefined behavior (hoặc crash chương trình)..get()có thể block: Nhớ nhé, khi bạn gọiget(), nếu tác vụ chưa hoàn thành, luồng hiện tại của bạn sẽ bị 'treo' cho đến khi có kết quả. Hãy cân nhắc xem khi nào thì thực sự cần kết quả.- Xử lý ngoại lệ (Exceptions): Nếu tác vụ bất đồng bộ ném ra một ngoại lệ,
get()cũng sẽ ném lại ngoại lệ đó trong luồng gọi. Luôntry-catchkhi gọiget()để đảm bảo chương trình không 'sập' bất ngờ. std::shared_futurecho nhiều người 'hóng' kết quả: Nếu bạn muốn nhiều luồng khác nhau cùng 'hóng' và lấy kết quả từ mộtfuture, hãy chuyển đổi nó thànhstd::shared_future(std::shared_future<T> shared_fut = fut.share();).shared_futurecó thể được copy và.get()có thể gọi nhiều lần (nhưng kết quả chỉ có một).std::futurelà move-only: Bạn không thể copy mộtstd::future, chỉ có thể move nó. Điều này đảm bảo rằng mỗi kết quả chỉ được liên kết với mộtfutureduy nhất (trừ khi dùngstd::shared_future).
4. Ứng dụng thực tế: future có mặt ở đâu?
std::future và các khái niệm liên quan đến bất đồng bộ là xương sống của rất nhiều ứng dụng bạn dùng hàng ngày:
- Giao diện người dùng (UI) mượt mà: Trong các ứng dụng desktop (như Photoshop, game, IDE), khi bạn thực hiện một thao tác nặng (ví dụ: áp dụng bộ lọc phức tạp, tải map game lớn, biên dịch code), UI vẫn phản hồi, không bị 'đơ'. Các tác vụ nặng được đẩy xuống các luồng khác và kết quả được trả về qua
future. - Web Servers: Khi một request đến server cần truy vấn database tốn thời gian hoặc gọi API từ một dịch vụ bên ngoài, server sẽ không 'block' toàn bộ các request khác. Nó dùng cơ chế bất đồng bộ (có thể dùng
futurehoặc các cơ chế tương tự) để xử lý request đó trong background, trong khi vẫn tiếp nhận và xử lý các request khác. - Xử lý dữ liệu lớn (Big Data): Chia nhỏ các tác vụ xử lý dữ liệu (ví dụ: nén, giải nén, phân tích) thành nhiều phần nhỏ và chạy chúng song song, sau đó tổng hợp kết quả lại.
- Game Development: Tải tài nguyên (assets), tính toán AI, xử lý vật lý trong background để game không bị giật lag.
5. Thử nghiệm và Nên dùng cho case nào?
Thử nghiệm đã từng: Hồi anh Creyt mới tập tành làm game, có một lần anh viết code tải tất cả tài nguyên (ảnh, âm thanh, model 3D) ngay trên luồng chính. Kết quả là game 'đứng hình' khoảng 5-10 giây mỗi khi khởi động. Sau đó, anh chuyển sang dùng std::async và std::future để tải tài nguyên ở một luồng riêng, UI chính chỉ hiển thị màn hình 'Loading...' và khi nào future.get() có kết quả thì mới chuyển sang màn hình chính. Trải nghiệm người dùng 'lên level' hẳn!
Nên dùng cho case nào:
- Tác vụ tốn thời gian: Bất kỳ tác vụ nào có thể kéo dài vài trăm mili giây đến vài giây (hoặc hơn) mà bạn không muốn nó làm 'đơ' chương trình chính.
- Tác vụ I/O bound: Đọc/ghi file, gọi network API, truy vấn database. Đây là những tác vụ mà CPU thường chờ đợi (chứ không phải tính toán liên tục), rất phù hợp để chạy bất đồng bộ.
- Tác vụ CPU bound (có thể song song hóa): Các phép tính toán phức tạp có thể chia nhỏ và chạy độc lập trên các nhân CPU khác nhau.
std::asyncvàstd::futurelà một cách đơn giản để làm điều này mà không cần quản lýstd::threadmột cách thủ công. - Khi bạn cần kết quả của một tác vụ ở một thời điểm sau: Bạn 'phát động' tác vụ, làm việc khác, và chỉ khi nào bạn thực sự cần kết quả thì mới 'đòi' nó qua
future.get().
Nhớ nhé các 'dev-er', std::future không phải là 'viên đạn bạc' giải quyết mọi vấn đề về concurrency, nhưng nó là một công cụ cực kỳ hữu ích và là bước khởi đầu tuyệt vời để làm quen với lập trình bất đồng bộ. 'Biết người biết ta, trăm trận trăm thắng', hiểu rõ future sẽ giúp bạn viết code 'mượt mà' và 'chất lượng' hơn rất nhiều đó!
Thuộc Series: C++
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!