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,我们需要三个核心组件:

  1. 共享状态:存放结果或错误信息,以及一个标志位表示是否就绪。
  2. 同步机制:用互斥锁保护共享状态,用条件变量通知等待者。
  3. 引用计数:管理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 模式核心架构 生产者线程 Promise 计算任务 设置结果/错误 共享状态 结果指针 就绪标志 互斥锁 + 条件变量 消费者线程 Future 获取结果 阻塞/非阻塞等待 写入 读取 核心流程: 1. 创建Future/Promise对,两者共享同一份状态 2. 生产者线程执行任务,完成后通过Promise设置结果 3. 消费者线程通过Future获取结果(可阻塞或非阻塞) 4. 引用计数管理生命周期,确保双方都释放后才销毁

Future/Promise模式的价值在于,它把异步编程的复杂度封装在了一个简洁的接口后面。你不需要关心线程同步的细节,只需要关注「什么时候设置结果」和「什么时候获取结果」这两个核心问题。

我个人觉得,掌握这个模式最大的收获不是会用某个API,而是建立起一种「异步思维」——不再把代码写成线性的流水账,而是学会用事件驱动的方式来组织逻辑。这种思维方式,在构建高性能、高并发的系统时,会给你带来巨大的帮助。


公众号:蓝海资料掘金营,微信deep3321