linux线程基础篇 您所在的位置:网站首页 linux的线程状态 linux线程基础篇

linux线程基础篇

#linux线程基础篇| 来源: 网络整理| 查看: 265

linux编程--线程

一、线程概念

  1.什么是线程

    LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)

         进程:独立地址空间,拥有PCB

         线程:也有PCB,但没有独立的地址空间(共享)

         区别:在于是否共享地址空间。 独居(进程);合租(线程)。                                                             

 

         Linux下: 线程:最小的执行单位

                         进程:最小分配资源单位,可看成是只有一个线程的进程。

          

  2.线程共享资源与非共享资源

  共享资源:                                                                   

                     1.文件描述符表

                     2.每种信号的处理方式

                     3.当前工作目录

                     4.用户ID和组ID

                     5.内存地址空间 (.text/.data/.bss/heap/共享库)

  非共享资源:

      1.线程id

              2.处理器现场和栈指针(内核栈)

              3.独立的栈空间(用户空间栈)(自己的栈空间)

           4.errno变量

              5.信号屏蔽字

            6.调度优先级

  3.线程优缺点

    优点:     1. 提高程序并发性        2. 开销小        3. 数据通信、共享数据方便

         缺点:     1. 库函数,不稳定        2. 调试、编写困难、gdb不支持         3. 对信号支持不好

         优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

 

二、线程控制函数 

   1.pthread_self()函数

  获取线程ID。其作用对应进程中 getpid() 函数。

  函数原型:pthread_t pthread_self(void);          返回值:成功:0;       失败:无

  线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现

       线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)

       注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。

  在主控线程中可以使用tid获得线程ID。

   2.pthread_create()函数  

  创建一个新线程。其作用,对应进程中fork() 函数。

  函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

  返回值:成功:0;失败:错误号 -----Linux环境下,所有线程特点,失败均直接返回错误号,但不设置errno,所以无法使用perror

       函数打印错误信息,需要使用strerror函数将返回的错误号转换成错误信息后再打印。

  参数:

    1. thread:传出参数,保存系统为我们分配好的线程ID

           2. attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数,后面线程属性会详细讲解。

           3. start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。

         4. arg:传递给线程主函数执行期间所使用的参数,回调函数。

  示例代码:循环创建N个子线程,每个线程打印自己是第几个被创建的线程。(类似于进程循环创建子进程)   

#include #include #include #include void *tfn(void *arg) { int i; i = (int)arg; sleep(i); //通过i来区别每个线程 printf("I'm %dth thread, Thread_ID = %lu\n", i+1, pthread_self()); return NULL; } int main(int argc, char *argv[]) { int n = 5, i; pthread_t tid; if (argc == 2) n = atoi(argv[1]); for (i = 0; i < n; i++) { pthread_create(&tid, NULL, tfn, (void *)i); //将i转换为指针,在tfn中再强转回整形。 } sleep(n); printf("I am main, and I am not a process, I'm a thread!\n" "main_thread_ID = %lu\n", pthread_self()); pthread_exit(NULL); }

   思考:将pthread_create函数参4修改为(void *)&i, 将线程主函数内改为 i=*((int *)arg) 是否可以。

   答案:不可以,值传递与地址传递的区别,如果使用i=*((int *)arg),就是地址传递,直接操作读取主控线程中的i值,

        而主控线程中的i值在for循环中一直自增,即第一个子线程创建出来后,i已经自增,在子线程中读取的是自增后的值,

        所以是从2开始打印的。     

    3.pthread_exit()函数

   将单个线程退出,类似于进程中的exit()

  函数原型:void pthread_exit(void *retval);     

       参数:retval表示线程退出状态,通常传NULL,当retval不为NULL时,其值由pthread_join函数接收,后续会讲到

  思考:使用exit将指定线程退出,可以吗?          

    结论:多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit

       导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。pthread_exit或者return返回的指针所指向的内存单元

       必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

  总结:

             return:返回到调用者那里去,在主线程使用return会将整个进程退出。

                pthread_exit():将调用该函数的线程退出

              (在主控线程使用只是将线程退出,而不是整个进程退出,在主线程中使用return会将整个进程退出,

                 导致子线程来不及执行就结束了, 在主线程尽量不要要exit和return)              

                exit: 将进程退出,在线程中使用exit会导致整个进程退出,导致其他线程未执行,

     所以在多线程编程统一使用pthread_exit()函数。

   4.pthread_join()函数

  阻塞等待线程退出,获取线程退出状态 , 其作用对应进程中 waitpid() 函数。

  函数原型:int pthread_join(pthread_t thread, void **retval);

  返回值:成功:0;失败:错误号

  参数:thread:线程ID (【注意】:不是指针);retval:存储线程结束状态(pthread_exit(void*)retval)的值)。

  对比记忆:

                 进程中:main返回值、exit参数-->int;等待子进程结束 waitpid 函数参数-->int * status

                 线程中:线程主函数返回值、pthread_exit-->void *;等待线程结束 pthread_join 函数参数-->void **retval

  示例代码:参数 retval 非空用法,使用pthread_join回收pthread_exit()的退出值  

