一文解读Linux线程编程-线程原理、线程编程等等,带丰富的例子

目录

Linux 线程

各个 API 原型介绍

使用这些 API 的例程


Linux 线程

详细学习的地方,可以当字典备查:

多线程指的是在单个 程序/进程 中可以同时运行多个不同的线程(可以视作共享同一块内存资源的多个任务),执行不同的任务:

  • 更高的运行效率,并行执行;

  • 多线程是模块化的编程模型;

  • 与进程相比,线程的创建和切换开销更小;

  • 通信方便;

  • 能简化程序的结构,便于理解和维护;更高的资源利用率。

多线程的应用场景:

  1. 程序中出现需要等待的操作,比如网络操作、文件IO等,可以利用多线程充分使用处理器资源,而不会阻塞程序中其他任务的执行。

  2. 程序中出现可分解的大任务,比如耗时较长的计算任务,可以利用多线程来共同完成任务,缩短运算时间。

  3. 程序中出现需要后台运行的任务,比如一些监测任务、定时任务,可以利用多线程来完成。

线程安全与线程同步:

线程安全:多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。

线程不安全:不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。如果多个线程同时读写共享变量,会出现数据不一致的问题。

线程安全问题都是由全局变量及静态变量引起的

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,

线程异步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制。

同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去

异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待

线程同步的优势

好处:解决了线程的安全问题。

弊端:每次都有判断锁,降低了效率。

但是在安全与效率之间,首先考虑的是安全。

Linux 系统下的用 C 开发多线程使用叫 pthread 的线程库;内核级线程 和 用户级线程 是在创建线程时通过传入 API 的不同参数进行 区分/设置 的。

因为 pthread 并非 Linux 系统的默认库,而是 POSIX 标准的线程库。在 Linux 中将其作为一个库来使用,因此编译选项需要加上 -lpthread(或-pthread)以显式链接该库。例子:gcc xxx.c -lpthread -o xxx.bin

