【问题标题】:Making a function thread safe: Thread-specific data vs mutex使函数线程安全:线程特定数据与互斥锁
【发布时间】:2020-04-16 20:41:51
【问题描述】:

来自 Linux 编程接口,在 §31.3.4 Employing the Thread-Specific Data API 中,它给出了使用线程特定数据制作线程不安全函数 thead-safe:

头部不安全版本:

/* Listing 31-1 */

/* strerror.c

   An implementation of strerror() that is not thread-safe.
*/
#define _GNU_SOURCE                 /* Get '_sys_nerr' and '_sys_errlist'
                                       declarations from <stdio.h> */
#include <stdio.h>
#include <string.h>                 /* Get declaration of strerror() */

#define MAX_ERROR_LEN 256           /* Maximum length of string
                                       returned by strerror() */

static char buf[MAX_ERROR_LEN];     /* Statically allocated return buffer */

char *
strerror(int err)
{
    if (err < 0 || err >= _sys_nerr || _sys_errlist[err] == NULL) {
        snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
    } else {
        strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
        buf[MAX_ERROR_LEN - 1] = '\0';          /* Ensure null termination */
    }

    return buf;
}

具有线程特定数据的线程安全版本:

/* Listing 31-3 */

/* strerror_tsd.c

   An implementation of strerror() that is made thread-safe through
   the use of thread-specific data.

   See also strerror_tls.c.
*/
#define _GNU_SOURCE                 /* Get '_sys_nerr' and '_sys_errlist'
                                       declarations from <stdio.h> */
#include <stdio.h>
#include <string.h>                 /* Get declaration of strerror() */
#include <pthread.h>
#include "tlpi_hdr.h"

static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerrorKey;

#define MAX_ERROR_LEN 256           /* Maximum length of string in per-thread
                                       buffer returned by strerror() */

static void                         /* Free thread-specific data buffer */
destructor(void *buf)
{
    free(buf);
}

static void                         /* One-time key creation function */
createKey(void)
{
    int s;

    /* Allocate a unique thread-specific data key and save the address
       of the destructor for thread-specific data buffers */

    s = pthread_key_create(&strerrorKey, destructor);
    if (s != 0)
        errExitEN(s, "pthread_key_create");
}

char *
strerror(int err)
{
    int s;
    char *buf;

    /* Make first caller allocate key for thread-specific data */

    s = pthread_once(&once, createKey);
    if (s != 0)
        errExitEN(s, "pthread_once");

    buf = pthread_getspecific(strerrorKey);
    if (buf == NULL) {          /* If first call from this thread, allocate
                                   buffer for thread, and save its location */
        buf = malloc(MAX_ERROR_LEN);
        if (buf == NULL)
            errExit("malloc");

        s = pthread_setspecific(strerrorKey, buf);
        if (s != 0)
            errExitEN(s, "pthread_setspecific");
    }

    if (err < 0 || err >= _sys_nerr || _sys_errlist[err] == NULL) {
        snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
    } else {
        strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
        buf[MAX_ERROR_LEN - 1] = '\0';          /* Ensure null termination */
    }

    return buf;
}

在本章的总结部分它说:

...
SUSv3 中规定的大部分功能都需要 线程安全的。 SUSv3 还列出了一小组不属于 要求是线程安全的。通常,这些是采用 静态存储将信息返回给调用者或维护 连续呼叫之间的信息。 根据定义,此类函数 是不可重入的,并且不能使用互斥锁来制作它们 线程安全的。我们考虑了两个大致等效的编码 技术——特定于线程的数据和线程本地存储——可以 用于渲染不安全的函数线程安全而无需 改变它的界面。
...

我了解,使用特定于线程的数据旨在将线程不安全函数变为线程安全函数,不更改函数的接口/签名

但我不明白:

根据定义,这样的函数是不可重入的,不能使用互斥体来制作它们 线程安全的。

问题:

  1. 为什么说“不能使用互斥锁......而线程特定的数据可以......”?是否有任何条件可以使线程不安全函数仅使用特定于线程的数据而不使用互斥锁成为线程安全的?

  2. 我想我可以将线程不安全的strerror() 设置为线程安全的,只需添加一个互斥锁即可。与使用线程特定数据发布的相比,它有什么不同吗? (可能会损失一些并发效率?因为我要使用互斥锁来锁定访问静态变量的代码)

【问题讨论】:

  • strerror() 的“线程安全”版本通过返回一个指针来打破长期建立的 strerror() 接口,如果调用 strerror() 的线程退出,该指针将变为无效。跨度>

标签: c multithreading unix thread-safety mutex


【解决方案1】:

