确保函数线程安全:线程专用数据与互斥锁

时间:2019-12-26 16:55:06

标签: c multithreading unix thread-safety mutex

Linux Progamming接口(在§31.3.4中使用线程专用数据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()成为thead安全的线程,只需添加一个互斥体即可。与使用线程特定数据发布的数据相比,它有什么区别吗? (也许会失去一些并发效率?因为我要使用互斥锁来锁定访问静态变量的代码)

2 个答案:

答案 0 :(得分:5)

  

我认为我可以使线程不安全的strerror()成为安全于线程的strerror(),只需添加互斥量即可。

好吧,你错了,SUSv3的作者是正确的。

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

添加互斥量可以使strerror本身安全。

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

这是我认为您要想到的:在开始时锁定互斥锁,在结束时解锁,完成工作。容易。

但是,它也是完全一文不值的-因为调用者永远无法安全地使用返回的缓冲区:该缓冲区仍与其他线程共享,并且互斥锁仅在对{{1}的调用内 }。

使函数安全且有用的唯一方法(使用互斥锁)是调用方保留互斥锁,直到完成使用缓存器 为止。 。需要更改接口。

答案 1 :(得分:1)

  

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

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

或者考虑使用诸如ctime()之类的函数,该函数返回指向静态数据的指针。两个线程不仅可以通过调用ctime来覆盖彼此的(共享)数据,而且甚至可以通过指针操作直接 对其进行修改。

即使存在保护此类数据并暴露给用户代码的互斥量,该库也无法确保所有用户线程都将通过适当地使用它来进行协作。而且,使用这样的互斥锁会产生瓶颈,而为此目的提供多个不同的互斥锁将为死锁创造大量机会。

另一方面,

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

  

有什么条件可以使线程不安全   仅对特定于线程的数据具有线程安全功能,而对不具有   互斥锁?

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

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

  

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

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

  

与   使用线程专用数据发布了一个?

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