#include #include #include #include typedef struct{ int a; int b; } exit_t; void *tfn(void *arg) { exit_t *ret; ret = malloc(sizeof(exit_t)); ret->a = 100; ret->b = 300; pthread_exit((void *)ret); return NULL; //should not be here. } int main(void) { pthread_t tid; exit_t *retval; pthread_create(&tid, NULL, tfn, NULL); /*调用pthread_join可以获取线程的退出状态*/ pthread_join(tid, (void **)&retval); printf("a = %d, b = %d \n", retval->a, retval->b); return 0; }

  总结:调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join

       得到的终止状态是不同的,总结如下:

          1.如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。

       2.如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。

         3.如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。

       4.如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

                    阻塞等待回收,防止僵尸线程;

   示例代码:使用pthread_join函数将循环创建的多个子线程回收。

#include #include #include #include int var = 100; void *tfn(void *arg) { int i; i = (int)arg; sleep(i); if (i == 1) { //i = 0 100 333 333 777 777 var = 333; printf("var = %d\n", var); pthread_exit((void *)var); } else if (i == 3) { var = 777; printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var); pthread_exit((void *)var); } else { printf("I'm %dth pthread, pthread_id = %lu\n var = %d\n", i+1, pthread_self(), var); pthread_exit((void *)var); } return NULL; } int main(void) { pthread_t tid[5]; int i, *ret[5]; for (i = 0; i < 5; i++) pthread_create(&tid[i], NULL, tfn, (void *)i); for (i = 0; i < 5; i++) { pthread_join(tid[i], (void **)&ret[i]); printf("-------%d 's ret = %d\n", i, (int)ret[i]); } printf("I'm main pthread tid = %lu\t var = %d\n", pthread_self(), var); pthread_exit(NULL); }

  5.pthread_detach()函数

  实现线程分离,

       线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。

  网络、多线程服务器常用。

       进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,

  导致内核认为该进程仍存在。也可使用 pthread_create函数参2(线程属性)来设置线程分离,后续在线程属性中详细讲解。

  函数原型:int pthread_detach(pthread_t thread); 

  返回值: 成功:0;失败:错误号

  示例代码:使用pthread_detach函数实现线程分离      

