11. 1 线程概念
进程可以有多个控制线程,每个线程处理各自独立的任务。一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。
1. 多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享,而多个线程共享相同的存储地址空间和文件描述符。
2. 通过为每种事件类型分配单独的处理线程,可以简化处理异步事件的代码。
3. 有些问题可以分解使用多线程从而提高整个程序的吞吐量。
4. 交互的程序可以通过使用多线程来改善响应时间。
11.2 线程标识
进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
线程ID是用pthread_t数据类型来表示的,不一定是整形。
#include "apue.h"
#include <pthread.h>
pthread_t ntid;
void printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid,
(unsigned long)tid, (unsigned long)tid);
}
void * thr_fn(void *arg)
{
printf("%s\n", (char*)arg);
printids("new thread: ");
return((void *)0);
}
int main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, "huahua");
if (err != 0)
err_exit(err, "can't create thread");
printids("main thread:");
sleep(1);
exit(0);
}
11.3 线程终止
如果进程中的任意线程调用了exit、 _Exit 或者_exit,整个进程就会终止。
如果默认的动作是终止进程,则发送到线程的信号就会终止整个进程。
单个线程正常终止的方式:
- 线程返回。
- 被其他线程取消。
- 线程调用pthread_exit
#include "apue.h"
#include <pthread.h>
void clean_up(void* arg) {
printf("clean up, %s\n", (char*)arg);
}
void* thr_fn1(void* arg) {
printf("thread1 start\n");
printf("thread1 exit\n");
return (void*)1;
}
void* thr_fn2(void* arg) {
printf("thread2 start\n");
pthread_cleanup_push(clean_up, "clean push"); //设置线程清理函数,线程退出时执行
printf("thread2 exit\n");
pthread_exit((void*)2);
pthread_cleanup_pop(0); //传入参数0时不会调用清理函数,这里是为了编译通过
}
int main(void) {
int err;
pthread_t tid1, tid2;
void* tret;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "cannot exit thread1\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "cannot exit thread2\n");
err = pthread_join(tid1, &tret); //主线程等待新创建的线程
if (err != 0)
err_exit(err, "cannot join thread1");
printf("thread1 exit code, %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "cannot join thread2");
printf("thread2 exit code, %ld\n", (long)tret);
exit(0);
}
11.4 线程同步
11.4.1 互斥量
多个线程使用互斥量控制访问共同变量的顺序。
例子
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
typedef struct foo {
int f_count;
int f_id;
pthread_mutex_t f_lock;
} foo;
/*
* 创建一个结构体,使用malloc,使对象分配在堆上,调用结束后地址仍然可用
*/
foo* foo_alloc(int id) {
foo* fp;
if ((fp = malloc(sizeof(struct foo))) != NULL){
fp->f_count = 1;
fp->f_id = id;
if ((pthread_mutex_init(&fp->f_lock, NULL)) != 0) { //初始化互斥量
free(fp);
return NULL;
}
}
return fp;
}
void foo_hold(foo* fp) {
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void foo_release(foo* fp) {
pthread_mutex_lock(&fp->f_lock);
if (--fp->f_count == 0) {
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock); //销毁互斥量
free(fp); //释放内存
} else {
pthread_mutex_unlock(&fp->f_lock);
}
}
void do_hold(foo* fp) {
for (int i = 0; i < 100; ++i) {
foo_hold(fp);
}
}
int main() {
foo* fp = foo_alloc(1);
pthread_t pt;
pthread_t pt2;
do_hold(fp);
pthread_create(&pt, NULL, do_hold, fp);
pthread_create(&pt2, NULL, do_hold, fp);
pthread_join(pt, NULL);
pthread_join(pt, NULL);
printf("%d", fp->f_count);
free(fp);
}
11.4.2 避免死锁
在同时需要两个互斥量时,总是让它们以相同的顺序加锁,这样可以避免死锁。需要在满足锁需求的情况下,在代码复杂性和性能之间找到正确的平衡。
例子
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)
typedef struct foo {
int f_count;
int f_id;
struct foo* next;
pthread_mutex_t f_lock;
} foo;
foo* fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER; //列表锁
/*
* 创建一个结构体,使用malloc,使对象分配在堆上,调用结束后地址仍然可用
*/
foo* foo_alloc(int id) {
foo* fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL){
fp->f_count = 1;
fp->f_id = id;
if ((pthread_mutex_init(&fp->f_lock, NULL)) != 0) { //初始化互斥量
free(fp);
return NULL;
}
idx = HASH(id);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
fp->next = fh[idx];
fh[idx] = fp;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
}
return fp;
}
void foo_hold(foo* fp) {
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void foo_release(foo* fp) {
foo* tfp;
int idx;
pthread_mutex_lock(&hashlock);
if (--fp->f_count == 0) {
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->next;
} else {
while (tfp->next != fp)
tfp = tfp->next;
tfp->next = fp->next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_destroy(&fp->f_lock); //销毁互斥量
free(fp); //释放内存
} else {
pthread_mutex_unlock(&hashlock);
}
}
foo* foo_find(int id) {
int idx;
foo* tfp;
idx = HASH(id);
pthread_mutex_lock(&hashlock);
tfp = fh[idx];
while (tfp != NULL && tfp->f_id != id) {
tfp = tfp->next;
}
if (tfp != NULL)
tfp->f_count++;
pthread_mutex_unlock(&hashlock);
return tfp;
}
void do_hold(foo* fp) {
for (int i = 0; i < 100; ++i) {
foo_hold(fp);
}
}
int main() {
foo* fp = foo_alloc(1);
pthread_t pt;
pthread_t pt2;
do_hold(fp);
pthread_create(&pt, NULL, do_hold, fp);
pthread_create(&pt2, NULL, do_hold, fp);
pthread_join(pt, NULL);
pthread_join(pt, NULL);
printf("%d", fp->f_count);
free(fp);
}
11.4.3 带超时的加锁
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict tsptr);
指定超时的绝对时间,mac上不支持。
11.4.4 读写锁
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。
在释放读写锁占用的内存之前,需要调用 pthread_rwlock_destroy 做清理工作。
#include <pthread.h>
typedef struct job {
struct job* j_next;
struct job* j_prev;
pthread_t pid;
} job;
typedef struct queue {
job* q_head;
job* q_tail;
pthread_rwlock_t q_lock;
} queue;
int queue_init(queue *qp) {
int err;
qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->q_lock, NULL);
if (err != 0)
return err;
return 0;
}
/*
* 在头部插入job
*/
void job_insert(queue* qp, job* jp) {
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = qp->q_head;
jp->j_prev = NULL;
if (qp->q_head != NULL)
qp->q_head->j_prev = jp;
else
qp->q_tail = jp;
qp->q_head = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* 在尾部插入job
*/
void job_append(queue* qp, job* jp) {
pthread_rwlock_wrlock(&qp->q_lock); //写锁
jp->j_prev = qp->q_tail;
jp->j_next = NULL;
if (qp->q_tail != NULL)
qp->q_tail->j_next = jp;
else
qp->q_head = jp;
qp->q_tail = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* 删除job
*/
void job_remove(queue* qp, job* jp) {
pthread_rwlock_wrlock(&qp->q_lock); //读锁
if (qp->q_head == jp) {
qp->q_head = jp->j_next;
if (qp->q_tail == jp)
qp->q_tail = NULL;
else
jp->j_next->j_prev = jp->j_prev;
} else if (qp->q_tail == jp) {
qp->q_tail = jp->j_prev;
jp->j_prev->j_next = jp->j_next;
} else {
jp->j_prev->j_next = jp->j_next;
jp->j_next->j_prev = jp->j_prev;
}
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* find job
*/
job* job_find(queue* qp, pthread_t job_id) {
if (pthread_rwlock_rdlock(&qp->q_lock) != 0)
return NULL;
job* jp = qp->q_head;
while (jp != NULL && jp->pid != job_id)
jp = jp->j_next;
pthread_rwlock_unlock(&qp->q_lock);
return jp;
}
11.4.5 带超时的读写锁
pthread_rwlock_timedrdlock 和 pthread_rwlock_timedwrlock
传入的参数指绝对时间
11.4.6 条件信号
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。就是等待和通知机制。
例子
#include <pthread.h>
#include <stdio.h>
struct msg {
struct msg* next;
int id;
};
struct msg* workq;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
void enqueue_msg(struct msg* mp) {
pthread_mutex_lock(&qlock);
mp->next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
void process_msg(void) {
struct msg* mp;
for(;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
printf("id: %d\n", mp->id);
workq = mp->next;
pthread_mutex_unlock(&qlock);
}
}
int main(void) {
pthread_t pid;
pthread_create(&pid, NULL, process_msg, NULL);
struct msg m = {.id = 1, .next = NULL};
enqueue_msg(&m);
struct msg m2 = {.id = 2, .next = NULL};
enqueue_msg(&m2);
pthread_join(pid, NULL);
}
11.4.7 自旋锁
自旋锁适用于锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本的情况。
使用方式可以和互斥量直接替换。mac上不支持。
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);