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\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 <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 \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 <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\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 <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", 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 <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

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;
}

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

  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\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 <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('\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

 

 

  

 
posted @ 2018-08-14 23:02  FREMONT  阅读(2032)  评论(0编辑  收藏  举报