#include #include #include #include #include void *tfn(void *arg) { int n = 3; while (n--) { printf("thread count %d\n", n); sleep(1); } return (void *)1; } int main(void) { pthread_t tid; void *tret; int err; #if 1 pthread_attr_t attr; /*通过线程属性来设置游离态,必须在在线程创建前设置属性*/ pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_create(&tid, &attr, tfn, NULL); pthread_attr_destroy(&attr); #else pthread_create(&tid, NULL, tfn, NULL); pthread_detach(tid); #endif while (1) { err = pthread_join(tid, &tret); if (err != 0) fprintf(stderr, "thread %s\n", strerror(err)); else fprintf(stderr, "thread exit code %d\n", (int)tret); sleep(1); } return 0; }

 

  总结:1.一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以

                 被置为detach状态,这样的线程一旦终止就立刻回收它占用的  所有资源而不保留终止状态。

                 2.不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。

                 如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

  6.pthread_cancel()函数

   杀死(取消)线程 ,其作用对应进程中 kill() 函数。

   函数原型:int pthread_cancel(pthread_t thread);  

  返回值: 成功:0;失败:错误号

  注意事项:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

  类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,

      必须要到达取消点。

 

  取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,

  write.....                                                                                                                                      ——参 APUE.12.7 取消选项。

  可粗略认为一个系统调用(进入内核)即为一个取消点。

  若线程主体函数长时间不会调用系统调用函数,可以自己手动添加取消点:pthread_testcancel(void);     //自己添加取消点

  扩展:

  设置线程可取消和不可取消状态函数:int pthread_setcancelstate(int  state,int *oldstate);

  函数参数:state:分离属性,可分离或者不可分离,可以设置的参数为PTHREAD_CANCEL_ENABLE或者

       PTHREAD_CANCEL_DISABLE

       oldstate:函数将当前可取消状态设置为state,而将原来的可取消状态保存在oldstate所指向的内存空间,

                        这两步是原子操作, 不可分割

  我们所默认的取消为推迟取消,即调用pthread_cancel函数后,在线程没有到达取消点之前,并不会取消。

       可以通过调用pthread_setcreatetype()来修改取消类型

  函数原型:pthread_setcanceltype(int type,int *oldtype);

  参数:type:参数可以设置为PTHREADCANCEL_DEFERRED或者PTHREADCANCEL_ASYNCHRONOUS,

     推迟取消或者异步取消

     oldtype:保存原来的状态

     使用异步取消时,可以在任意时间撤销而不需要非要遇到取消点

  示例代码:终止线程的三种方法。注意“取消点”的概念。return,pthread_exit,pthread_cancel

#include #include #include #include void *tfn1(void *arg) { printf("thread 1 returning\n"); return (void *)111; } void *tfn2(void *arg) { printf("thread 2 exiting\n"); pthread_exit((void *)222); } void *tfn3(void *arg) { while (1) { // printf("thread 3: I'm going to die 1 seconds after...\n"); // sleep(1); pthread_testcancel(); //自己添加取消点 } } int main(void) { pthread_t tid; void *tret = NULL; pthread_create(&tid, NULL, tfn1, NULL); pthread_join(tid, &tret); printf("thread 1 exit code = %d\n\n", (int)tret); pthread_create(&tid, NULL, tfn2, NULL); pthread_join(tid, &tret); printf("thread 2 exit code = %d\n\n", (int)tret); pthread_create(&tid, NULL, tfn3, NULL); sleep(3); pthread_cancel(tid); pthread_join(tid, &tret); printf("thread 3 exit code = %d\n", (int)tret); return 0; }

  总结:终止某个线程而不终止整个进程,有三种方法:

从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。 线程可以调用pthread_exit终止自己。

  7.线程与共享

   【牢记】:线程默认共享数据段、代码段等地址空间,常用的是全局变量。而进程不共享全局变量,只能借助共享内存。

    示例代码:设计程序,验证线程之间共享全局数据。  

#include #include #include #include int var = 100; void *tfn(void *arg) { var = 200; printf("thread\n"); return NULL; } int main(void) { printf("At first var = %d\n", var); pthread_t tid; pthread_create(&tid, NULL, tfn, NULL); sleep(1); printf("after pthread_create, var = %d\n", var); return 0; }

 8.最终总结,控制函数对比 

     进程                            线程

      fork                     pthread_create

      exit                      pthread_exit

      waitpid                pthread_join

      kill                       pthread_cancel

      getpid                 pthread_self             命名空间

 

  综合练习:多线程拷贝

  

