十、linux应用编程之八:线程
线程是包含在进程内部的顺序执行流,是进程中的实际运作单位,也是操作系统能够进行调度的最小单位。一个进程中可以并发多条线程,每条线程并行执行不同的任务。
简单来说,进程是由线程组成的,线程是系统调度的最小单位,进程是拥有资源的基本单位而线程共享进程的资源。
线程的内容有点复杂,分线程创建与终止,连接与分离,线程属性,互斥量,条件变量五部分做笔记。
1.线程创建与终止
1) 函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
输入参数:thread,线程ID,实际上是个结构体。attr,属性对象,暂时先置为NULL。start_routine, 函数指针,指向线程执行函数。arg,线程执行函数的输入参数。
返 回 值:成功返回0,失败返回一个非零错误码。
示 例:rc = pthread_create(&threadID, NULL, PrintHello, (void *)t);
2) 函数原型:void pthread_exit(void *retval);
输入参数:retval,线程终止时向主线程返回的参数。
返 回 值:无
示 例:pthread_exit(0);
3) 伪代码:void* 执行函数{... pthread_exit(NULL);}
main{
pthread_create(&线程ID, NULL, 执行函数, (void*)参数);
pthread_exit(NULL); //一定不要忘了这里要退出主线程
}
4) 代码示例:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #define NUM_THREAD 5 void* PrintHello(void* threadid){ long id; id = (long)threadid; printf("Thread#%ld : Hello World! It's me, thread #%ld\n", id, id); pthread_exit(NULL); } int main(){ pthread_t threads[NUM_THREAD]; int rc; long i; for(i=0; i<NUM_THREAD; i++){ printf("main : creating thread %ld\n", i); rc = pthread_create(&threads[i], NULL, PrintHello, (void*)i); if(rc){ fprintf(stderr,"create thread #%ld error\n",i); exit(-1); } } printf("main : main exit!\n"); pthread_exit(NULL); return 0; }
运行结果:
2.连接与分离
线程分为分离线程和非分离线程,分离线程在退出时会立即释放系统资源并返回,而非分离线程在退出时不会立即释放资源,需要另一个线程为它调用pthread_join()函数。这样的意义是:当线程A退出时,为其调用join函数的线程B可以获得线程A的返回值。但是这样会导致一个问题,如果没有一个线程A为线程B调用join函数,那线程B就一直无法退出,也就无法释放资源,只能等到进程终止时,线程B才能退出。对于长时间运作的程序来说,完成工作的线程长期不退出会导致占用资源过多的现象。因此,对于长时间运行的程序,最好将线程设置为分离线程或者为线程调用join函数,当然,设置为分离线程的线程将无法返回值。
1) 函数原型:int pthread_detach(pthread_t thread);
输入参数:thread,线程ID,由变量定义以及create函数得到。
返 回 值:int,成功返回0,失败返回非零错误码。
示 例:pthread_detach(pthread_self());
2) 函数原型:int pthread_join(pthread_t thread, void **retval);
输入参数:thread,线程ID。retval,返回值存放地址。
返 回 值:int,成功返回0,失败返回非零错误码。
示 例:void *status; rc = pthread_join(threadID, &status);
说 明:viod **retval作为要被终止线程返回值的存放地址,这里用了&(void*)status,但是实 际上要检查或者引用这个返回值时,可以直接用(long)status来引用,保留疑问,待解 决。因为执行函数的返回值类 型为void*,也就是说void*类型才是有效数据,又因为 C语言中没有引用类型,因而要通过参数获得函数返回值只能通过指针,所以出现了 void** retval。
3) 伪代码:void* 执行函数(void *参数){...pthread_exit(要返回的参数);}
main(){
void* status;
rc = pthread_create(&线程ID, NULL, 执行函数, (void*)要输入的参数);
rc = pthread_join(线程ID, &status); //用status时,用(long)status即可。
}
4) 代码示例:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> void* PrintHello(void* threadid){ long id = (long)threadid; printf("thread #%ld : it is #%ld ,hello!!!\n", id, id); pthread_exit((void*)5); } int main(){ pthread_t threadID; int rc; void* status; rc = pthread_create(&threadID, NULL, PrintHello, (void*)1); if(rc){ perror("create thread error\n"); exit(-1); } rc = pthread_join(threadID, &status); if(rc){ perror("join thread error\n"); exit(-1); } printf("main thread : return value = %ld\n", (long)status); pthread_exit(NULL); return 0; }
运行结果:
3.线程属性
之前使用create()函数时,第二个参数属性对象att总是使用NULL,其实是有实际作用的。线程是有属性的,上文说到的分离线程和非分离线程就是线程的属性之一,称为线程状态。线程的基本属性包括:线程状态,调整策略和栈大小。
1) 属性对象arr
初始化属性对象:int pthread_attr_init(pthread_attr_t *attr);
arrt是属性对象,成功返回0,失败返回非零错误码。
销毁属性对象:int pthread_attr_destroy(pthread_attr_t *attr);
arrt是属性对象,成功返回0,失败返回非零错误码。
2) 线程状态
获取线程状态:int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
arrt为属性对象,detachstate是所获取状态值的指针。
设置线程状态:int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
arrt为属性对象,detachstate是要设置的状态值。
3) 线程栈
获取线程栈:int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
arrt为属性对象,stacksize 是保存所获取栈大小的指针。
设置线程栈:int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
arrt为属性对象,stacksize 是要设置的栈大小。
4) 属性对象使用伪代码:
int main(){
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_getstacksize(&attr, stacksize);
pthread_create(&thread, &attr, 执行函数, (void *)arg);
pthread_attr_destroy(&attr);
}
5) 代码实例:代码中涉及到getopt(),calloc(),strdup()等函数需要注意。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> #include <ctype.h> //extern char* optarg; //extern int optind; //extern int opterr; //extern int optopt; struct thread_info{ pthread_t pthreadID; int number_id; char* argv_string; }; void* fun(void* var){ char* p; struct thread_info* thread_point = var; static char* uargv; printf("Thread #%d : top of stack near %p; argv_string = %s\n", thread_point->number_id, &p, thread_point->argv_string); uargv = strdup(thread_point->argv_string); p = uargv; while(*p != '\0'){ *p = toupper(*p); p++; } return(uargv); } int main(int argc, char* argv[]){ int ch, thread_num; int stack_size = -1; int ret; pthread_attr_t attr; void* status; struct thread_info* threads = NULL; while((ch=getopt(argc, argv, "s:")) != -1){ switch(ch){ case 's': printf("HAVE option : -s\n"); stack_size = strtoul(optarg, NULL, 0); printf("thread's stack size = %d\n", stack_size); break; default: fprintf(stderr, "Usage:%s [-s stack-size] arg...\n", argv[0]); exit(-1); break; } } thread_num = argc - optind; printf("thread_num = %d\n", thread_num); ret = pthread_attr_init(&attr); if(ret){ perror("init attr error\n"); exit(-1); } if(stack_size > 0){ ret = pthread_attr_setstacksize(&attr, stack_size); if(ret){ perror("set stack size error\n"); exit(-1); } } else{ perror("stack size < 0\n"); exit(-1); } threads = calloc(thread_num, sizeof(struct thread_info)); if(threads == NULL){ perror("calloc error\n"); exit(-1); } for(int i=0; i<thread_num; i++){ threads[i].number_id = i; threads[i].argv_string = argv[optind + i]; ret = pthread_create(&threads[i].pthreadID, &attr, fun, (void*)&threads[i]); if(ret){ perror("create thread error\n"); exit(-1); } } pthread_attr_destroy(&attr); for(int i=0; i<thread_num; i++){ ret = pthread_join(threads[i].pthreadID, &status); if(ret){ perror("join thread error\n"); exit(-1); } printf("Join with thread #%d; return value was %s \n", threads[i].number_id, (char*)status); } pthread_exit(NULL); free(threads); return 0; }
代码稍微有点长,实际上是有一条脉络下来的。首先代码的目的是:在运行程序时用-s选项输入线程堆栈的大小,接着输入其他字符串参数。然后输出结果是字符串参数的大写形式。要实现这个目标首先一点就是要创建线程create(),创建线程需要线程ID,线程属性对象,线程执行函数,执行函数输入参数。线程ID通过变量声明得到,线程属性对象也是变量声明+初始化得到,但是要修改栈大小。栈大小从-s选项来,因而需要getopt()函数解析选项。得到栈大小之后需要调用pthread_attr_setstacksize()函数设置栈大小。属性对象得到之后,接下来就是编写执行函数,获取执行函数的输入参数。这样整个代码就写完了。
需要注意的是,在调用pthread_attr_setstacksize()函数设置栈大小之前,必须先初始化属性对象。-s输入栈大小时,栈大小不能小于16K。
运行结果:
4.互斥量
程序中有一些代码一次只能由一个线程访问,因此要对这些代码进行保护,这些代码叫临界区代码,临界区代码必须以互斥的方式访问。互斥量(也叫互斥锁),是一种用来保护临界区的特殊变量,有锁定和解锁两种状态,当互斥量处于锁定状态时,说明某个线程正在持有这个互斥量,其他线程想要对这个互斥量加锁的时候,将会阻塞,直到持有互斥量的线程解锁这个互斥量。互斥量常用来做临界区保护和代码同步。
1) 互斥量创建与销毁
创建方式①:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;(常用)
创建方式②:pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t *restrict mutex, constpthread_mutexattr_t *restrict attr);
销毁互斥量:int pthread_mutex_destroy(pthread_mutex_t *mutex);
2) 互斥量加锁和解锁
加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);(尝试加锁,无法加锁也不会阻塞)
解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);
3) 互斥量使用伪代码:
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mylock);
临界区代码
pthread_mutex_unlock(&mylock);
4) 代码示例:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <pthread.h> 5 6 pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER; 7 8 void* doPrint(void* var){ 9 10 long id = (long)var; 11 12 pthread_mutex_lock(&mylock); 13 for(int i=0; i<5; i++){ 14 printf("thread #%ld : it's me #%ld!!\n", id, id); 15 usleep(100); 16 } 17 pthread_mutex_unlock(&mylock); 18 pthread_exit(NULL); 19 } 20 21 int main(){ 22 23 pthread_t pthreadID[3]; 24 int ret; 25 for(long i=0; i<3; i++){ 26 ret = pthread_create(&pthreadID[i], NULL, doPrint, (void*)i); 27 if(ret){ 28 perror("create thread error\n"); 29 exit(-1); 30 } 31 } 32 pthread_exit(NULL); 33 return 0; 34 }
使用了互斥锁后,每个线程都执行结束之后再执行下一个线程,输出是整齐的,如下图:
假设将代码的第12行和第17行注释掉,输出变得参差不齐了,如下:
5) 死锁现象以及避免死锁
死锁现象指的是:两个或两个以上的线程在执行过程中,因争夺资源而造成互相等待的情况。例如,线程A已经对互斥锁a加锁,而且想要对互斥锁b加锁。线程B已经对互斥锁b加锁,而且想要对互斥锁a加锁。那就会导致,线程A阻塞等待线程B解锁互斥锁b,线程B阻塞等待线程A解锁互斥锁a,但是两个线程都已经阻塞,无法解锁任何一个互斥锁,所以线程A和线程B就死锁了。
避免死锁:加锁应按照一定的顺序进行加锁。比如上面的例子中,加锁顺序为互斥锁a->互斥锁b。线程A已经对互斥锁a加锁,想要对互斥锁b加锁,这符合加锁顺序。而线程B此时不应该处于已经对互斥锁b加锁,而想要对互斥锁a加锁的状态,这是不符合加锁顺序的。正确的加锁顺序应该是先对互斥锁a加锁,再对互斥锁b加锁。
5.条件变量
条件变量也是程序同步的一种。与互斥锁不同,条件变量是通过 等待-通知 的方式进行同步的,同步方式比较高效。实际上使用条件变量要有互斥锁的前提。
1) 创建和销毁
静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;(常用)
动态初始化:pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
销毁:int pthread_cond_destroy(pthread_cond_t *cond);
2) 等待与通知
等待: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 abstime);(阻塞一段时间后继续运行)
通知:int pthread_cond_signal(pthread_cond_t *cond);(唤醒一个等待线程)
int pthread_cond_broadcast(pthread_cond_t *cond);(唤醒所有等待线程)
3) 条件变量使用伪代码:
线程A(等待) | 线程B(通知) |
pthread_mutex_lock(&mutex) while(a < b) pthread_cond_wait(&cond, &mutex) pthread_mutex_unlock(&mutex) | if(a<b) pthread_cond_signal(&cond) |
4) 代码示例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t mycond = PTHREAD_COND_INITIALIZER; long sum = 0; void* fun12(void* var){ long id = (long)var; for(int i=0; i<60; i++){ usleep(1); pthread_mutex_lock(&mylock); sum++; printf("thread #%ld : sum + 1, \t sum = %ld\n", id, sum); pthread_mutex_unlock(&mylock); if(sum>100){ pthread_cond_signal(&mycond); } } pthread_exit(NULL); } void* fun3(void* var){ long id = (long)var; pthread_mutex_lock(&mylock); while(sum < 100) pthread_cond_wait(&mycond, &mylock); sum = 0; printf("thread #%ld : sum alread > 100, \t clear sum\n", id); pthread_mutex_unlock(&mylock); pthread_exit(NULL); } int main(){ pthread_t pthreadID[3]; int ret; for(long i=0;i<3;i++){ if(i<2) ret = pthread_create(&pthreadID[i], NULL, fun12, (void*)i); else ret = pthread_create(&pthreadID[i], NULL, fun3, (void*)i); if(ret){ perror("create thread error!!\n"); exit(-1); } } pthread_exit(NULL); return 0; }
线程0和线程1对sum进行+1,并且检测sum是否超过100,如果超过100即通知线程3继续运行。运行结果如下:
这样做的好处是,线程3只需要运行1次,而不用反复运行线程3检测sum是否超过100