zoukankan      html  css  js  c++  java
  • linux线程基础篇----线程函数

    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 <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    void *tfn(void *arg)
    {
        int i;
    
        i = (int)arg;
        sleep(i);     //通过i来区别每个线程
        printf("I'm %dth thread, Thread_ID = %lu
    ", 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!
    " 
                "main_thread_ID = %lu
    ", 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 <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <stdlib.h>
    
    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 
    ", 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 <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <pthread.h>
    
    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
    ", var);
            pthread_exit((void *)var);
    
        } else  if (i == 3) {
            var = 777;
            printf("I'm %dth pthread, pthread_id = %lu
     var = %d
    ", i+1, pthread_self(), var);
            pthread_exit((void *)var);
    
        } else  {
    
            printf("I'm %dth pthread, pthread_id = %lu
     var = %d
    ", 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
    ", i, (int)ret[i]);
        }
        printf("I'm main pthread tid = %lu	 var = %d
    ", pthread_self(), var);
    
        pthread_exit(NULL);
    }

      5.pthread_detach()函数

      实现线程分离,

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

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

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

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

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

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

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

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <string.h>
    
    void *tfn(void *arg)
    {
        int n = 3;
        while (n--) {
            printf("thread count %d
    ", 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
    ", strerror(err));
            else
                fprintf(stderr, "thread exit code %d
    ", (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 <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    #include <stdlib.h>
    
    void *tfn1(void *arg)
    {
        printf("thread 1 returning
    ");
        return (void *)111; 
    }
    
    void *tfn2(void *arg)
    {
        printf("thread 2 exiting
    ");
        pthread_exit((void *)222);
    }
    
    void *tfn3(void *arg)
    {
        while (1) {
    //        printf("thread 3: I'm going to die 1 seconds after...
    ");
    //        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
    
    ", (int)tret);
    
        pthread_create(&tid, NULL, tfn2, NULL);
        pthread_join(tid, &tret);
        printf("thread 2 exit code = %d
    
    ", (int)tret);
    
        pthread_create(&tid, NULL, tfn3, NULL);
        sleep(3);
        pthread_cancel(tid);
        pthread_join(tid, &tret);
        printf("thread 3 exit code = %d
    ", (int)tret);
    
        return 0;
    }

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

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

      7.线程与共享

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

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

    #include <stdio.h>
    #include <pthread.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int var = 100;
    
    void *tfn(void *arg)
    {
        var = 200;
        printf("thread
    ");
    
        return NULL;
    }
    
    int main(void)
    {
        printf("At first var = %d
    ", var);
    
        pthread_t tid;
        pthread_create(&tid, NULL, tfn, NULL);
        sleep(1);
    
        printf("after pthread_create, var = %d
    ", var);
    
        return 0;
    }

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

         进程                            线程

          fork                     pthread_create

          exit                      pthread_exit

          waitpid                pthread_join

          kill                       pthread_cancel

          getpid                 pthread_self             命名空间

      综合练习:多线程拷贝

      

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/mman.h>
    
    #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 < ITEMS) {
            for (i = 0, sum = 0; i < n; i++)    //借助done数组获取当前已经拷贝的总字节数
                sum += done[i];
    
            j = sum / interval;                 //计算到当前应该打印多少个'='
    
            for (; j > draw; draw++) {          //输出与线程拷贝字节数相对应个数的'='
                putchar('='); 
                fflush(stdout);
            }
        }
        putchar('
    ');
    
        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]
    ");
        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

      

     
  • 相关阅读:
    使用ServiceStackRedis链接Redis简介
    浅谈SQL SERVER中事务的ACID
    Sql Server查询性能优化之走出索引的误区
    Redis命令总结
    TSQL查询进阶—理解SQL Server中的锁
    SQL Server 2005 分区表实践——分区切换
    SQL Server Profiler 模板
    深入浅出SQL Server中的死锁
    不同的单元中的类可以共用同一个命名空间
    从硬盘上装xp手记(2005.8.14 )
  • 原文地址:https://www.cnblogs.com/FREMONT/p/9478590.html
Copyright © 2011-2022 走看看