多线程程序之线程私有数据

在C/C++的多线程程序中,所有的线程共享相同的地址空间。所以数据段和进程堆的数据,都可以被所有线程读写。在极端情况下,线程的栈空间也可以被共享。只有线程的寄存器上下文算是真正私有的。对于某些变量,我们希望它在不同的线程中具有不同的值,这就是线程私有数据的用途。

对于普通的全局变量,或者通过指针或引用实现的共享,其实在内存中只存在一个备份。而进程私有数据则相当于一种Map型的变量,根据线程的id,线程私有变量对应于不同的值。

/*
 * tsd_once.c
 *
 * Demonstrate use of pthread_once to initialize something
 * exactly once within a multithreaded program.
 *
 * Note that it is often easier to use a statically initialized
 * mutex to accomplish the same result.
 */
#include <pthread.h>
#include "errors.h"

/*
 * 线程私有变量的类型.
 */
typedef struct tsd_tag {
    pthread_t   thread_id;
    char        *string;
} tsd_t;

pthread_key_t tsd_key;           /* 线程私有变量的句柄,所有线程共用一个*/
pthread_once_t key_once = PTHREAD_ONCE_INIT;

/*
 * One-time initialization routine used with the pthread_once
 * control block.
 */
void once_routine (void)
{
    int status;

    printf ("initializing key\n");
    status = pthread_key_create (&tsd_key, NULL);
    if (status != 0)
        err_abort (status, "Create key");
}

/*
 * Thread start routine that uses pthread_once to dynamically
 * create a thread-specific data key.
 */
void *thread_routine (void *arg)
{
    tsd_t *value;
    int status;

    status = pthread_once (&key_once, once_routine);
    if (status != 0)
        err_abort (status, "Once init");
    value = (tsd_t*)malloc (sizeof (tsd_t));
    if (value == NULL)
        errno_abort ("Allocate key value");
    /*对象线程私有变量的改写*/
    status = pthread_setspecific (tsd_key, value);
    if (status != 0)
        err_abort (status, "Set tsd");
    printf ("%s set tsd value %p\n", arg, value);
    value->thread_id = pthread_self ();
    value->string = (char*)arg;
    /*对象线程私有变量的读取*/ 
    value = (tsd_t*)pthread_getspecific (tsd_key);
    printf ("%s starting...\n", value->string);
    sleep (2);
    value = (tsd_t*)pthread_getspecific (tsd_key);
    printf ("%s done...\n", value->string);
    return NULL;    
}

void main (int argc, char *argv[])
{
    pthread_t thread1, thread2;
    int status;

    status = pthread_create (
        &thread1, NULL, thread_routine, "thread 1");
    if (status != 0)
        err_abort (status, "Create thread 1");
    status = pthread_create (
        &thread2, NULL, thread_routine, "thread 2");
    if (status != 0)
        err_abort (status, "Create thread 2");
    pthread_exit (NULL);
}

在这个程序中,两个线程以使用相同的线程私有数据句柄去访问线程私有数据(TSD),但他们之间是相互不影响的,这便是TSD所能实现的效果。最近在看Java并发相关的树,发现Java里也有相应的数据结构,具体是在《Java多线程设计模式》的第11章。这里给出示例代码中的比较重要的部分,首先是日志功能类Log。

package tsLog;

public class Log {
    private static final ThreadLocal<TSLog> tsLogCollection = new ThreadLocal<TSLog>();
    
    public static void println(String s) {
        getTSLog().println(s);
    }
    
    public static void close() {
        getTSLog().close();
    }
    
    private static TSLog getTSLog() {
        TSLog tsLog = (TSLog)tsLogCollection.get();
        
        if(tsLog == null) {
            tsLog = new TSLog(Thread.currentThread().getName() + "-log.txt");
            tsLogCollection.set(tsLog);
        }
        
        return tsLog;
    }
}

其次是用户线程类的实现:

package tsLog;

public class ClientThread extends Thread{
    public ClientThread(String name) {
        super(name);
    }
    
    public void run() {
        System.out.println(getName() + " BEGIN");
        for(int i=0; i<10; i++) {
            Log.println("i = " + i);
            try {
                Thread.sleep(100);
            } catch(InterruptedException e) {
                
            }
        }
        Log.close();
        System.out.println(getName() + " END");
    }
}

其中Log类的静态代码段会在类加载的时候完成,所以对于整个多线程程序来说,tsLogCollection是唯一且不变的。但是tsLogCollection这个句柄为每个线程维护着一个tsLog示例。所以工作线程在打log的时候不需要把自己的上下文信息传递(即进程id、对应日志文件等)传递给Log。

此外,还有几个小知识点需要记录下:

  • 在较新Java版本中,ThreadLocal已经成为泛型类型,实例化最给出线程私有变量的类型
  • 由于线程私有变量是专有的,所以不存在同步问题,不用实现访问

发表评论