#include #include #include #include #include #include #include #define T_NUM 5 //默认线程数5 #define ITEMS 66 //'='的个数 void err_sys(void *str) { perror(str); exit(1); } void err_usr(char *str) { fputs(str, stderr); exit(1); } /*每个线程都对应如下3个属性*/ typedef struct { int off; //拷贝的起始位置 int size; //拷贝的长度 int t_no; //自己是第几个被创建的线程 } arg_t; char *s, *d; int *done; //为一个数组,每个元素记录每个线程完成任务字节数 int n = T_NUM; void *tfn(void *arg) { arg_t *arg_p; int i; char *p, *q; arg_p = (arg_t *)arg; //每个线程自己的结构体 arg{off, size, t_no} p = s + arg_p->off; //当前线程执行拷贝任务,在原文件中的起始位置 q = d + arg_p->off; //目标文件的起始位置 //每个线程按字节拷贝自己的任务,并将拷贝字节数写入字节对应的done数组中 for (i = 0; i < arg_p->size; i++) { *q++ = *p++; done[arg_p->t_no]++; usleep(100); } return NULL; } void *display(void *arg) { int size, interval, draw, sum, i, j; size = (int)arg; //文件总大小 interval = size / ITEMS; //每个'='所代表的字节数 draw = 0; //画出的'='的个数 while (draw draw; draw++) { //输出与线程拷贝字节数相对应个数的'=' putchar('='); fflush(stdout); } } putchar('\n'); return NULL; } int main(int argc, char *argv[]) { int src, dst, i, len, off; struct stat statbuf; pthread_t *tid; arg_t *arr; if (argc != 3 && argc != 4) err_usr("usage : cp src dst [thread_no]\n"); if (argc == 4) n = atoi(argv[3]); //用户指定线程数,默认值5 src = open(argv[1], O_RDONLY); if (src == -1) err_sys("fail to open"); dst = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644); if (dst == -1) err_sys("fail to open"); if (fstat(src, &statbuf) == -1) err_sys("fail to stat"); ftruncate(dst, statbuf.st_size); //源文件映射区首地址,保存在 全局变量s中,转换为指针方便定位操作,否则对文件只能使用lseek定位 s = (char *)mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, src, 0); if (s == MAP_FAILED) err_sys("fail to mmap"); //目标文件映射区首地址,保存在 全局变量d中 d = (char *)mmap(NULL, statbuf.st_size, PROT_WRITE , MAP_SHARED, dst, 0); if (d == MAP_FAILED) err_sys("fail to mmap"); close(src); close(dst); //pthread_t tid[n+1]; 线程ID数组分配空间 tid = (pthread_t *)malloc(sizeof(pthread_t) * (n+1)); if (tid == NULL) err_sys("fail to malloc"); //int done[n] 为int *done分配空间 done = (int *)calloc(sizeof(int), n); if (done == NULL) err_sys("fail to calloc"); //arr[n] 为结构体数据arr分配空间 arr = (arg_t *)malloc(sizeof(arg_t) * n); if (arr == NULL) err_sys("fail to malloc"); //计算每个线程应拷贝字节数, 起始偏移位置归零 len = statbuf.st_size / n, off = 0; //计算出每个线程拷贝的起始地址, 对应i写入结构体数组arr for (i = 0; i < n; i++, off += len) arr[i].off = off, arr[i].size = len, arr[i].t_no = i; //调整下最后一个线程拷贝的字节个数 arr[n-1].size += (statbuf.st_size % n); //创建拷贝线程, 对应i将每个线程arr[i]中记录的应拷贝的任务,传入线程主函数 for(i = 0; i < n; i++) pthread_create(&tid[i], NULL, tfn, (void *)&arr[i]); //创建进度线程, 传入文件总大小 pthread_create(&tid[n], NULL, display, (void *)statbuf.st_size); //回收子线程 for(i = 0; i < n+1; i++) pthread_join(tid[i], NULL); munmap(s, statbuf.st_size); munmap(d, statbuf.st_size); free(tid); free(done); free(arr); return 0; } mult_pthread_cp.c

 

 

  

 


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有