如何使 C 包装器 API 线程安全?

How to make a C wrapper API thread safe?

编辑:我们使用的是 C99。

首先,一些 setup/information 的上下文。我正在用 C 语言编写一个 API,它环绕着一个利用 IUF(Initialize()、Update()、Finalize())函数的 C 校验和 API。

我的 API 包含一个函数,我们称它为 foo(),它接受一个输入结构指针作为参数。它是以这样一种方式编写的,即可以在 blocks/chunks 而不是整个数据缓冲区中提供被散列的数据。此代码将 运行 在资源非常有限的嵌入式系统上,因此能够以块的形式读取文件以对数据进行哈希处理是此 API 的特定目的。使用单一功能不是我的选择,而是一个要求。

int foo(struct_t*);

而 struct_t 看起来像这样。

{
   char* data,
   int dataLen,
   bool finalBlock,
   char checksum[CHECKSUM_SIZE]
}

基本上,foo() 的调用者每次调用该函数时都会相应地用下一个数据块和该数据的大小填充 'data' 和 'dataLen' 参数,直到数据已经完全提供给 foo()。不幸的是,'finalBlock' 参数是告诉 foo() 您正在提供要处理的最后一块数据的唯一方法。这对于用例来说是可以理解的。

现在进入真正的问题。内部 IUF 校验和 API 有一个独特的数据结构,我不允许将其暴露给 foo() 的调用者。由于我们现在只在一个线程中使用 foo(),当前的解决方案是使 IUF API 的结构(我们称之为 bar)成为一个静态变量。这使得 foo() 函数不是线程安全的。

int foo(struct_t* x)
{
   /*Struct required by IUF API*/
   static bar y = {0};
   int retCode = 0;
   /*rest of code that processes data as needed*/
   ...
   /*after finalizing checksum and storing in x, reset y*/
   return retCode;
}

我希望尽可能使 foo() 线程安全,而不会将 'bar' 结构暴露给 foo() 的调用者。有什么建议吗?

TL;DR:API 有一个函数需要多次调用才能完成工作。必须向此 API 的调用者隐藏内部结构,但该结构必须持续到 API 完全完成。目前将该结构设置为静态变量,因此它会一直存在直到 API 完成,但这使得我的 API 不是线程安全的。请帮忙

有两种基本的构造方法:

  1. 一次授予一个线程对整个子系统的独占访问权限。您可以借助互斥锁和条件变量来实现这一点,如果您愿意,它们可以是 foo() 的静态成员。这对调用者是不可见的,但它会阻止所有并发,即使在多块情况下 foo() 调用之间也是如此。

  2. 给每个线程自己的数据。如果您不能适应 foo().

  3. 的非并发,这就是您想要的

即使无法访问 C11 的内置线程本地数据支持,您使用的任何线程 API 也可能具有 TLD 机制或实质上的等效机制。特别是 pthreads does,如果这恰好是您正在使用的。您甚至可以借助哈希 table.

自己滚动

但是由于您正在为资源受限的系统编写代码,因此您可能对轻量级方法感兴趣,而绝对最轻量级的方法是让每个线程提供自己的数据,方法是 struct_t a bar 为成员。这不仅回避了 访问 每线程数据所涉及的任何类型的间接寻址或查找,而且还可以避免任何动态内存分配。但这确实会将 bar 暴露给 foo 的调用者,你说你不能这样做。

在这些之间,您可以选择让 foo 分配 bars,然后将它们 不透明地 交给调用者,期望它们会在每个关联的调用中返回。这可能最适合您的需求,我将重点关注此答案的其余部分。

要实现这种方法,首先给struct_t一个额外的成员来存储上下文数据:

struct struct_t {
    char* data;
    int dataLen;
    bool finalBlock;
    char checksum[CHECKSUM_SIZE];
    void *state;  // <-- this
};

调用者应该将 state 设置为空指针以用于序列的初始调用(这就是 foo 可以识别的方式,否则我看不到规定),并在随后的调用中传回其中提供的任何值 foo。因此,自然调用模式看起来像这样:

    struct struct_t checksum_state = { 0 };
    int result;

    checksum_state.data = some_data;
    checksum_state.dataLen = the_length;
    result = foo(&checksum_state);
    // ... check / handle result ...

    checksum_state.data = more_data;
    checksum_state.dataLen = new_length;
    result = foo(&checksum_state);
    // ... check / handle result ...

    checksum_state.data = last_data;
    checksum_state.dataLen = last_length;
    checksum_state.finalBlock = 1;
    result = foo(&checksum_state);
    // ... check / handle result ...
    // ... use checksum_state.checksum ...

我认为这就是您已经拥有的使用模式,并且我注意到它根本不需要调用者明确使用或确认 checksum_state.state,尽管这部分是由调用者使用初始化器而不是每个成员初始化。

foo() 方面,它的结构如下:

int foo(struct_t* x) {
    int retCode = 0;
    struct bar *y;

    if (!x->state) {
        x->state = calloc(1, sizeof(struct bar));
        if (!x->state) // ... handle allocation error ...
    }
    y = x->state;

    /* Do stuff with x and y */

    if (x->lastBlock) {
        free(x->state);
    }

    return retCode;
}