各个 API 原型介绍

  • pthread_self()——获取线程 ID。

    /* pthread_self()——函数获取线程 ID
        #include <pthread.h>
        pthread_t pthread_self(void);
        成功:返回线程号
    */
    #include <pthread.h>
    #include <stdio.h>
    int main()
    {
        pthread_t tid = pthread_self();
        printf("tid = %lu\n",(unsigned long)tid);
        return 0;
    }
  • pthread_create()——线程创建。

    /* pthread_create()——线程创建
        #include <pthread.h>
        int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
        该函数第一个参数为pthread_t指针,用来保存新建线程的线程号。
        第二个参数表示了线程的属性,可传入NULL表示默认属性。
        第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为void*,形参为void*。
        第四个参数则表示为向线程处理函数传入的参数,若不传入,可用NULL填充。
        返回 0 表示成功,负值表示失败。
    */
    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <errno.h>
    void *fun(void *arg) {    printf("pthread_New = %lu\n",(unsigned long)pthread_self());    }
    int main()
    {
        pthread_t tid1;
        int ret = pthread_create(&tid1,NULL,fun,NULL);
        ... 简化,错误处理略
        
        /* tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间 */
        /* 即 tid1 与 pthread_New 打印结果应为一致 */
        printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
        /* 因线程执行顺序随机,不加sleep可能导致主线程先执行,导致进程结束,无法执行到子线程 */
        /* 也就是说,主线程 执行到这里 如果不加 sleep 则 后面直接 return 结束了,那么 线程 fun 还没执行 本进程就结束了 */
        sleep(1);    return 0;
    }
    /*
    通过pthread_create确实可以创建出来线程,主线程中执行pthread_create后的tid指向了线程号空间,与子线程通过函数pthread_self打印出来的线程号一致。
    特别说明的是,当主线程伴随进程结束时,所创建出来的线程也会立即结束,不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一个线程会先运行。可以将上述代码中sleep函数进行注释,观察实验现象。
    */

    创建 进程时候 传入参数:

    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <errno.h>
    void *fun1(void *arg){    printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);    }
    void *fun2(void *arg){    printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);    }
    int main()
    {
        pthread_t tid1,tid2;    int a = 50;
        int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); /* 传入地址 */
        ... 简化,错误处理略
        ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a); /* 传入值 */
        ... 简化
        sleep(1);    printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);    return 0;
    }
  • pthread_exit / pthread_cancel 和 pthread_join / pthread_tryjoin_np——线程的退出。

    线程的退出情况有三种:

    • 第一种是进程结束,进程中所有的线程也会随之结束。

    • 第二种是通过函数 pthread_exit() 来主动的退出所在的线程。

    • 第三种被其他线程调用 pthread_cancel() 来被动退出。

    关于线程退出后的资源回收:

    一个进程中的多个线程是共享数据段的。如果一个线程是 joinable 或者叫 非分离状态的,在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,要用 pthread_join/pthread_tryjoin_np 函数来同步并释放资源,即 当线程结束后,主线程要通过函数 pthread_join/pthread_tryjoin_np 来回收线程的资源,并且获得线程结束后需要返回的数据。如果一个线程是 unjoinable 或者叫 分离状态的,则 在线程退出之后 其自己会主动回收资源,主线程里便不用再调用 pthread_join/pthread_tryjoin_np 来回收线程的资源,当然此时 线程退出的时候也就不能传出参数。joinable 和 unjoinable 可以设置,后面 线程属性 部分会说到。

    关于主线程 / 进程的退出:

    • 在主线程中,在 main 函数中 return 了或是调用了 exit() 函数,则主线程退出,且整个进程也会终止,此时进程中的所有线程也将终止,因此要避免 main 函数过早结束。

    • 在任何一个线程中调用 exit() 函数都会导致进程结束,进程一旦结束,那么进程中的所有线程都将结束。

    以下 是对 pthread_exit / pthread_cancel 和 pthread_join / pthread_tryjoin_np 线程的退出 相关 API 的说明。

    /*
    线程主动退出 pthread_exit
        #include <pthread.h>
        void pthread_exit(void *retval);
        pthread_exit函数为线程退出函数,在退出时候可以传递一个void*类型的数据带给主线程,若选择不传出数据,可将参数填充为NULL。
        pthread_exit函数唯一的参数value_ptr是函数的返回值,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr
    ​
    线程被动退出 pthread_cancel,其他线程使用该函数让另一个线程退出 
        #include <pthread.h>
        int pthread_cancel(pthread_t thread);
        成功:返回0
        该函数传入一个tid号,会强制退出该tid所指向的线程,若成功执行会返回0。
    ​
    线程资源回收(阻塞在执行到 pthread_join 的地方,然后等待 thread 线程的退出)
        #include <pthread.h>
        int pthread_join(pthread_t thread, void **retval);
        该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的tid号,第二个参数为线程回收后接受线程传出的数据,     或者该线程被取消而返回PTHREAD_CANCELED。
        
    线程资源回收(非阻塞,需要循环查询)
        #define _GNU_SOURCE            
        #include <pthread.h>
        int pthread_tryjoin_np(pthread_t thread, void **retval);
        该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回0,其余参数与pthread_join一致。
    ​
    阻塞方式 pthread_join 和 非阻塞方式 pthread_tryjoin_np 使用上的区别:
        通过函数 pthread_join 阻塞方式回收线程,几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。
        通过函数 pthread_tryjoin_np 使用非阻塞回收线程,可以根据退出先后顺序自由的进行资源的回收。
    */
  • 线程属性相关:

    参考 pthread_attr_init线程属性高司机的博客-CSDN博客pthread_attr_destroy,线程属性详解 线程属性pthread_attr_t简介_Robin Hu的专栏-CSDN博客

    /* 定义 pthread_attr_t 线程属性变量,用于设置线程属性,主要包括 scope 属性(用于区分用户态或者内核态)、detach(分离/joinable)属性、堆栈地址、堆栈大小、优先级 */
    pthread_attr_t attr_1,attr_2_3_4[3];
    ​
    /* 首先调用 pthread_attr_init 来对 线程属性变量 进行默认的初始化,然后才可以调用 pthread_attr_xxx 类函数来改变其值 */
    pthread_attr_init(&attr_1);
    ​
    /* 比如 (这里举一个例子,有很多设置属性的 API)
        pthread_attr_setdetachstate(&attr_1,PTHREAD_CREATE_DETACHED); 来设置 该线程的 可分离属性(pthread_detach 函数也是设置某一个 线程的 这个属性)
        在默认情况下线程是 joinable 或者叫非分离状态的,这种情况下,主线程等待子线程退出后,只有当 pthread_join() 函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。如果设置线程 为 unjoinable 或者叫 分离状态,即 子线程退出后 其主动的回收资源,主线程这里不必再调用 pthread_join 等待 子线程退出了。
        可以使用 pthread_attr_setdetachstate 函数把线程属性 detachstate 设置为下面的两个合法值之一:设置为 PTHREAD_CREATE_DETACHED 以分离状态启动线程(unjoinable);或者设置为 PTHREAD_CREATE_JOINABLE 正常启动线程(joinable,即默认的)。
    可以使用 pthread_attr_getdetachstate 函数获取当前的 datachstate 线程属性。
        另外,一般 不 建 议 去主动更改线程的优先级。
        上面的 线程属性相关 的参考 链接中 对更多属性设置API进行了介绍,包括 继承性、调度策略(两种可选+其它方式)、调度参数
    */
    ​
    /* 这里是通过设置线程属性 设置为 系统级线程,还是用户级线程 */
    pthread_attr_setscope(&attr_1, PTHREAD_SCOPE_SYSTEM);         /* 系统级线程,适合计算密集 */
    pthread_attr_setscope(&attr_2_3_4[0], PTHREAD_SCOPE_PROCESS); /* 用户级线程,适合IO密集 */
    ​
    /* 然后用这个属性去创建线程 */
     pthread_create(&tid, &attr_1, fn, arg);
    ​
    /* 可以将属性都设为 NULL 值,来重新 init 然后设置 */
    pthread_attr_destroy(&attr_1);

    线程的竞争:参考 Linux线程的实现 & LinuxThread vs. NPTL & 用户级内核级线程 & 线程与信号处理 - blcblc - 博客园 (cnblogs.com)(227条消息) Linux进程解析_deep_explore的博客-CSDN博客

    系统级线程会与其它 进程 共同竞争时间片,用户及线程仅与所在进程内的其它用户及线程竞争调度。

    Linux 2.6 以后的 pthread 使用 NPTL(更好支持 POSIX) 实现,都是系统级别的1:1线程(一个线程相当于一个进程,1:n就相当于一个进程里面n各线程相互竞争)模型,都是系统级线程。而 pthread_create() 里调用 clone() 时设置了CLONE_VM,所以在内核看来就产生了两个拥有相同内存空间的进程。所以用户态创建一个新线程,内核态就对应生成一个新进程。

    因此,Linux 是一个多任务,多线程的操作系统。但其实现的线程机制非常独特,从内核的角度来说,它并没有线程这个概念。 Linux 把所有的线程当作进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。进程和线程都有自己的 task_struct, 在内核看来两者没有司马区别。

  • etc.

