第十二章 线程控制
一、线程限制
与其它的系统限制一样,这些线程也可以通过sysconf函数进行查询。
与sysconf报告的其它限制一样,这些限制的使用是为了增强应用程序在不同的操作系统实现之间的可移植性。
二、线程属性
实例:以分离状态创建的线程
#include "apue.h"
#include
int makethread(void *(*fn), void *arg)
{
int err;
pthread_t tid;
pthread_attr_t attr;
err = pthread_attr_init(attr);
if (err != 0)
{
return(err);
}
err = pthread_attr_setdatachstate(&attr, PTHREAD_CREATE_DETACHED);
if (err == 0)
{
err = pthread_create(&tid, &attr, fn, arg);
}
pthread_attr_destroy(&attr);
return err;
}
更多的线程属性:
(1)可取消状态
(2)可取消类型
(3)并发度
并发度控制着用户级线程可以映射的内核线程或进程的数目。如果操作系统的实现在内核级的线程和用户级的线程之间保持一对一的映射,那么改变并发度并不会有什么效果,因为所有的用户级的线程都可能被调度到。
三、同步属性
就像线程具有属性一样,线程的同步对象也有属性。
1.互斥量属性
用pthread_mutexattr_init初始化pthread_mutexattr_t结构,用pthread_mutex_destroy来对该结构进行回收。
实例:使用递归互斥量
#include "apue.h"
#include
#include
#include
extern int makethread(void *(*)(void *), void *);
struct to_info
{
void (*to_fn)(void *);
void *to_arg;
struct timespec to_wait;
};
#define SECTONSEC 1000000000
#define USECTONSEC 1000
void *
timeout_helper(void *arg)
{
struct to_info *tip;
tip = (struct to_info *)arg;
nanosleep(&tip->to_wait, NULL);
(*tip->to_fn)(tip->to_arg);
return 0;
}
void
timeout(const struct timespec *when, void (*func)(void *), void *arg)
{
struct timespec now;
struct timeval tv;
struct to_info *tip;
int err;
gettimeofday(&tv, NULL);
now.tv_sec = tv.tv_sec;
now.tv_nsec = tv.tv_usec * USECTONSEC;
if ((when->tv_sec > now.tv_sec) || (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec))
{
tip = malloc(sizeof(struct to_info));
if (tip != NULL)
{
tip->to_fn = func;
tip->to_arg = arg;
tip->to_wait.tv_sec = when->tv_nsec - now.tv_nsec;
if (when->tv_nsec >= now.tv_nsec)
{
tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
}
else
{
tip->to_wait.tv_sec--;
tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec;
}
err = makethread(timeout_helper, (void *)tip);
if (err == 0)
{
return;
}
}
}
(*func)(arg);
}
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
void
retry(void *arg)
{
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
}
int
main(void)
{
int err, condition,arg;
struct timespec when;
if ((err = pthread_mutexattr_init(&attr)) != 0)
{
err_exit(err, "pthread_mutexattr_init failed");
}
if ((err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) != 0)
{
err_exit(err, "can't set recursive type");
}
if ((err = pthread_mutex_init(&mutex, &attr)) != 0)
{
err_exit(err, "can't create recursive mutex");
}
pthread_mutex_lock(&mutex);
if (condition)
{
timeout(&when, retry, (void *)arg);
}
pthread_mutex_unlock(&mutex);
exit(0);
}
int makethread(void *(*fn)(void *), void *arg)
{
int err;
pthread_t tid;
pthread_attr_t attr;
err = pthread_attr_init(&attr);
if (err != 0)
{
return(err);
}
err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (err == 0)
{
err = pthread_create(&tid, &attr, fn, arg);
}
pthread_attr_destroy(&attr);
return err;
}
2.读写锁属性
读写锁与互斥量类似,也有属性。用pthread_rwlockattr_init初始化pthread_rwlockattr_t结构,用
pthread_rwlockattr_tdestroy回收结构。
读写锁支持的唯一属性是进程共享属性,该属性与互斥量的进程共享属性相同。就像互斥量的进程共享属 性一样,用一对函数读取和设置读写锁的进程共享属性。
3.条件变量属性
条件变量也有属性。与互斥量和读写锁类似,有一对函数用于初始化和回收条件变量属性。
四、重入
如果一个函数在同一时刻可以被多个线程安全的调用,就称该函数是线程安全的。支持线程安全函数的操作系统会在
如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。
实例:getenv的非可重入版本
#include
#include
static char envbuf[ARG_MAX];
extern char **environ
char *
getenv(const char *name)
{
int i,len;
len = strlen(name);
for (i = 0; envrion[i] != NULL; i++)
{
if ((strcmp(name, environ[i], len) == 0) && (environ[i][len] == '='))
{
strcpy(envbuf, &environ[i][len + 1]);
return (envbuf);
}
}
return NULL;
}
实例:getenv的可重入(线程安全)版本
#include
#include
#include
#include
extern char **environ;
pthread_mutex_t env_mutex;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;
static void
thread_init(void)
{
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&env_mutex, &attr);
}
int
getenv_r(const char *name, char *buf, int buflen)
{
int i, len, olen;
pthread_once(&init_done, thread_init);
len = strlen(name);
pthread_mutex_lock(&env_mutex);
for (i = 0; environ[i] != NULL; i++)
{
if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '='))
{
olen = strtlen(&environ[i][len + 1]);
if (len >= buflen)
{
pthread_mutex_unlock(&enc_mutex);
return (ENOSPC);
}
strcpy(buf, &environ[i][len + 1]);
pthread_mutex_unlock(&env_mutex);
return (0);
}
}
pthread_mutex_unlock(&env_mutex);
return (ENOENT);
}
五、线程私有数据
线程私有程序(也称线程特定数据)是存储和查询与某个线程相关的数据的一种机制。把这种数据称为线程私有数据或线程特定数据的原因是,希望每个线程可以独立的访问数据副本,而不需要担心与其他线程的同步访问问题。
在分配线程私有数据之前,需要创建与该数据关联的键,这个键将用于获取对线程私有数据的访问权。使用pthread_key_create创建一个键:
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
创建的键存放在keyp指向的内存单元,这个键可以被进程中所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联。创建新键时,每个线程的数据地址设为null值。
线程通常使用malloc为线程私有数据分配内存空间,析构函数通常释放已分配的内存。如果线程没有释放内存就退出了,那么这块内存就会丢失,即线程所属进程出现了内存泄漏。
有些线程可能看到某个键值,而其它的线程看到的可能是另一个不同的键值,这取决于系统是如何调度线程的,解决这种竞争的办法是使用pthread_once。
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
initflag必须是一个非本地变量(即全局变量或静态变量),而且必须初始化为PTHREAD_ONCE_INIT。
如果每个线程都掉用pthread_once,系统就能保证初始化例程initflag只被调用一次,即在系统首次调用pthread_once时。
键一旦创建,就可以通过调用pthread_setspecific函数把键和线程私有数据关联起来。可以通过pthread_getspecific函数获得线程私有数据的地址:
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t, const void *value);
如果没有线程私有数据值与键关联,pthread_getspecific将返回一个空指针,可以据此来确定是否需要调用pthread_setspecofic。
实例:线程安全的getenv兼容版本
#include
#include
#include
#include
static pthread_key_t key;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;
pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;
extern char **environ;
static void
thread_init(void)
{
pthread_key_create(&key, free);
}
char *
getenv(const char *name)
{
int i, len;
char *envbuf;
pthread_once(&init_done, thread_init);
pthread_mutex_lock(&env_mutex);
envbuf = (char *)pthread_getspecific(key);
if (envbuf == NULL)
{
envbuf = malloc(ARG_MAX);
if (envbuf == NULL)
{
pthread_mutex_unlock(&env_mutex);
return (NULL);
}
pthread_setspecific(key, envbuf);
}
len = strlen(name);
for (i = 0; environ[i] != NULL; i++)
{
if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '='))
{
strcpy(envbuf, &environ[i][len + 1]);
pthread_mutex_unlock(&env_mutex);
return (envbuf);
}
}
pthread_mutex_unlock(&env_mutex);
return (NULL);
}
使用pthread_once来确保只为将要使用的线程私有数据创建了一个键。如果pthread_getspecific返回的是空指针,就使用pthread_getspecific返回的内存单元。对析构函数,使用free来释放之前由malloc分配的内存,只有当线程私有数据为非NULL时,析构函数才会被调用。
六、取消选项
可取消状态属性可以是PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCEL_DISABLE。线程可以通过调用pthread_setcancelstate修改它的可取消状态。
int pthread_setcancelstate(int state, int *oldstate);
pthread_setcancelstate把当前的可取消状态设置为state,把原来的可取消状态存放在由oldstate指向的内存单元中,这两步是原子操作。
可以调用pthread_testcancel函数在程序中自己添加取消点:
void pthread_testcancel(void);
可以通过调用pthread_setcanceltype来修改取消类型:
int pthread_setcanceltype(int type, int *oldtype);
七、线程和信号
每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。
线程使用pthread_sigmask来阻止信号发送:
int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
线程可以通过调用sigwait等待一个或多个信号发生:
int sigwait(const sig_set *restrict set, int *restrict signo);
使用sigwait的好处在于它可以简化信号处理,允许把异步产生的信号用同步的方式处理。为了防止信号中断线程,可以把每个信号加到每个线程的信号屏蔽字中,然后安排专用线程做信号处理。
要把信号发送到进程,可以调用kill, 要把信号发送到线程,可以调用pthread_kill;
int pthread_kill(pthread_t thread, int signo);
实例:同步信号处理
#include "apue.h"
#include
int quitflag;
sigset_t mask;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait = PTHREAD_COND_INITIALIZER;
void *
thr_fn(void *arg)
{
int err, signo;
for (;;)
{
err = sigwait(&mask, &signo);
if (err != 0)
{
err_exit(err, "sigwait failed");
}
switch (signo)
{
case SIGINT:
printf("ninterrupt\n");
break;
case SIGQUIT:
pthread_mutex_lock(&lock);
quitflag = 1;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&wait);
return(0);
default:
printf("unexpected signal %d\n", signo);
exit(1);
}
}
}
int main(void)
{
int err;
sigset_t oldmask;
pthread_t tid;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT);
if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
{
err_exit(err, "SIG_BLOCK error");
}
err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0)
{
err_exit(err, "can't create thread");
}
pthread_mutex_lock(&lock);
while (quitflag == 0)
{
pthread_cond_wait(&wait, &lock);
}
pthread_mutex_unlock(&lock);
quitflag = 0;
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
{
err_sys("SIG_SETMASK error");
}
exit(0);
}
八、线程和fork
当线程调用fork时,就为子进程创建了整个地址空间的副本。
要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序。
int pthread_atfork(void (*prepare)(void), void (*parent) (void), void (*child)(void));
实例:pthread_atfork实例
#include "apue.h"
#include
pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;
void
prepare(void)
{
printf("prepared locks...\n");
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2);
}
void
parent(void)
{
printf("parent unloking locks...\n");
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
}
void
child(void)
{
printf("child unlocking locks...\n");
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
}
void *
thr_fn(void *arg)
{
printf("thread started...\n");
pause();
return(0);
}
int main(void)
{
int err;
pid_t pid;
pthread_t tid;
#if defined(BSD) || defined(MACOS)
printf("pthread_atfork is unsupported\n");
#else
if ((err = pthread_atfork(prepare, parent, child)) != 0)
{
err_exit(err, "can't install fork handlers");
}
err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0)
{
err_exit(err, "can't create thread");
}
sleep(2);
printf("parent about to fork...\n");
if ((pid = fork()) < 0)
{
err_quit("fork failed");
}
else if (pid == 0)
{
printf("child returned from fork\n");
}
else
{
printf("parent returned from fork\n");
}
#endif
exit(0);
}
编译和运行结果:
gcc 12.7.c -o 12.7 -lpthread
./12.7
thread started...
parent about to fork...
prepared locks...
parent unloking locks...
parent returned from fork
[root@localhost Unix]# child unlocking locks...
child returned from fork
程序中定义了两个互斥量,lock1和lock2,prepare fork处理程序获取这两把锁,child fork处理程序在子进程环境中释放锁,parent fork处理程序在父进程中释放锁。
小结:在UINIX系统中,线程提供了分解并发任务的一种替代模型。线程控制了独立控制线程之间的共享,但也带来了它特有的同步问题。