15、Future与Promise:Future/Promise模式、C语言中的实现、异步结果获取
聊到并发编程,大家最先想到的往往是锁、条件变量、线程池这些底层工具。但说实话,真正让并发编程变得优雅的,是那些更高层的抽象模式。今天要讲的Future与Promise,就是其中之一。
我最早接触这个模式是在写C++的异步网络库时,后来在Go语言的goroutine和channel里也看到了类似的思想。回到C语言的世界,虽然标准库没有直接提供,但自己动手实现一套,反而能让你把并发编程的理解提升一个台阶。
什么是Future/Promise模式?
说白了,Future/Promise就是一套「生产者-消费者」的异步通信协议。它解决了一个很实际的问题:你发起一个异步操作,但结果还没准备好,怎么办?
举个例子。你点了一份外卖,但外卖还没到。你不会傻等在门口吧?你会继续做自己的事。等外卖到了,骑手打电话通知你,你再去拿。这就是Future/Promise的核心理念。
- Promise:负责生产结果的一方。它承诺「将来某个时刻,我会把结果给你」。
- Future:负责消费结果的一方。它代表「一个尚未就绪的值」,你可以通过它来获取最终结果。
我习惯把Promise看作「写端」,Future看作「读端」。它们通过一个共享的存储区来传递数据,配合同步机制保证线程安全。
核心思想:Future/Promise将「发起异步操作」和「获取结果」这两个动作解耦。你不需要阻塞等待,而是得到一个「凭证」,后续再凭这个凭证去取结果。
C语言中的实现思路
在C语言里实现Future/Promise,我们需要三个核心组件:
- 共享状态:存放结果或错误信息,以及一个标志位表示是否就绪。
- 同步机制:用互斥锁保护共享状态,用条件变量通知等待者。
- 引用计数:管理Future和Promise的生命周期,防止悬空指针。
嗯,这里要注意:C语言没有RAII,所以内存管理得格外小心。我曾经在一个项目中因为忘记释放Promise对象,导致内存泄漏,排查了整整一个下午。
代码实现
先定义数据结构:
// future_promise.h
#ifndef FUTURE_PROMISE_H
#define FUTURE_PROMISE_H
#include <pthread.h>
#include <stdint.h>
typedef struct future_promise_t {
void *result; // 结果指针
int is_ready; // 是否就绪
int has_error; // 是否有错误
pthread_mutex_t mutex; // 互斥锁
pthread_cond_t cond; // 条件变量
int ref_count; // 引用计数
} future_promise_t;
// 创建一对Future和Promise
int future_promise_create(future_promise_t **future,
future_promise_t **promise);
// Promise端:设置结果
void promise_set_value(future_promise_t *promise, void *value);
// Promise端:设置错误
void promise_set_error(future_promise_t *promise, int error_code);
// Future端:获取结果(阻塞等待)
void *future_get(future_promise_t *future);
// Future端:非阻塞检查
int future_is_ready(future_promise_t *future);
// 释放资源
void future_promise_destroy(future_promise_t *fp);
#endif
实现文件:
// future_promise.c
#include "future_promise.h"
#include <stdlib.h>
int future_promise_create(future_promise_t **future,
future_promise_t **promise) {
future_promise_t *fp = malloc(sizeof(future_promise_t));
if (!fp) return -1;
fp->result = NULL;
fp->is_ready = 0;
fp->has_error = 0;
fp->ref_count = 2; // Future和Promise各占一个引用
pthread_mutex_init(&fp->mutex, NULL);
pthread_cond_init(&fp->cond, NULL);
*future = fp;
*promise = fp;
return 0;
}
void promise_set_value(future_promise_t *promise, void *value) {
pthread_mutex_lock(&promise->mutex);
promise->result = value;
promise->is_ready = 1;
promise->has_error = 0;
// 唤醒所有等待的Future
pthread_cond_broadcast(&promise->cond);
pthread_mutex_unlock(&promise->mutex);
}
void *future_get(future_promise_t *future) {
pthread_mutex_lock(&future->mutex);
while (!future->is_ready) {
// 等待条件变量通知
pthread_cond_wait(&future->cond, &future->mutex);
}
void *result = future->result;
pthread_mutex_unlock(&future->mutex);
return result;
}
小技巧:使用pthread_cond_broadcast而不是pthread_cond_signal,是因为可能有多个线程在等待同一个Future的结果。广播能确保所有等待者都被唤醒。
使用示例
来看一个实际的使用场景:异步计算斐波那契数列。
#include <stdio.h>
#include <pthread.h>
#include "future_promise.h"
typedef struct {
int n;
future_promise_t *promise;
} task_t;
void *compute_fib(void *arg) {
task_t *task = (task_t *)arg;
int n = task->n;
future_promise_t *promise = task->promise;
// 计算斐波那契数
long result = 0, a = 0, b = 1;
for (int i = 0; i < n; i++) {
result = a + b;
a = b;
b = result;
}
// 将结果放入Promise
long *res_ptr = malloc(sizeof(long));
*res_ptr = result;
promise_set_value(promise, res_ptr);
free(task);
return NULL;
}
int main() {
future_promise_t *future, *promise;
future_promise_create(&future, &promise);
task_t *task = malloc(sizeof(task_t));
task->n = 10;
task->promise = promise;
pthread_t thread;
pthread_create(&thread, NULL, compute_fib, task);
pthread_detach(thread);
// 主线程继续做其他事...
printf("正在计算中,我先干点别的...\n");
// 需要结果时,通过Future获取
long *result = (long *)future_get(future);
printf("第10个斐波那契数是: %ld\n", *result);
free(result);
future_promise_destroy(future);
return 0;
}
Future/Promise的典型应用场景
| 场景 | 说明 |
|---|---|
| RPC调用 | 发起远程调用后立即返回Future,后续获取结果 |
| 异步I/O | 读写操作完成后通过Promise通知调用方 |
| 任务编排 | 多个异步任务组合,等待所有Future完成 |
| 超时控制 | 在Future上设置超时,避免无限等待 |
避坑指南
我曾经踩过的坑:
- 重复设置:Promise只能设置一次结果。如果多次调用
promise_set_value,后一次会覆盖前一次,而且可能造成资源泄漏。建议在设置时加一个断言检查。 - 内存所有权:谁负责释放结果内存?我建议约定:Promise端分配内存,Future端负责释放。或者使用引用计数来管理。
- 死锁风险:如果在持有Future锁的情况下调用
future_get,而Promise端也在等待同一个锁,就会死锁。记住:不要在持锁时等待Future。
进阶:带超时的Future
实际项目中,无限等待往往不可接受。我们可以给Future加上超时机制:
#include <sys/time.h>
int future_get_timed(future_promise_t *future, void **result,
int timeout_ms) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout_ms / 1000;
ts.tv_nsec += (timeout_ms % 1000) * 1000000;
pthread_mutex_lock(&future->mutex);
while (!future->is_ready) {
int ret = pthread_cond_timedwait(&future->cond,
&future->mutex, &ts);
if (ret == ETIMEDOUT) {
pthread_mutex_unlock(&future->mutex);
return -1; // 超时
}
}
*result = future->result;
pthread_mutex_unlock(&future->mutex);
return 0;
}
建议:在工业级项目中,我通常还会加入「取消」功能。当Future被取消时,Promise端可以提前终止计算,避免浪费CPU资源。这需要额外的标志位和协作机制。
知识体系总览
下面这张图展示了Future/Promise模式的核心逻辑和组件关系:
Future/Promise模式的价值在于,它把异步编程的复杂度封装在了一个简洁的接口后面。你不需要关心线程同步的细节,只需要关注「什么时候设置结果」和「什么时候获取结果」这两个核心问题。
我个人觉得,掌握这个模式最大的收获不是会用某个API,而是建立起一种「异步思维」——不再把代码写成线性的流水账,而是学会用事件驱动的方式来组织逻辑。这种思维方式,在构建高性能、高并发的系统时,会给你带来巨大的帮助。