我想我可以将线程不安全的 strerror() 变成一个线程安全的 strerror(),只需添加一个互斥锁。

嗯,你错了,SUSv3 的作者是对的。

要了解为什么互斥锁不能使这些不可重入函数成为线程安全的,请考虑strerror 的原始(不安全)代码。

添加互斥锁可以使strerror 自身安全。

也就是说,我们可以避免在不同线程中并发调用strerror 之间的数据竞争。

这就是我认为您的想法:在开始时锁定互斥体,在结束时解锁,完成工作。很简单。

然而,它也是完全没有价值的——因为调用者永远不能安全地使用返回的缓冲区:它仍然与其他线程共享,并且互斥锁仅在对strerror 的调用内部保护它。

使函数安全有用(使用互斥体)的唯一方法是让调用者保持互斥体直到它完成使用缓冲区,这... . 需要更改界面。

【讨论】:

  • 你是对的! 1. 添加互斥锁只能避免不同线程中对strerror 的并发调用之间的数据竞争。 2. 关键是调用者不能安全地使用返回的缓冲区。
  • 3. 如果我要求调用者持有互斥锁,那么使函数安全的是用户的程序设计,但对于不安全的函数本身来说,它仍然不是线程-安全的。函数内没有任何变化。 (也可以通过更改接口来实现,因为您可以将函数接口更改为每次调用时都需要 mutex 参数。我认为这就是您的意思,最后是“需要更改接口”)。跨度>
  • 还有,想想你的回答,我明白了一些别的。使用互斥锁不会使 strerror 线程安全。但是使用互斥锁会使malloc 安全。为什么?我发现对于malloc,返回的缓冲区/指针是不同的/“动态”,所以它没有你提到的strerror 的问题。然后我更深入地理解了为什么“根据定义,这样的函数是不可重入的,并且不能使用互斥锁使它们成为线程安全的。” ;P.
【解决方案2】:

为什么说“不能使用互斥锁......而线程特定 数据可以....”?

互斥锁仅保护受互斥锁保护的区域内的共享数据。如果所有这些区域都由同一个互斥锁保护,那么一切都很好,但是考虑一个函数,例如strtok(),它在调用之间存储静态状态。可以通过使用互斥锁来保护该状态免受数据竞争,但是如果两个不同的线程尝试同时使用strtok,这并不能保护它们相互干扰——它们可能会在@987654323 中产生意外和不需要的更改@ 的内部状态,相对于其他线程的期望。这正是引入strtok_r()的原因。

或者考虑一个函数,例如ctime(),它返回一个指向静态数据的指针。两个线程不仅可以通过调用ctime 覆盖来破坏彼此的(共享)数据,而且它们甚至可以直接通过指针操作来修改它。

即使有一个互斥锁来保护这些数据并暴露给用户代码,库也不能确保所有用户线程都会通过适当地使用它来合作。更重要的是,使用这样的互斥体会产生瓶颈,而为此目的提供多个不同的互斥体会为死锁创造大量机会。

另一方面,线程特定的数据通过自动为每个线程维护单独的数据来解决此类问题。它不能保护线程免受自身的干扰,并且它可以通过跨线程泄漏线程特定数据指针的客户端代码来阻止,但它仍然提供互斥锁无法提供的安全性。此外,它不会造成瓶颈,也不会导致死锁。

有什么条件可以让线程不安全 函数线程安全仅与线程特定的数据,但不与 互斥体?

上面讨论的strtok()ctime() 函数的模拟可以使用线程本地存储而不是静态数据来编写。正确实现,这样的strtok_tsd() 函数将是完全线程安全的。这样的ctime_tsd() 函数也将是线程安全的,但受限于用户代码不得将指向其 TSD 区域的任何指针泄漏给另一个线程。

当然,另一方面,线程特定的数据完全不适合应该在线程之间共享的数据。这是每种方法最适合的制度之间的明显和自然的区别。特定于线程的数据提供了一种可变的静态数据的模拟,适合在多线程场景中使用,其中所涉及的数据是或可能与特定的计算系列相关联,因此不应在线程之间共享。

我想我可以将线程不安全的strerror() 设置为线程安全的, 只需添加一个互斥锁。

不。 strerror()ctime() 模具中的另一个功能。问题不在于strerror() 本身不安全,而在于多线程程序没有安全的方法来使用其结果。

与之前相比有什么不同吗? 使用线程特定数据发布了一个?

是的。返回(指向)特定于线程的数据允许调用线程安全地访问结果。尽管返回(指向)静态数据的指针,但在被调用函数中使用互斥锁。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-21
    • 1970-01-01
    相关资源
    最近更新 更多