使函数线程安全:线程特定数据与互斥锁
Making a function thread safe: Thread-specific data vs mutex
来自Linux编程接口,在§31.3.4 使用线程特定数据API,它给出了一个使用线程特定数据使线程不安全函数成为线程安全函数的好例子:
不安全版本:
/* 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] = '[=10=]'; /* 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] = '[=11=]'; /* Ensure null termination */
}
return buf;
}
在本章的总结部分,它说:
...
Most of the functions specified in SUSv3 are required to be
thread-safe. SUSv3 also lists a small set of functions that are not
required to be thread-safe. Typically, these are functions that employ
static storage to return information to the caller or to maintain
information between successive calls. By definition, such functions
are not reentrant, and mutexes can’t be used to make them
thread-safe. We considered two roughly equivalent coding
techniques—thread-specific data and thread-local storage—that can be
used to render an unsafe function thread-safe without needing to
change its interface.
...
我了解使用线程特定数据旨在将线程不安全函数变为线程安全函数,无需更改函数的 interface/signature。
但我不明白:
By definition, such functions are not reentrant, and mutexes can’t be used to make them
thread-safe.
问题:
为什么说"mutexes can't be use... while thread-specific data can...."?是否有任何条件可以使线程不安全的函数仅使用特定于线程的数据而不是互斥锁来实现线程安全?
我想我可以把线程不安全的 strerror()
变成线程安全的,只需添加一个互斥锁。与使用 thread-specifia 数据发布的相比,它有什么区别吗? (可能会损失一些并发效率?因为我要使用互斥锁来锁定访问静态变量的代码)
Why is it saying that "mutexes can't be use... while thread-specific
data can...."?
互斥锁仅在受互斥锁保护的区域内保护共享数据。如果所有这些区域都由同一个互斥锁保护,那么一切都很好,但考虑一个函数,例如 strtok()
,它在调用之间存储静态状态。该状态可以通过使用互斥锁来防止数据竞争,但是如果两个不同的线程试图同时使用 strtok
则不能保护它们相互干扰——它们可能会产生意想不到的和不需要的变化strtok
的内部状态,相对于其他线程的期望。这正是引入 strtok_r()
的原因。
或者考虑一个函数,例如 ctime()
,它 returns 是一个指向静态数据的指针。两个线程不仅可以通过调用 ctime
覆盖彼此的(共享)数据来破坏它,而且它们甚至可以 直接 通过指针操作来修改它。
即使存在保护此类数据并暴露给用户代码的互斥锁,该库也无法确保所有用户线程都会通过适当地使用它来进行协作。更重要的是,使用这样的互斥量会产生瓶颈,为此目的提供多个不同的互斥量会产生大量死锁机会。
另一方面,线程特定数据通过自动为每个线程维护单独的数据来解决此类问题。它不会保护线程免受自身干扰,并且可以通过跨线程泄漏特定于线程的数据指针的客户端代码来阻止它,但它仍然提供了互斥量所不能提供的安全性。此外,它不会造成瓶颈,也不会导致死锁。
Is there any conditions that I can make a thread-unsafe
function thread-safe only with thread-specific data but not with
mutex?
上面讨论的 strtok()
和 ctime()
函数的模拟可以使用线程本地存储而不是静态数据来编写。如果实现正确,这样的 strtok_tsd()
函数将是完全线程安全的。这样的 ctime_tsd()
函数也将是线程安全的,但受到用户代码不得将任何指向其 TSD 区域的指针泄露给另一个线程的限制。
当然,另一方面是线程特定数据完全不适合应该在线程之间共享的数据。这是每种方法最适合的制度之间明显而自然的区别。特定于线程的数据提供了一种可变的静态数据模拟,适合在多线程场景中使用,其中涉及的数据是或可能绑定到特定的一系列计算,因此不应在线程之间共享。
I think I can make the thread-unsafe strerror()
to a thead-safe one,
simply adding a mutex.
没有。 strerror()
是 ctime()
模型中的另一个函数。问题不在于 strerror()
本身不安全,而在于多线程程序没有安全的方法来使用其结果。
Does it make any difference compared to the
posted one using thread-specifia data?
是的。返回(指向)线程特定数据允许调用线程安全地访问结果。返回(指向)静态数据不会,尽管在被调用函数中使用互斥锁。
I think I can make the thread-unsafe strerror() to a thead-safe one, simply adding a mutex.
嗯,你错了,SUSv3 的作者是对的。
要了解 为什么 互斥锁不能使这些不可重入函数成为线程安全的,请考虑 strerror
.
的原始(不安全)代码
添加互斥量可以使 strerror
自身安全。
也就是说,我们可以避免不同线程中对 strerror
的并发调用之间的数据竞争。
这就是我认为您的想法:在开始时锁定互斥体,在结束时解锁,工作完成。简单。
然而,它也是完全没有价值的 - 因为调用者永远无法安全地使用返回的缓冲区:它仍然与其他线程共享并且互斥体只保护它 在调用中至 strerror
.
使函数安全和有用(使用互斥锁)的唯一方法是让调用者持有互斥锁直到它完成使用 缓冲区,...需要更改接口。
来自Linux编程接口,在§31.3.4 使用线程特定数据API,它给出了一个使用线程特定数据使线程不安全函数成为线程安全函数的好例子:
不安全版本:
/* 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] = '[=10=]'; /* 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] = '[=11=]'; /* Ensure null termination */
}
return buf;
}
在本章的总结部分,它说:
...
Most of the functions specified in SUSv3 are required to be thread-safe. SUSv3 also lists a small set of functions that are not required to be thread-safe. Typically, these are functions that employ static storage to return information to the caller or to maintain information between successive calls. By definition, such functions are not reentrant, and mutexes can’t be used to make them thread-safe. We considered two roughly equivalent coding techniques—thread-specific data and thread-local storage—that can be used to render an unsafe function thread-safe without needing to change its interface.
...
我了解使用线程特定数据旨在将线程不安全函数变为线程安全函数,无需更改函数的 interface/signature。
但我不明白:
By definition, such functions are not reentrant, and mutexes can’t be used to make them thread-safe.
问题:
为什么说"mutexes can't be use... while thread-specific data can...."?是否有任何条件可以使线程不安全的函数仅使用特定于线程的数据而不是互斥锁来实现线程安全?
我想我可以把线程不安全的
strerror()
变成线程安全的,只需添加一个互斥锁。与使用 thread-specifia 数据发布的相比,它有什么区别吗? (可能会损失一些并发效率?因为我要使用互斥锁来锁定访问静态变量的代码)
Why is it saying that "mutexes can't be use... while thread-specific data can...."?
互斥锁仅在受互斥锁保护的区域内保护共享数据。如果所有这些区域都由同一个互斥锁保护,那么一切都很好,但考虑一个函数,例如 strtok()
,它在调用之间存储静态状态。该状态可以通过使用互斥锁来防止数据竞争,但是如果两个不同的线程试图同时使用 strtok
则不能保护它们相互干扰——它们可能会产生意想不到的和不需要的变化strtok
的内部状态,相对于其他线程的期望。这正是引入 strtok_r()
的原因。
或者考虑一个函数,例如 ctime()
,它 returns 是一个指向静态数据的指针。两个线程不仅可以通过调用 ctime
覆盖彼此的(共享)数据来破坏它,而且它们甚至可以 直接 通过指针操作来修改它。
即使存在保护此类数据并暴露给用户代码的互斥锁,该库也无法确保所有用户线程都会通过适当地使用它来进行协作。更重要的是,使用这样的互斥量会产生瓶颈,为此目的提供多个不同的互斥量会产生大量死锁机会。
另一方面,线程特定数据通过自动为每个线程维护单独的数据来解决此类问题。它不会保护线程免受自身干扰,并且可以通过跨线程泄漏特定于线程的数据指针的客户端代码来阻止它,但它仍然提供了互斥量所不能提供的安全性。此外,它不会造成瓶颈,也不会导致死锁。
Is there any conditions that I can make a thread-unsafe function thread-safe only with thread-specific data but not with mutex?
上面讨论的 strtok()
和 ctime()
函数的模拟可以使用线程本地存储而不是静态数据来编写。如果实现正确,这样的 strtok_tsd()
函数将是完全线程安全的。这样的 ctime_tsd()
函数也将是线程安全的,但受到用户代码不得将任何指向其 TSD 区域的指针泄露给另一个线程的限制。
当然,另一方面是线程特定数据完全不适合应该在线程之间共享的数据。这是每种方法最适合的制度之间明显而自然的区别。特定于线程的数据提供了一种可变的静态数据模拟,适合在多线程场景中使用,其中涉及的数据是或可能绑定到特定的一系列计算,因此不应在线程之间共享。
I think I can make the thread-unsafe
strerror()
to a thead-safe one, simply adding a mutex.
没有。 strerror()
是 ctime()
模型中的另一个函数。问题不在于 strerror()
本身不安全,而在于多线程程序没有安全的方法来使用其结果。
Does it make any difference compared to the posted one using thread-specifia data?
是的。返回(指向)线程特定数据允许调用线程安全地访问结果。返回(指向)静态数据不会,尽管在被调用函数中使用互斥锁。
I think I can make the thread-unsafe strerror() to a thead-safe one, simply adding a mutex.
嗯,你错了,SUSv3 的作者是对的。
要了解 为什么 互斥锁不能使这些不可重入函数成为线程安全的,请考虑 strerror
.
添加互斥量可以使 strerror
自身安全。
也就是说,我们可以避免不同线程中对 strerror
的并发调用之间的数据竞争。
这就是我认为您的想法:在开始时锁定互斥体,在结束时解锁,工作完成。简单。
然而,它也是完全没有价值的 - 因为调用者永远无法安全地使用返回的缓冲区:它仍然与其他线程共享并且互斥体只保护它 在调用中至 strerror
.
使函数安全和有用(使用互斥锁)的唯一方法是让调用者持有互斥锁直到它完成使用 缓冲区,...需要更改接口。