使用这些 API 的例程

/* 文件:线程基本API的例子\线程API的例程-Linux下,被动回收.c */
​
#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
​
/* 例子说明:
创建 系统级 线程 1,无传入和传出参数,死循环,特定条件时退出,使用 pthread_join 回收
创建 用户级 线程 2、3、4 三个线程,线程号使用数组,传入和传出参数,使用 pthread_tryjoin_np 回收
​
在 linux 环境 的 gnu 编译器 下 执行编译:gcc temp.c -lpthread -o temp.bin
*/
​
/* 用于设置线程属性,主要包括 scope 属性(用于区分用户态或者内核态)、detach(分离/joinable)属性、堆栈地址、堆栈大小、优先级 */
pthread_attr_t attr_1, attr_2_3_4[3];
/* 指向线程标识符的指针,区分线程,即可称为 线程号,仅在本进程中有效。本质是 unsigned long int */
pthread_t id_1, id_2_3_4[3];
​
/* 线程 1,无传入和传出参数,执行完后退出,使用 pthread_join 回收 */
void *thread_1(void *in_arg)
{
    int i = 0;
    printf("thread_1 ID = %lu\n", (unsigned long)pthread_self());
    for(;;)
    {
        printf("thread_1 print times = %d\n", ++i);
        if(i >= 3)
            pthread_exit(NULL);
            /* 用 pthread_exit() 来调用线程的返回值,用来退出线程,但是退出线程所占用的资源不会随着线程的终止而得到释放 */
        sleep(1); /* sleep() 单位秒,程序挂起 1 秒 */
    }
}
​
/* 线程 2 3 4 */
void *thread_2_3_4(void *in_arg)
{
    /* 必须要 static 修饰,否则 pthread_join/pthread_tryjoin_np 无法获取到正确值 */
    static char* exit_arg;
    
    /* exit_arg 是 本函数的一个局部变量,多个线程 2、3、4 都会修改它,因此最后返回的时候不知道是谁最后一次修改的 */
    /* 因此要格外注意 */
    exit_arg = (char*)in_arg;
    
    pthread_t self_id = pthread_self();
    if(self_id == id_2_3_4[0])
    {
        printf("thread_2 ID = %lu\n", (unsigned long)self_id);
        sprintf((char*)in_arg,"id_2 gagaga");
    }else if(self_id == id_2_3_4[1])
    {
        printf("thread_3 ID = %lu\n", (unsigned long)self_id);
        sprintf((char*)in_arg,"id_3 lalala");
    }else if(self_id == id_2_3_4[2])
    {
        printf("thread_4 ID = %lu\n", (unsigned long)self_id);
        sprintf((char*)in_arg,"id_4 hahaha");
    }else
    {
        pthread_exit(NULL);
    }
​
    pthread_exit((void*)in_arg);
}
​
int main(void)
{
    int ret = -1, i = 0, return_thread_num = 0;
    char *str_gru[3];
    void *exit_arg = NULL;
​
    pthread_attr_init(&attr_1);
    pthread_attr_setscope(&attr_1, PTHREAD_SCOPE_SYSTEM); /* 系统级线程 */
    
    for(i = 0;i < 3;i++)
    {
        pthread_attr_init(&attr_2_3_4[i]);
        pthread_attr_setscope(&attr_2_3_4[i], PTHREAD_SCOPE_PROCESS); /* 用户级线程 */
    }
​
    /* 创建线程 1 */
    ret = pthread_create(&id_1, &attr_1, thread_1, NULL);
    if(ret != 0)
    {
        /* perror 把一个描述性错误消息输出到标准错误 stderr, 调用"某些"函数出错时,该函数已经重新设置了errno 的值。perror 函数只是将你输入的一些信息和 errno 所对应的错误一起输出 */
        perror("pthread1, pthread_create: ");
        return -1;
    }
    
    /* 创建线程 2、3、4 */
    for(i = 0;i < 3;i++)
    {
        str_gru[i] = (char*)malloc(sizeof(char) * 42 + i);
        ret = pthread_create(&id_2_3_4[i], &attr_2_3_4[i], thread_2_3_4, (void *)str_gru[i]);
        if(ret != 0)
        {
            perror("pthread 2 3 4, pthread_create: ");
            return -1;
        }
    }
    
    /* 等待所有线程结束,先等 线程 2、3、4 相继的、无顺序要求的 退出,再等 线程 1 退出 */
    for(;;)
    {
        for(i = 0;i < 3;i++)
        {
            /* pthread_tryjoin_np 的 np 为不可移植,是gnu定的非POSIX标准的API,仅linux里面的编译器能用 */
            if(pthread_tryjoin_np(id_2_3_4[i], &exit_arg) == 0)
            {
                printf("pthread : %lu exit with str: %s\n", (unsigned long)id_2_3_4[i], (char*)exit_arg);
                free(str_gru[i]);
                return_thread_num++;
            }
        }
        if(return_thread_num >= 3) break;
    }
    pthread_join(id_1, NULL);
​
    return 0;
}

猜你喜欢

转载自blog.csdn.net/Staokgo/article/details/132630744
今日推荐