ACE技术论文集-第5章 C/C++线程专有存储(Thread-Specific Storage)
发布: 2008-6-13 14:50 | 作者: Prashant Jain and D | 来源: 转载 | 查看: 585次
下面的代码演示在使用专有钥定长数组将TS Object“内部地”存储在线程中时,线程专有存储是怎样实现的。这个例子是从POSIX Pthreads[4]的一个公开可用的实现[3]改编而来。
下面所示的thread_state结构含有线程的状态:
struct thread_state
{
// The thread-specific error number.
int errno_;
// Thread-specific data values.
void *key_[_POSIX_THREAD_KEYS_MAX];
// ... Other thread state.
};
除了errno和线程专有存储指针的数组,该结构还包括了一个指向线程的栈和空间的指针;这些栈和空间用于存储在上下文切换过程中存储/恢复的数据(例如,程序计数器)。
对于一个特定的线程专有对象,所有线程用同一个专有钥值来设置或取回线程专有的值。例如,如果Logger对象正在被登记以跟踪线程专有的日志属性,线程专有的Logger代理将分配得到某个专有钥值N。所有线程都将使用这个值N来访问它们的线程专有日志对象。目前正在使用的专有钥的总数目可相对于所有线程全局地存储。如下所示:
typedef int pthread_key_t;
// All threads share the same key counter.
static pthread_key_t total_keys_ = 0;
每次有新的线程专有钥被请求时,total_keys_count都会自动增长,如下面的pthread_key_create函数所示:
// Create a new global key and specify
// a "destructor" function callback.
Int pthread_key_create (pthread_key_t *key,
void (*thread_exit_hook) (void *))
{
if (total_keys_ >= _POSIX_THREAD_KEYS_MAX)
{
// pthread_self() refers to the context of the
// currently active thread.
pthread_self ()->errno_ = ENOMEM;
return -1;
}
thread_exit_hook_[total_keys_] = thread_exit_hook;
*key = total_keys_++;
return 0;
}
pthread_key_create函数分配一个新的专有钥值,唯一地标识一个线程专有数据对象。此外,它还允许应用将一个thread_exit_hook与一个专有钥关联。该挂钩(hook)是一个函数指针,会被自动调用,当(1)线程退出时,以及(2)有线程专有对象登记专有钥时。指向“线程退出挂钩”的函数指针的数组可被全局地存储。如下所示:
// Exit hooks to cleanup thread-specific keys.
static void (*thread_exit_hook_[_POSIX_THREAD_KEYS_MAX]) (void);
下面的pthread_exit函数演示线程退出挂钩函数是怎样在pthread_exit的实现中被调用的:
// Terminate the thread and call thread exit hooks.
void pthread_exit (void *status)
{
// ...
for (i = 0; i < total_keys; i++)
if (pthread_self ()->key_[i] && thread_exit_hook_[i])
// Indirect pointer to function call.
(*thread_exit_hook_[i]) (pthread_self ()->key_[i]);
// ...
}
应用可为各个线程专有数据对象登记不同的函数,但是对于每个对象、会为每个线程调用同一个函数。登记动态分配的线程专有存储对象是一种常见的使用方法。因此,线程退出挂钩通常看起来是这样的:
static void cleanup_tss_Logger (void *ptr)
{
// This cast is necessary to invoke
// the destructor (if it exists).
delete (Logger *) ptr;
}
该函数释放一个动态分配的Logger对象。
pthread_setspecific函数为调用线程将value绑定到给定的key:
// Associate a value with a data key
// for the calling thread.
int pthread_setspecific (int key, void *value)
{
if (key < 0 || key >= total_keys) {
pthread_self ()->errno_ = EINVAL;
return -1;
}
pthread_self ()->key_[key] = value;
return 0;
}
同样地,pthread_getspecific为调用线程把绑定到给定Key的数据存储到value中:
// Retrieve a value from a data key
// for the calling thread.
int pthread_getspecific (int key, void **value)
{
if (key < 0 || key >= total_keys)
{
pthread_self ()->errno_ = EINVAL;
return -1;
}
*value = pthread_self ()->key_[key];
return 0;
}
因为数据存储在每个线程的内部状态中,这些函数不需要任何额外的锁来访问线程专有数据。
下面的例子演示怎样在可从多于一个线程调用的一个C函数中,使用来自POSIX Pthread规范的线程专有存储API,而又无须显式地调用初始化函数:
// Local to the implementation.
static pthread_mutex_t keylock = PTHREAD_MUTEX_INITIALIZER;
static pthread_key_t key;
static int once = 0;
void *func (void)
{
void *ptr = 0;
// Use the Double-Checked Locking pattern
// (described further below) to serialize
// key creation without forcing each access
// to be locked.
if (once == 0)
{
pthread_mutex_lock (&keylock);
if (once == 0)
{
// Register the free(3C) function
// to deallocation TSS memory when
// the thread goes out of scope.
pthread_key_create (&key, free);
once = 1;
}
pthread_mutex_unlock (&keylock);
}
pthread_getspecific (key, (void **) &ptr);
if (ptr == 0)
{
ptr = malloc (SIZE);
pthread_setspecific (key, ptr);
}
return ptr;
}
上面的解决方案直接在应用代码中调用线程专有库函数(比如pthread_getspecific和pthread_setspecific)。但是这些直接用C编写的API有以下局限:
- 不可移植:POSIX Pthreads、Solaris线程和Win32线程非常类似。但是,Win32线程的语义有着微妙的不同,因为它们不提供一种可*的方法来在线程退出时清理在线程专有存储中分配的对象。而且,在Solaris线程中没有API用于删除专有钥。这使得开发者难以在UNIX和Win32平台间编写可移植代码。
- 难以使用:尽管省略了错误检查,在5.8.2中的func例子所演示的锁定操作仍然是复杂而又不直观的。该代码是“双重检查锁定模式”(Double-Checked Locking pattern)[5]的C实现。将这个C实现与5.9.2.1中的C++版本相比较,以观察使用C++包装所带来的更多的简单性、清晰性和类型安全性是富有启发意义的。
- 非类型安全的:POSIX Pthreads、Solaris和Win32线程专有存储接口将指向线程专有对象的指针作为void *存储。尽管这种方法很灵活,它很容易造成错误,因为void *消除了类型安全性。
5.8演示了怎样通过POSIX pthread接口实现和使用线程专有存储模式。但是,所得到的解决方案不可移植、难以使用,且不是类型安全的。为了克服这些局限,可以开发另外的类和C++包装来健壮地以一种类型安全的方式编写线程专有存储。
这一部分演示怎样使用C++包装来封装POSIX Pthreads、Solaris或Win32线程提供的低级线程专有存储机制。5.9.1描述怎样通过硬编码的C++包装来封装POSIX Pthread库接口,5.9.2描述一种使用C++模板包装的更为通用的解决方案。用于每种可选方法的例子是5.6.2描述的Logger抽象的一个变种。
使一个类的所有实例成为线程专有的一种方法是直接使用线程专有库例程。实现该方法所需的步骤描述如下。错误检查已被省略到最少以节约空间。
第一步是决定必须在线程专有存储中存取的对象状态信息。例如,Logger可能有以下状态:
class Logger_State
{
public:
int errno_;
// Error number.
int line_num_;
// Line where the error occurred.
// ...
};
每个线程都将拥有它自己的一份这些状态信息的拷贝。
下一步是定义被所有应用线程使用的外部类接口。下面的Logger外部类接口看起来就像是一个平常的非线程专有的C++类:
class Logger
{
public:
// Set/get the error number.
int errno (void);
void errno (int);
// Set/get the line number.
int line_num (void);
void line_num (int);
// ...
};
该步骤使用线程库提供的线程专有存储函数来定义一个助手函数,它返回一个指向适当的线程专有存储的指针。这个助手函数通常执行以下步骤:
- 专有钥初始化:为每个线程专有对象初始化一个专有钥,并使用此专有钥来存/取一个指向动态分配内存的线程专有指针;此内存含有内部结构的实例。该代码可以被实现如下:
class Logger
{
public:
// ... Same as above ...
protected:
Logger_State *get_tss_state (void);
// Key for the thread-specific error data.
pthread_key_t key_;
// "First time in" flag.
int once_;
};
Logger_State *Logger::get_tss_state (void)
{
// Check to see if this is the first time in
// and if so, allocate the key (this code
// doesn’t protect against multi-threaded
// race conditions...).
if (once_ == 0)
{
pthread_key_create (this->key_, free);
once_ = 1;
}
Logger_State *state_ptr;
// Get the state data from thread-specific
// storage. Note that no locks are required...
pthread_getspecific (this->key_, (void **) &state_ptr);
if (state_ptr == 0)
{
state_ptr = new Logger_State;
pthread_setspecific (this->key_, (void *) state_ptr);
}
// Return the pointer to thread-specific storage.
return state_ptr;
};
- 获取指向线程专有存储对象的指针:外部接口中的每个方法都调用get_tss_state助手函数来获取指向驻留在线程专有存储中的Logger_State对象的指针。如下所示:
int Logger::errno (void)
{
return this->get_tss_state ()->errno_;
}
- 执行正常操作:一旦外部接口方法有了该指针,应用就可以对线程专有对象进行操作,就如同它是平常的(也就是,非线程专有的)C++对象:
Logger logger;
int recv_msg (HANDLE socket, char *buffer, size_t bufsiz)
{
if (recv (socket, buffer, bufsiz, 0) == -1)
{
logger->errno () = errno;
return -1;
}
// ...
}
int main (void)
{
// ...
if (recv_msg (socket, buffer, BUFSIZ) == -1
&& logger->errno () == EWOULDBLOCK)
// ...
}
使用硬编码包装的优点是它将应用与线程专有库函数的知识屏蔽开来。该方法的缺点是它不能促进复用性、可移植性,或是灵活性。特别地,对于每个线程专有类,开发者都需要在类中重新实现线程专有助手方法。
而且,如果应用被移植到有着不同的线程专有存储API的平台上,就必须改变在每个线程专有类中的代码,以使用新的线程库。此外,直接改变线程专有类使得程序难以变更线程策略。例如,将一个线程专有类变为全局类需要对代码进行侵入性的变动,从而降低了灵活性和复用性。特别地,每次对对象内部的状态信息的访问都将要求改变用于从线程专有存储取回该状态的助手方法。
一种更为可复用、可移植和灵活的方法是实现TS Object Proxy模板,负责所有的线程专有方法。该方法允许类与线程专有存储怎样实现的知识去耦合。通过定义称为TSS的代理类,该解决方案改善了代码的可复用性、可移植性和灵活性。如下所示,该类是一个模板,通过其对象驻留在线程专有存储中的类来参数化:
// TS Proxy template
template
class TSS
{
public:
// Constructor.
TSS (void);
// Destructor
?TSS (void);
// Use the C++ "smart pointer" operator to
// access the thread-specific TYPE object.
TYPE *operator-> ();
private:
// Key for the thread-specific error data.
pthread_key_t key_;
// "First time in" flag.
int once_;
// Avoid race conditions during initialization.
Thread_Mutex keylock_;
// Cleanup hook that deletes dynamically
// allocated memory.
static void cleanup_hook (void *ptr);
};
该类中的方法描述如下。和前面一样,错误检查已被省略到最少以节约空间。
