1 什么是线程: 线程包含了进程内执行环境的必需信息:进程中标识线程的线程ID、一组寄存器、栈、调度优先级和策略、信号屏蔽字、errno遍历以及线程私有数据。
2 相关函数:线程涉及到的函数都包含在头文件<pthread.h>中
比较两个线程的pid是否相同,相同返回非0值
int pthread_equal(pthread_t tid1, pthread_t tid2);
获得线程自身的pid
pthread_t pthread_self(void);
创建线程,成功返回0
线程创建时没有保证创建线程本身还是调用线程先运行。 新创建的线程继承浮点环境和信号屏蔽字,但是未决信号集被清除。
int pthread_create(pthread_t *restrict tidp, //线程ID
const pthread_attr_t *restrict attr, //线程属性
void *(*start_rtn)(void), //线程的回调函数
void *restrict arg); //传递给线程回调函数的参数
线程退出
void pthread_exit(void *rval_ptr);
阻塞等待指定的线程退出。 rval_ptr会指向传递给pthread_exit的指针。
如果线程被取消,rval_ptr指向的内存置为: PTHREAD_CANCELED
成功返回0
int pthread_join(pthread_t thread, void **rval_ptr);
取消同一进程中的其他线程
该函数并不等待线程终止,仅仅提出请求。线程本身可能忽略该请求。
成功返回0
int pthread_cancel(pthread_t tid);
设置线程的取消状态:PTHREAD_CANCEL_ENABLE or PTHREAD_CANCEL_DISABLE
如果是disable,线程不能被取消
int pthread_setcancelstate(int state, int *oldstate);
线程取消只是一种请求,线程会继续执行,而不是立即退出,直到遇到取消点。
取消点是线程检查是否被取消并执行取消动作的位置。很多函数包含取消点。
如果代码中不能保证到取消点,可以通过以下函数,主动触发取消点的检查动作:
void pthread_testcancel(void);
设置取消的类型,包括:
PTHREAD_CANCEL_DEFERRED or PTHREAD_CANCEL_ASYNCHRONOUS
int pthread_setcanceltype(int type, int *oldtype);
设定线程退出时调用的函数:
注册顺序与调用顺序相反。
当下面的情况时,回调函数才会被调用:
a 调用pthread_exit时
b 响应pthread_cancle时
c pthread_cleanup_pop(非零值)调用时
如果pthread_cleanup_pop(0)用零值,会取消栈上对应位置的回调函数。
再有就是,使用时,要使得push与pop成对出现,否则可能会导致编译不通过。
因为这两个函数可能是宏实现的。 pop可以在不会被调用的地方写。。
void pthread_cleanup_push(void (*rtn)(void *),
void *arg);
void pthread_cleanup_pop(int execute);
将指定的线程设置为分离状态,线程退出时资源会自动收回。
分离状态的线程不能被pthread_join
int pthread_detach(pthread_t tid);
3 线程同步:
a 互斥量 一下函数成功时均返回0
初始化一个互斥量
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
对互斥量加锁,如果已经上锁调用线程将阻塞知道互斥量被解锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
尝试对互斥量加锁,如果已经上锁返回EBUSY,而不回阻塞。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
锁的粒度太粗,使得很多线程阻塞相同的锁。
锁的粒度太细,过多的锁开销影响系统的性能,以及程序本身逻辑控制的复杂度。
b 读写锁:
读写锁有三种状态:读模式下加锁,写模式加锁,不加锁。一次只有一个线程可以占有写锁。
但是可以有多个线程占用读锁。
如果写加锁状态,所有锁都会阻塞。如果是读加锁,读加锁成功,但是写加锁要等到所有的读锁释放。
也被成为共享-独占锁。读锁,是共享锁,写锁是独占。
以下函数成功均返回0
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
不会阻塞,不能获得锁,返回EBUSY
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
c 条件变量
也可以直接用静态变量初始化:PTHREAD_COND_INITIALIZER
int pthread_cond_init(pthread_cond_t *restrict cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
将锁住的互斥量传递给该函数,该函数将线程放到等待该条件的线程列表上,
然后对互斥量解锁。这个过程是原子操作。当该函数返回时,互斥量被锁住。
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
传递给该函数的时间是绝对值,而不是相对值。
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict timeout);
唤醒等待该条件的某个线程 (POSIX允许唤醒多个线程)
int pthread_cond_signal(pthread_cond_t *cond);
唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
条件变量机制不是异步信号安全的,也就是说,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。
一个使用条件变量的例子:
一个工厂批量生产一个产品,有多个工人要对每个产品进行不同的操作,相当于每个对象会被多个线程修改。如果给每个对象都分配一个互斥量,可能会超过操作系统对互斥量数目的上线。对于该问题,有这样的一种解决方案:
对于该对象定义一个stat字段,标记状态,当有人使用时,状态标记为加锁,否则是可用状态。 如果一个线程要使用一个处于加锁状态的对象,利用条件变量先让其等待。怎么实现呢?
假设我们我们的对象统一都在一个数据结构中,每次都从该结构中取出,我们定义一个全局的互斥量,表示对该结构体的访问。
//我们我处理的每个对象
struct Object {
int stat;
Cond *cond;
};
//所有的对象都要从这个里面获取
Object *vec_obj;
//获取一个对象时的锁
pthread_mutex_t mu;
struct Cond{
pthread_cond_t cond;
int ref; //标识是否有人已经在使用该条件变量
//假设我们用链表管理所有的条件变量
Cond *next;
};
Cond * hcond;
当我们要访问一个对象时,调用get方法:
Object *get_obj(int pos) {
//首先获得全局的锁,这个锁也同时保护了cond变量
pthread_mutex_lock(&mu);
Object *t = NULL;
//通过一些手段我们获得了要找的对象
//一些其他的处理
//如果该对象已经有人在访问
if(t->stat == T_LOCKED) {
if(t->cond == NULL) {
//从所有的cond中获得一个没有使用的。
Cond *tcond = hcond;
for(;tcond & tcond->ref>0; tcond=tcond->next)
;
if(tcond == NULL) {
cond = new Cond();
}
t->cond = tcond;
}
//等待
++t->ref;
pthread_cond_wait(&t->cond->cond,&mu);
//等待结束后-1
--t->ref;
}
pthread_mutex_unlock(&mu);
}
当我们对该对象的访问结束时,释放:
void put_obj(Object *obj) {
//虽然只操作这一个变量,但是cond变量需要这个锁的保护
//也就是说,唤醒的条件,这个上下文,需要这个互斥量的保护
//我们可以看做我们的操作是在修改我们全局的数据。
pthread_mutex_lock(&mu);
if(obj->stat == T_LOCKED) {
obj->stat = T_FREE;
if(obj->cond != NULL) {
thread_cond_signal(obj->cond->cond);
}
}
pthread_mutex_unlock(&mu);
}
4 线程的属性:
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
最典型的是设置可分离属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
设置线程栈的大小
int pthread_attr_getstack(const pthread_attr_t *restrict attr,
void **restrict stackaddr, //栈内存中可寻址范围中的最低地址
size_t *restrict stacksize);
int pthread_attr_setstack(const pthread_attr_t *attr,
void *stackaddr, size_t *stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,
size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
线程属性guardsize控制线程栈末尾之后用以避免栈溢出的扩展内存大小。
当我们改变了栈的大小时,该值为0.
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,
size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
进程中的所有信息对线程都是共享的,包括可执行程序的文本,程序的全局变量和堆内存、栈以及文件描述符。
linux线程的实现比较特殊,是一种特殊的进程,即将父子进程的一些数据设为共享,线程创建的过程内核也是通过fork进行的,只是后续设置了一些属性。
fork的写时拷贝: fork本来的动作应该是创建一个新的进程,该进程的数据与父进程的数据完全相同,即将需要将父进程的数据拷贝到新创建的进程。但是fork之后经常会调用exec,这样拷贝的工作做了无用功,于是就出现了写时拷贝。就是说新的子进程与父进程共享原进程空间,子进程是只读的,当子进程需要写的时候,才会进行实际的拷贝过程。
2 相关函数:线程涉及到的函数都包含在头文件<pthread.h>中
比较两个线程的pid是否相同,相同返回非0值
int pthread_equal(pthread_t tid1, pthread_t tid2);
获得线程自身的pid
pthread_t pthread_self(void);
创建线程,成功返回0
线程创建时没有保证创建线程本身还是调用线程先运行。 新创建的线程继承浮点环境和信号屏蔽字,但是未决信号集被清除。
int pthread_create(pthread_t *restrict tidp, //线程ID
const pthread_attr_t *restrict attr, //线程属性
void *(*start_rtn)(void), //线程的回调函数
void *restrict arg); //传递给线程回调函数的参数
线程退出
void pthread_exit(void *rval_ptr);
阻塞等待指定的线程退出。 rval_ptr会指向传递给pthread_exit的指针。
如果线程被取消,rval_ptr指向的内存置为: PTHREAD_CANCELED
成功返回0
int pthread_join(pthread_t thread, void **rval_ptr);
取消同一进程中的其他线程
该函数并不等待线程终止,仅仅提出请求。线程本身可能忽略该请求。
成功返回0
int pthread_cancel(pthread_t tid);
设置线程的取消状态:PTHREAD_CANCEL_ENABLE or PTHREAD_CANCEL_DISABLE
如果是disable,线程不能被取消
int pthread_setcancelstate(int state, int *oldstate);
线程取消只是一种请求,线程会继续执行,而不是立即退出,直到遇到取消点。
取消点是线程检查是否被取消并执行取消动作的位置。很多函数包含取消点。
如果代码中不能保证到取消点,可以通过以下函数,主动触发取消点的检查动作:
void pthread_testcancel(void);
设置取消的类型,包括:
PTHREAD_CANCEL_DEFERRED or PTHREAD_CANCEL_ASYNCHRONOUS
int pthread_setcanceltype(int type, int *oldtype);
设定线程退出时调用的函数:
注册顺序与调用顺序相反。
当下面的情况时,回调函数才会被调用:
a 调用pthread_exit时
b 响应pthread_cancle时
c pthread_cleanup_pop(非零值)调用时
如果pthread_cleanup_pop(0)用零值,会取消栈上对应位置的回调函数。
再有就是,使用时,要使得push与pop成对出现,否则可能会导致编译不通过。
因为这两个函数可能是宏实现的。 pop可以在不会被调用的地方写。。
void pthread_cleanup_push(void (*rtn)(void *),
void *arg);
void pthread_cleanup_pop(int execute);
将指定的线程设置为分离状态,线程退出时资源会自动收回。
分离状态的线程不能被pthread_join
int pthread_detach(pthread_t tid);
3 线程同步:
a 互斥量 一下函数成功时均返回0
初始化一个互斥量
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
对互斥量加锁,如果已经上锁调用线程将阻塞知道互斥量被解锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
尝试对互斥量加锁,如果已经上锁返回EBUSY,而不回阻塞。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
锁的粒度太粗,使得很多线程阻塞相同的锁。
锁的粒度太细,过多的锁开销影响系统的性能,以及程序本身逻辑控制的复杂度。
b 读写锁:
读写锁有三种状态:读模式下加锁,写模式加锁,不加锁。一次只有一个线程可以占有写锁。
但是可以有多个线程占用读锁。
如果写加锁状态,所有锁都会阻塞。如果是读加锁,读加锁成功,但是写加锁要等到所有的读锁释放。
也被成为共享-独占锁。读锁,是共享锁,写锁是独占。
以下函数成功均返回0
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
不会阻塞,不能获得锁,返回EBUSY
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
c 条件变量
也可以直接用静态变量初始化:PTHREAD_COND_INITIALIZER
int pthread_cond_init(pthread_cond_t *restrict cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
将锁住的互斥量传递给该函数,该函数将线程放到等待该条件的线程列表上,
然后对互斥量解锁。这个过程是原子操作。当该函数返回时,互斥量被锁住。
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
传递给该函数的时间是绝对值,而不是相对值。
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict timeout);
唤醒等待该条件的某个线程 (POSIX允许唤醒多个线程)
int pthread_cond_signal(pthread_cond_t *cond);
唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
条件变量机制不是异步信号安全的,也就是说,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast()很可能引起死锁。
一个使用条件变量的例子:
一个工厂批量生产一个产品,有多个工人要对每个产品进行不同的操作,相当于每个对象会被多个线程修改。如果给每个对象都分配一个互斥量,可能会超过操作系统对互斥量数目的上线。对于该问题,有这样的一种解决方案:
对于该对象定义一个stat字段,标记状态,当有人使用时,状态标记为加锁,否则是可用状态。 如果一个线程要使用一个处于加锁状态的对象,利用条件变量先让其等待。怎么实现呢?
假设我们我们的对象统一都在一个数据结构中,每次都从该结构中取出,我们定义一个全局的互斥量,表示对该结构体的访问。
//我们我处理的每个对象
struct Object {
int stat;
Cond *cond;
};
//所有的对象都要从这个里面获取
Object *vec_obj;
//获取一个对象时的锁
pthread_mutex_t mu;
struct Cond{
pthread_cond_t cond;
int ref; //标识是否有人已经在使用该条件变量
//假设我们用链表管理所有的条件变量
Cond *next;
};
Cond * hcond;
当我们要访问一个对象时,调用get方法:
Object *get_obj(int pos) {
//首先获得全局的锁,这个锁也同时保护了cond变量
pthread_mutex_lock(&mu);
Object *t = NULL;
//通过一些手段我们获得了要找的对象
//一些其他的处理
//如果该对象已经有人在访问
if(t->stat == T_LOCKED) {
if(t->cond == NULL) {
//从所有的cond中获得一个没有使用的。
Cond *tcond = hcond;
for(;tcond & tcond->ref>0; tcond=tcond->next)
;
if(tcond == NULL) {
cond = new Cond();
}
t->cond = tcond;
}
//等待
++t->ref;
pthread_cond_wait(&t->cond->cond,&mu);
//等待结束后-1
--t->ref;
}
pthread_mutex_unlock(&mu);
}
当我们对该对象的访问结束时,释放:
void put_obj(Object *obj) {
//虽然只操作这一个变量,但是cond变量需要这个锁的保护
//也就是说,唤醒的条件,这个上下文,需要这个互斥量的保护
//我们可以看做我们的操作是在修改我们全局的数据。
pthread_mutex_lock(&mu);
if(obj->stat == T_LOCKED) {
obj->stat = T_FREE;
if(obj->cond != NULL) {
thread_cond_signal(obj->cond->cond);
}
}
pthread_mutex_unlock(&mu);
}
4 线程的属性:
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
最典型的是设置可分离属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
设置线程栈的大小
int pthread_attr_getstack(const pthread_attr_t *restrict attr,
void **restrict stackaddr, //栈内存中可寻址范围中的最低地址
size_t *restrict stacksize);
int pthread_attr_setstack(const pthread_attr_t *attr,
void *stackaddr, size_t *stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,
size_t *restrict stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
线程属性guardsize控制线程栈末尾之后用以避免栈溢出的扩展内存大小。
当我们改变了栈的大小时,该值为0.
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,
size_t *restrict guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);