对linux线程编程的学习整理。
1、线程相对于进程的优点
a、在多进程情况下,创建一个新的线程花费时间少。
b、在系统调度方面,线程的切换速度快。
c、在通信机制方面,进程间的数据空间相互独立,通信需要专门的通信方式,需要操作系统的参与。线程共享数据空间,通信不用通过操作系统。
d、线程可以提高应用程序的相应速度,耗时线程用完时间片后,让出CUP,其他操作容易得到相应。
e、线程可以提高多处理器效率,让多个线程在不同的处理器上运行。
f、线程可以改善程序的结构,对大程序的每个命令设计成一个个线程,这样就避免了大程序的复杂结构。
注:线程的私有数据信息:线程号、寄存器、堆栈、信号掩码、优先级、线程私有的存储空间。
2、线程操作函数总结
1)、线程创建函数
int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void), void *restrict arg); 返回值:若是成功建立线程返回0,否则返回错误的编号 形 参: thread 要创建的线程的线程id指针,当线程创建成功时,返回创建的线程ID. attr 指定线程的属性,NULL为默认属性 start_routine 该参数为一个函数指针,指向线程创建后要调用的函数,这个被线程调用的函数称为线程函数 arg 该参数指向传递给线程函数的参数 说 明:创建一个具有指定参数的线程。 头文件:#include <pthread.h>
函数应用举例:
#include ...
int *thread(void *arg)//要调用的函数
{
pthread_t newid;
newid = pthread_self();//获取本线程的线程id
printf("this is a new thread, thread ID = %u\n", newid);//此处将打印出新建成进程的ID
return NULL;
}
int main(void)
{
pthread_t th_id;
printf("main thread, ID is %u\n", pthread_self());//打印主线程的ID
if(pthread_create(&th_id, NULL, (void*)thread, NULL) != 0){ //创建新进程,并执行thread函数
printf("thread creation failed\n");
exit(1);
}
sleep(1);
exit(0);
}
2)、等待线程结束函数
int pthread_join(pthread_t thread, void **retval); 返回值:若是成功建立线程返回0,否则返回错误的编号 形 参: thread 被等待的线程标识符 retval 一个用户定义的指针,它可以用来存储被等待线程的返回值
功 能:pthread_join使一个线程等待另一个线程结束。代码中如果没有pthread_join(),主线程会很快结束从而使整个进程结束,那么创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程(自己创建的进程)结束自己才结束,使创建的线程有机会执行。 说 明:这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。在任何一个时间点上,线程是可结合的(joinable)。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调用pthread_join。 头文件:#include <pthread.h>
函数应用举例:(转)
- #include <stdio.h>
- #include <pthread.h>
- int count =0;
- // 初始化mutex变量。
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- void * print_count(int);
- int main()
- {
- const int num = 10;
- pthread_t threads[num];
- int i=0;
- for(i=0; i< num; i++){
- //第一个参数是pthread_t变量,第二个是pthread_t变量的属性
- //第三个是pthread_t需要执行的函数,其函数原型为
- // void* (*startfun)(void*)
- // 所以传入的是一个返回void*的函数(这里当然可以转换)
- // 并且接受的是void*的参数,所以需要把i转换为void *
- pthread_create(&threads[i],NULL,print_count,(void*)i);
- }
- void* result = NULL;
- for(i=0; i < num; i++){
- // 这里就是同步得到结果。打印的是线程执行的状态,0为成功。非0失败
- printf("end %d;\n",pthread_join(threads[i],&result));
- if(result == NULL){
- printf("NULL\n");
- }
- else{
- // 这里打印的是pthread_t执行完print_count后返回的结果。
- printf("%x\n",result);
- }
- }
- return 0;
- }
3)、线程终止函数
void pthread_exit(void* retval);返回值:无 形 参:retval 函数的返回指针,只要pthread_join中的第二个参数retval不是NULL,这个值将被传递给retval 说 明:终止调用它的线程并返回一个指向某个对象的指针。使用函数pthread_exit退出线程,这是线程的主动行为;由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,但是可以用pthread_join()函数来同步并释放资源。 头文件:#include <pthread.h>注:两种特殊情况:1、在主线程中,如果从main函数中返回或是调用exit函数退出主线程,则整个进程将终止。2、如果主线程中调用pthread_exit函数,则仅仅是主线程消亡,进程不会结束,进程内的其他线程也不会终止。
应用举例:(转)
- /*thread.c*/
- #include <stdio.h>
- #include <pthread.h>
- /*线程一*/
- void thread_1(void)
- {
- int i=0;
- for(i=0;i<=6;i++)
- {
- printf("This is a pthread_1.\n");
- if(i==2)
- pthread_exit(0); //用pthread_exit()来调用线程的返回值,用来退出线程,但是退出线程所占用的资源不会随着线程的终止而得到释放
- sleep(1);
- }
- }
- /*线程二*/
- void thread_2(void)
- {
- int i;
- for(i=0;i<3;i++)
- printf("This is a pthread_2.\n");
- pthread_exit(0); //用pthread_exit()来调用线程的返回值,用来退出线程,但是退出线程所占用的资源不会随着线程的终止而得到释放
- }
- int main(void)
- {
- pthread_t id_1,id_2;
- int i,ret;
- /*创建线程一*/
- ret=pthread_create(&id_1,NULL,(void *) thread_1,NULL);
- if(ret!=0)
- {
- printf("Create pthread error!\n");
- return -1;
- }
- /*创建线程二*/
- ret=pthread_create(&id_2,NULL,(void *) thread_2,NULL);
- if(ret!=0)
- {
- printf("Create pthread error!\n");
- return -1;
- }
- /*等待线程结束*/
- pthread_join(id_1,NULL); //同步并释放资源
- pthread_join(id_2,NULL);
- return 0;
- }
4)、线程取消函数
int pthread_cancel(pthread_t thread);
返回值:若是成功返回0,否则返回错误的编号
形 参: thread 要取消线程的标识符ID,设置thread的请求标志 ,如果是异步模式,发送信号。
说 明:它通常需要被取消线程的配合。默认情况(延迟取消),它就是给pd设置取消标志, pd线程在很多时候会查看自己是否有取消请求如果有就主动退出,这些查看是否有取消的地方称为取消点。取消点的本质就是修改取消模式为异步模式,并检查是否有未决取消请求。如果是异步取消(pthread_setcanceltype设置),那么 pthread_cancel同时还给发送信号通知对方来结束自己。 头文件:#include <pthread.h>
注:pthread_kill函数的功能是向指定线程发送信号,信号为0时用于检查此线程ID的线程是否存活。 pthread_cancel函数的功能是给线程发送取消信号,使线程从取消点退出。
5)、获取当前线程标识ID
pthread_t pthread_self(void); 返回值:当前线程的线程ID标识 形 参:无 说 明:获取当前调用线程的 thread identifier(标识号) 头文件:#include <pthread.h>
应用举例:
pthread_t th_id = pthread_self();//获取了当前线程ID6)、分离释放线程
int pthread_detach(pthread_t thread); 返回值:若是成功返回0,否则返回错误的编号 形 参:thread 要释放线程的标识符ID
功 能:在任何一个时间点上,线程是可分离的(detached)。一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。
应 用:比如在Web服务器中当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接),这时可以在子线程中加入代码pthread_detach(pthread_self())或者父线程调用pthread_detach(thread_id)(非阻塞,可立即返回)这将该子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。
说 明:linux线程执行和windows不同,pthread有两种状态joinable状态和unjoinable状态。一个线程默认的状态是joinable,如果线程是joinable状态,当线程函数自己返回退出时或pthread_exit时都不会释放线程所占用堆栈和线程描述符(总计8K多)。只有当你调用了pthread_join之后这些资源才会被释放。若是unjoinable状态的线程,这些资源在线程函数退出时或pthread_exit时自动会被释放。unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach自己, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。如果线程状态为joinable,需要在之后适时调用pthread_join。
头文件:#include <pthread.h>7)、比较两个线程是否是同一个线程
int pthread_equal(pthread_t thread1, pthread_t thread2); 返回值:若是返回0 不相等,非零相等 形 参: thread1 要比较的线程的标识符ID thread2 要比较的线程的标识符ID 说 明:判断两个线程ID是否相等。 头文件:#include <pthread.h>
应用举例:(转)
- #include <stdio.h>
- #include <stdlib.h>
- #include <pthread.h>
- int main(){
- pthread_t thread_id;
- thread_id=pthread_self(); // 返回调用线程的线程ID
- printf("Thread ID: %lu.\n",thread_id);
- if (pthread_equal(thread_id,pthread_self()))
- {
- printf("Equal!\n");
- } else {
- printf("Not equal!\n");
- }
- return 0;
- }
pthread_once_t once = PTHREAD_ONCE_INIT;8)、保证函数在进程中仅执行一次
int pthread_once(pthread_once_t *once_control, void(*init_routine)(void)); 返回值:功 能:本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。在LinuxThreads
中,实际"一次性函数"的执行状态有三种:NEVER(0)、IN_PROGRESS(1)、DONE (2),如果once初值设为1,则由于所有pthread_once()都必须等
待其中一个激发"已执行一次"信号,因此所有pthread_once ()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。 头文件:#include <pthread.h>
应用程序举例:
void run(void)
{
printf("....");
}
void *thread1(void *arg)
{
...
pthread_once(&once, run);//被调用的函数run()只能执行一次,即使再有一个线程调用这个函数也是只执行一次。
...
}
注:
- pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则将产生奇怪的结果
- 由 restrict 修饰的指针是最初唯一对指针所指向的对象进行存取的方法,仅当第二个指针基于第一个时,才能对对象进行存取。对对象的存取都限定于基于由 restrict 修饰的指针表达式中。 由 restrict 修饰的指针主要用于函数形参,或指向由 malloc() 分配的内存空间。restrict 数据类型不改变程序的语义。 编译器能通过作出 restrict 修饰的指针是存取对象的唯一方法的假设,更好地优化某些类型的例程。
3、线程属性
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之
前调用。之后须用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack
size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling
policy and parameters)。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
- //线程属性结构如下:
- typedef struct
- {
- int detachstate; //线程的分离状态 表示新创建的线程是否与进程中其他的线程脱离同步。
- int schedpolicy; //线程调度策略 SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)、SCHED_FIFO(..)
- structsched_param schedparam; //线程的调度参数 这个参数仅当调度策略为实时(SCHED_RR、SCHED_FIFO)时才有效。
- int inheritsched; //线程的继承性 1、显示指定新线程显示指定调度策略和参数。2、继承调用者线程的值
- int scope; //线程的作用域 表示线程间竞争CPU的范围,就是线程优先级的有效范围。
- size_t guardsize; //线程栈末尾的警戒缓冲区大小 警戒堆栈的大小
- int stackaddr_set; //线程的栈设置 堆栈的地址集
- void* stackaddr; //线程栈的位置 堆栈的地址
- size_t stacksize; //线程栈的大小 堆栈的大小
- }pthread_attr_t;
Linux线程属性总结(比较详细的博客)
http://blog.csdn.net/zsf8701/article/details/7842392
4、线程的私有数据
在多线程环境下,进程内的所有线程共享进程的数据空间,因此全局变量为所有线程所共有。在程序设计中有时需要保存线程
自己的全局变量,这种特殊的变量仅在某个线程内部有效。这个问题通过创建线程的私有数据(TSD)来解决的。
私有数据采用一键多值的技术,使用线程私有数据时,首先要为每个线程数据创建一个相关联的键,在各个线程内部都使用这
个公用的键来指代线程数据,但是在不同的线程中,这个键(key)代表的数据是不同的。key一旦被创建,所有线程可访问,但各
个线程可根据自己需要往key中填入不同的值。一键多值依赖于一个关键数据结构数组,即TSD池。数组结构如下图:
定义如下:
static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = {{0, NULL}};
在上图中Key 结构的“标志”指示这个数据元素是否正在使用。在刚开始时所有的标志初始化为“不在使用”。当一个线程调用
pthread_key_create创建一个新的线程特定数据元素时,系统会搜索Key结构数组,找出第一个“不在使用”的元素。并把该元素的索
引(0~127)称为“键”。 返回给调用线程的正是这个索引。
相关函数:1)、int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void*));
参数: keyp 指向键值的指针
(*destructor)(void*) 函数指针,指针不为空时,则在线程结束时调用destructor()函数,释放分配的缓冲区。
返回值:成功返回0,否则返回错误编号。
说明: 系统首先会返回给我们一个Key结构数组中第一个“未被使用”的键(即索引值),每个线程可以随后通过该键找到对
应的位置,并且为这个位置存储一个值(指针)。 一般来说,这个指针通常是每个线程通过调用malloc来获得的。
2)、int pthread_setspecific(pthread_key_t key, const void *value);
返回值:成功返回0,否则返回错误编号。说明:该函数将value指针的值与key相关联。
3)、void *pthread_getspecific(pthread_key_t key);
返回值:线程私有数据地址,若没有值与key关联则返回NULL。说明:通过该函数得到与key相关联的数据。
4)、int pthread_delete(pthread_key_t *keyp)
说明:注意调用pthread_delete不会激活与键关联的析构函数,容易造成内存泄露。当删除线程私有数据键的时候,不会影响
任何线程对该键设置的线程私有数据值,甚至不影响调用线程当前键值,所以容易造成内存泄露,如果你不记得释放所有线程内与
该键相关联的私有数据空间的话,使用已经删除的私有数据键将导致未定义的行为。
编程建议:最后不删除线程私有数据键!!!尤其当一些线程仍然持有该键的值时,就更不该释放该键!
5)、在线程中使用线程的特定数据
假设一个进程被启动,并且多个线程被创建。 其中一个线程调用pthread_key_create。系统在Key结构数组中找到第1个未使用
的元素。并把它的索引(0~127)返回给调用者。我们假设找到的索引为1 (我们会使用pthread_once 函数确保pthread_key_create
只被调用一次,这个在以后会讲到)。之后线程调用pthread_getspecific获取本线程的pkey[1] 的值,返回值是一个空值,线程那么
调用malloc分配内存区并初始化此内存区。之后线程调用pthread_setspecific把对应的所创建键的线程特定数据指针(pkey[1]) 设置为
指向它刚刚分配的内存区。下图指出了此时的情形。
当一个线程终止时,系统将扫描该线程的pkey数组,为每个非空的pkey指针调用相应的析构函数。 相应的析构函数是存放在图
1中的Key数组中的函数指针。这是一个线程终止时其线程特定数据的释放手段。
参:http://blog.csdn.net/caigen1988/article/details/7901248
私有数据函数编程应用:(转)
#include <stdio.h>
#include <string.h>
#include <pthread.h>
pthread_key_t key:
void *thread2(void *arg)//由thread1创建
{
int tsd = 5;
printf("thread %d is running.\n", pthread_self());
pthread_setspecific(key, (void *)tsd);
printf("thread %d returns %d\n", pthread_self(), pthread_getspecific(key));
}
void *thread1(void *arg)
{
int tsd = 0;
pthread_t thid2;
printf("thread %d is running.\n", pthread_self());
pthread_setspecific(key, (void *)tsd);
pthread_create(&thid2, NULL, thread2, NULL);
sleep(5);
printf("thread %d returns %d\n", pthread_self(), pthread_getspecific(key));
}
int main()
{
pthread_t thid1;
printf("main running\n");
pthread_key_create(&key, NULL);
pthread_create(&thid1, NULL, thread1, NULL);
sleep(5);
pthread_key_delete(key);
printf(main exit!);
return 0;
}
两个线程tsd修改互不干扰。
5、线程同步(结合程序看看,源:http://blog.csdn.net/zsf8701/article/details/7844316)
线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。
一、互斥锁(mutex)
通过锁机制实现线程间的同步。
- 初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化。
静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态分配:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);//mutexattr表示互斥锁的属性,为NULL则为默认属性,属性有:普通锁、嵌套锁、检错锁、适应锁(解锁后重新竞争)。 - 加锁。对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞等待,直到互斥量被解锁。
int pthread_mutex_lock(pthread_mutex *mutex);//若mutex已经被锁住,尝试加锁的线程会阻塞,直到其他线程释放。
int pthread_mutex_trylock(pthread_mutex_t *mutex);//若已被锁,它立即返回,错误码为EBUSY,而不是阻塞等待。
- 解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);//条件:1是必须处于加锁状态。2是调用本函数的线程必须是给互斥锁加锁的线程。
- 销毁锁。锁在是使用完成后,需要进行销毁以释放资源。
int pthread_mutex_destroy(pthread_mutex *mutex);//清楚锁时要求处于开放状态,若处于锁定状态,函数返回EBUSY。
二、条件变量(cond)
互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分为两部分: 条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
- 初始化条件变量。
静态态初始化,pthread_cond_t cond = PTHREAD_COND_INITIALIER;
动态初始化,int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
- 等待条件成立。释放锁,同时阻塞等待条件变量为真才行。timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
- 激活条件变量。pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有线程的阻塞
- 清除条件变量。无线程等待,否则返回EBUSY
int pthread_cond_destroy(pthread_cond_t *cond);
三、信号量(sem)
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。
- 信号量初始化。
int sem_init (sem_t *sem , int pshared, unsigned int value);
这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。
- 等待信号量。给信号量减1,然后等待直到信号量的值大于0。
int sem_wait(sem_t *sem);
- 释放信号量。信号量值加1。并通知其他等待线程。
int sem_post(sem_t *sem);
- 销毁信号量。我们用完信号量后都它进行清理。归还占有的一切资源。
int sem_destroy(sem_t *sem);