1.线程标识
和进程一样每一个线程都有一个线程ID,类型为pthread_t。但是与进程不同的是,进程ID是在系统中唯一的,而线程ID只在进程环境有效。
#include <pthread.h>
pthread_t pthread_self(void)
//返回值为调用线程的线程ID
#include <pthread.h>
int pthread_create(pthread_t *restricttidp,
constpthread_attr_t *restrict attr, void* (*start_rtn)(void*), void* restrict arg);
//成功返回0,出错返回错误编号
//参数说明:tidp为线程ID号(参数返回),attr为线程属性(NULL为默认),start_rtn为启动例程,注意参数和返回值都是void*,arg为启动例程的参数。
2.线程终止
如果某一个线程调用exit、_exit或者_Exit,那么整个进程将终止,线程终止时调用pthread_exit函数,或者线程请求终止另外一个线程就调用pthred_cancle函数
#include <pthread.h>
void pthread_eixt(void *rval_ptr);//rval_ptr为返回状态,一般进行强制转换(void*)1
int pthread_cancel(pthread_t tid);
//线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。
和进程类似,线程也有终止清理函数:
#include <pthread.h>
void pthread_clearnup_push(void(*rtn)(void*),void*arg);
//相当于atexit的功能,注意区别,线程的清理函数是有参数的
void pthread_clearnup_pop(int execute);
//execute设置为0,清理函数将不被调用,其将删除上次push调用建立的清理处理程序。
注意:如果进程直接从启动例程返回(return),而不是在启动例程中调用pthread_exit那么清理函数是不会被调用的,具体的函数间APUE,程序清单11-4。
这里总结一下线程原语和进程原语的比较:
3.线程同步
先看一个迅雷多线程的笔试题:编写一个程序,开启3个线程,这3个线程的ID分别为A、B、C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示;如:ABCABC….依次递推。
3.1互斥量
就是mutex,在unix中被定义为pthread_mutex_t类型(其实感觉一个bool类型就足够了,也就是0-1锁)。在使用互斥量之前必须对其进行初始化,如果是静态分配的互斥量,可以将其设置为常量PTHREAD_MUTEX_INITIALIZER,也可以通过pthread_mutex_init函数进行初始化。如果是动态分配的互斥量(例如malloc),那么在释放内存前需要调用pthread_mutex_destory。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t*restrict mutex,
constpthread_mutexattr_t *restrict attr);
//前一个参数为锁,有一个参数为所类型,NULL为默认类型
//成功返回0,出错返回错误编号
int pthread_mutex_destory(pthread_mutex_t*mutex);
//成功返回0,出错返回错误编号
对互斥量加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁,纳闷调用线程将会一直阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t*mutex);
int pthread_mutex_trylock(pthread_mutex_t*mutex);
//如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。
//如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock
//将锁住互斥量,不会出项阻塞并返回0,否则就会失败,不能锁住互斥量,而返回EBUSY
int pthread_mutex_unlock(pthread_mutex_t*mutex);
//这三个函数成功返回0,出错返回错误号
3.2条件变量
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的,线程在改变条件前必须首先锁住互斥量,且只有在锁住互斥量以后才能计算条件。
条件变量使用之前必须首先进行初始化,pthread_cond_t数据类型代表的条件变量可以用两种方式初始化。可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量,但是如果条件变量是动态分配的,可以使用pthread_cond_init函数进行初始化。在释放底层的内存空间前,可以使用pthread_mutex_destroy函数对条件变量进行销毁。
#include <pthread.h>
int pthread_cond_init(pthread_cond_t*restrict cond, pthread_condattr_t *restrict attr);
int pthread_cond_destory(pthread_cond_t*cond);
//成功返回0,出错返回错误编号
除非需要创建一个非默认属性的条件变量,否则pthread_cond_init函数的attr参数可以设置为NULL。
使用pthread_cond_wait等待条件变为真,如果在给定时间内条件不能满足,那么会生成一个代表出错码的返回值。调用者需要把锁住的互斥量传给pthread_cond_wait对条件进行保护。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。当pthread_cond_wait返回时(此时条件为真,测试返回值为0),互斥量再次被锁住。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t*restrict cond,
pthread_mutex_t*restrict mutex); //注意第二参数,cond需要mutex的保护
int pthread_cond_timedwait(pthread_cond_t*restrict cond,
pthread_mutex_t*restrict mutex
conststruct timespec *restrict timeout);
//成功返回0,出错返回错误编号
pthread_cond_signal函数将唤醒等待该条件的某个线程,而pthread_cond_broadcast函数将唤醒等待该条件的所有线程。必须注意一定要在改变条件状态以后再唤醒等待线程。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t*cond);
int pthread_cond_broadcast(pthread_cond_t*cond);
//成功返回0,出错返回错误号
3.3迅雷笔试题程序
有了互斥量和条件变量的概念就可以解上述迅雷的笔试题了,
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#define NUM 3
//省略了所有的出错检测
pthread_mutex_tlock=PTHREAD_MUTEX_INITIALIZER; //静态初始化互斥量
pthread_cond_tcond=PTHREAD_COND_INITIALIZER; //静态初始化条件变量
int n=0; //全局的n是用于判定输出那个字符
void* th_fn(void *arg)
{
intparam=(int)arg;//需要进行强制类型转换
charc='A'+param;
inti;
for(i=0;i<10;i++)
{
pthread_mutex_lock(&lock); //其实有返回值的,但是省略了出错检测
while(n!=param)
pthread_cond_wait(&cond,&lock);
printf("%c",c);
n=(n+1)%NUM; //轮流,输出下一个字符的条件
pthread_mutex_unlock(&lock);
pthread_cond_broadcast(&cond); //通知所欲等待cond的线程,条件改变
}
return(void*)0;
}
int main()
{
pthread_ttid[NUM];
inti;
void*th_exit_state; //注意返回状态是void*类型
for(i=0;i<NUM;i++)
pthread_create(&tid[i],NULL,th_fn,(void*)i);//创建3个线程
for(i=0;i<NUM;i++)
pthread_join(tid[i],&th_exit_state); //pthread_join返回状态的参数类型是void**
return0;
}
4.读写锁
读写锁是支持读者/写者模型的(读写锁的特性可以从读者/写者模型的特点来思考),比mutex(仅有两个状态,仅有一个线程可以对其加锁)有更高的并发性。因为读写锁有三种状态:读模式加锁状态、写模式加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
①当读写锁是写加锁状态时,所有(无论读写)试图对这个所加锁的线程都会被阻塞。②当读写锁是读加锁状态时,所有试图以读模式对其加锁的线程都可以得到访问权。③当读写锁是读加锁状态时,试图以写模式对其加锁的线程将阻塞,等待所有的读模式锁被释放,同时将阻塞所有后续读请求。
与互斥量一样读写锁也需要初始化和销毁,不同的是读写锁没有静态初始化方式!
#include<pthread.h>
intpthread_rwlock_init(pthrea_rwlock_t *restrict rwlock,
constpthread_rwlockattr_t *restrict attr);
//如果希望是默认属性可以传NULL
intpthread_rwlock_destory(pthread_rwlock_t *rwlock);
//若成功返回0,出错返回错误编号
读模式调用pthread_rwlock_rdlock,在写模式下加锁调用pthread_rwlock_wrlock,释放锁都是调用pthread_rwlock_unlock。
#include<pthread.h>
intpthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
intpthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
intpthread_rwlock_unlock(pthread_rwlock_t *rwlcok);
//所有的返回值都是:若成功返回0,出错返回错误编号
//因为可能会对共享模式下可以获取的锁的数量有限制,高并发的情况。
//所以也有有条件的读写锁原语的版本(试图获取)。
intpthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
itnpthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//可以获取锁的时候返回0;否则,返回错误EBUSY
4.1代码示例:读者写者问题:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#define WriterNum 2
#define ReaderNum 5
//#define DEBUG 0
pthread_rwlock_t rwlock;
class data //数据类
{
public:
data(int i,float f):m_i(i),m_f(f){};
int m_i;
float m_f;
};
data *pdata=NULL;
void* th_reader(void *arg)
{
int id=(int)arg;
int i;
for(i=0;i<10;i++)
{
pthread_rwlock_rdlock(&rwlock);
if(pdata==NULL)
printf("Nodata\n");
else
printf("datais: int(%d) float(%f)",pdata->m_i,pdata->m_f);
pthread_rwlock_unlock(&rwlock);
}
printf("readerexit id=");
pthread_exit((void*)id);
}
void *th_writer(void* arg)
{
int id=(int)arg;
int i;
for(i=0;i<25;i++)
{
pthread_rwlock_wrlock(&rwlock);
if(pdata==NULL)
{
pdata=new data(i,0.001);
printf("createnew data\n");
}
else
{
delete pdata;
pdata=NULL;
printf("deletedata\n");
}
pthread_rwlock_unlock(&rwlock);
}
printf("writerexit id=");
pthread_exit((void*)id);
}
int main(void)
{
pthread_ttid_writer[WriterNum];
pthread_ttid_reader[ReaderNum];
int i;
int ret;
void* tret;
pthread_rwlock_init(&rwlock,NULL);
for (i = 0; i <ReaderNum; ++i)
{
ret=pthread_create(&tid_reader[i],NULL,th_reader,(void*)i);
if(ret!=0)
printf("createthread error!\n");
}
for (i = 0; i <WriterNum; ++i)
{
ret=pthread_create(&tid_writer[i],NULL,th_writer,(void*)i);
if(ret!=0)
printf("createthread error!\n");
}
for (i = 0; i <ReaderNum; ++i)
{
pthread_join(tid_reader[i],&tret);
printf("%s\n",(char*)tret);
}
for (i = 0; i <ReaderNum; ++i)
{
pthread_join(tid_writer[i],&tret);
printf("%s\n",(char*)tret);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
5.线程属性
在创建线程的时候(pthread_create)如果属性参数设置为NULL,那么就采用线程的默认属性。线程的属性是一个pthread_attr_t的结构是对用户不透明的,程序员不知道其中的内部结构。对于pthread_attr_t的结构需要初始化和销毁。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t*attr);
int pthread_attr_destory(pthread_attr_t*attr);
//成功返回0,出错返回错误编号
调用pthread_attr_init函数之后,初始化的线程属性依然是默认属性,如果需要修改线程属性(自定义的attr变量)需要调用另外的函数。
与pthread_attr_t相关的线程属性有4个:
5.1分离属性
这里先提一个函数pthread_detach:
#include <pthread.h>
int pthread_detach(pthread_t tid);
//若成功返回0,否则返回错误编号
线程和进程一样,当线程退出后,需要回收线程的资源,如果不回收的话,类似僵死进程。所以需要调用函数pthread_join,但是调用pthread_join的线程会被阻塞,有时候是不希望这样的。比如在Web服务器中当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接)。
解决的办法就是以分离状态启动线程,以分离状态启动的线程在线程退出的时候,内核会负责回收资源。
做法就是在子线程中调用pthread_detach(pthread_self());或者父线程调用pthread_detach(thread_id)(非阻塞,可立即返回)。
当然也可以查看和设置线程的属性,并用此属性创建线程:
#include <pthread.h>
int pthread_attr_getdetachstate(constpthread_attr_t *restrict attr, int *detachstate);
//参数返回detachstate
int pthread_attr_setdetachstate(constpthread_attr_t *restrict attr, int detachstate);
//成功返回0,出错返回错误编号
5.2 其他属性
另外3个属性,暂时平时编程也用不到,分别是guardsize(线程栈末尾的警戒缓冲区大小)、stackaddr(线程栈的最低地址)、stacksize(字节数)(线程栈的大小)