C语言多线程学习记录(一)

[复制链接]
查看789 | 回复3 | 2023-9-15 20:40:45 | 显示全部楼层 |阅读模式

在Linux内核中多线程编程是很常见的一种性能提升手段。所以学习C语言,对于多线程也应该有一定的了解。关于线程与进程,在知乎看到下面这么一段说明,觉得很清晰:

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

创建线程

创建线程的函数是:pthread_create() ,该函数声明在pthread.h 头文件中,其函数声明为:

int pthread_create( pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routing)(void *),
                   void *arg);

参数

该函数需要四个参数,其含义如下:

  • pthread_t *thread 一个指向 “新创建线程” 的指针变量
  • const pthread_attr_t *attr 一个指向 “新创建线程的相关属性” 的指针变量,一般情况下不需要手动更改新建线程的属性,直接传NULL即可
  • void *(*start_routing)(void *) 一个指向 “新线程要执行的函数” 的函数指针
  • void *arg 代表传递给新线程要执行的函数的参数

返回值

再次声明pthread_create() 函数的作用就是创建一个新的线程。其返回值就表示是否成功创建。所以其返回值有成功与不成功两种情况:

  • 成功创建: 返回 0
  • 创建失败: 返回非0值,在errno.h头文件中定义了常见的几个异常返回值:
    • EAGIN:表示系统自远不足
    • EINVAL: 表示attr参数无效
    • EPERM: 表示attr参数非法,或者操作存在确权行为

线程创建实例

#include <stdio.h>
#include <unistd.h>    // sleep函数的头文件
#include <pthread.h>

void * ThreadFun(void * arg) {   // 新线程要执行的函数
    if (arg == NULL) {
        printf("arg is NULL!\n");
    }else {
        printf("%s\n", (char*)arg);
    }

    return NULL;
}

int main() {
    int res;
    char * url = "https://klelee.com";
    pthread_t mythread1, mythread2;    //定义两个新线程

    /** 
     * &mythread1 对应的形参是:pthread_t *thread,形参是指针变量,因此需要进行传址
     * NULL 对应的形参是:const pthread_attr_t *attr,也就是新线程的属性
     * ThreadFun 对应的形参是:void *(*start_routing)(void *), 执行新线程要执行的函数的函数指针
     * NULL 对应的形参是:void *arg,新线程要执行的函数的参数
     */
    res = pthread_create(&mythread1, NULL, ThreadFun, NULL);
    if (res != 0) {
        printf("thread create failed!\n");
        return 0;
    }

    /** 
     * &mythread2 对应的形参是:pthread_t *thread,形参是指针变量,因此需要进行传址
     * NULL 对应的形参是:const pthread_attr_t *attr,也就是新线程的属性
     * ThreadFun 对应的形参是:void *(*start_routing)(void *), 执行新线程要执行的函数的函数指针
     * NULL 对应的形参是:void *arg,新线程要执行的函数的参数
     */
    res = pthread_create(&mythread2, NULL, ThreadFun, (void*)url);
    if (res != 0) {
        printf("thread create failed!\n");
        return 0;
    }

    sleep(5);
    return 0;
}

多线程程序的编译

需要注意的地方就是对于多线程程序,在编译的时候需要添加lpthread 选项

gcc thread.c -o thread.out -lpthread

执行结果

image-20230404180113989

终止线程

线程的终止,有三种情况:

  1. 程序正常结束,线程终止
  2. 通过return语句或者pthread_exit()终止线程
  3. 其他线程发送pthread_cancel()

第一种情况就是字面意思,很容易理解。接下来详细说明后面两种情况。

通过return和pthread_exit()终止进程

pthread_exit() 可以理解为是线程中的return语句。该函数也声明在pthread.h 文件中。其声明为:

void pthread_exit(void * retval);

该函数返回值为空,有一个void类型的指针变量作为形式参数。这个参数就类似与return语句返回的数据一样,作为当前线程运行的函数的返回值。当没有返回值的时候,这里可以是NULL.

return和pthread_exit()的区别

关于两者的区别还是直接体现在程序中吧,首先来看一个用return的程序:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void * thread_func (void * arg) {
    sleep(5);
    printf("https://klelee.com");
}

int main() {
    int res;
    pthread_t mythread;

    res = pthread_create(&mythread, NULL, thread_func, NULL);
    if (res != 0) {
        printf("new thread failed!\n");
        return 0;
    }

    printf("main thread will return!\n");

    return res;    //主要体现在这里
}

编译和运行结果如下:

[klelee@arch first]$ gcc exit_return.c -o exit_return.out -lpthread
[klelee@arch first]$ ./exit_return.out 
main thread will return!
[klelee@arch first]$ 

再来看看pthread_exit()的实例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void * thread_func (void * arg) {
    sleep(5);
    printf("https://klelee.com");
}

int main() {
    int res;
    pthread_t mythread;

    res = pthread_create(&mythread, NULL, thread_func, NULL);
    if (res != 0) {
        printf("new thread failed!\n");
        return 0;
    }

    printf("main thread will return!\n");

    pthread_exit(NULL);    // 将这里换成了pthread_exit()
}

运行结果:

[klelee@arch first]$ gcc exit_return.c -o exit_return.out -lpthread
[klelee@arch first]$ ./exit_return.out 
main thread will return!
https://klelee.com
[klelee@arch first]$ 

对比如上两段程序的运行结果发现,区别就在于子线程中的程序有没有完全的执行。在主线程中直接使用return语句进行返回之后,由主线程创建的子线程也随之终止。

而使用pthread_exit语句的主线程终止之后,子线程没有终止,而是在继续运行,知道程序结束!

pthread_cancel()函数

与上面两种程序终止方式不同的是,使用cancel信号终止是被动终止。调用cancel函数的其他线程,而不是线程自身调用。其生命如下:

int pthread_cancel(pthread_t *thread);

参数

指向目标线程的指针变量

返回值

有关于pthread_cancel函数的返回值需要注意的是其成功的条件:发送成功即为成功。即:

  • cancel信号成功发送给目标线程,返回0
  • cancel信号发送失败,返回非0值。若未找到目标线程,返回ESRCH宏,该宏的值为3.

对于接收cancel信号后终止的线程,相当于自己调用了pthread_exit(PTHREAD_CANCELED) 其中宏:PTHREAD_CANCELED定义在pthread.h 头文件中。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void * thread_func (void * arg) {
    printf("https://klelee.com\n");
    sleep(5);    // 在这段时间内程序被终止
    printf("cclike.cc\n");    // 主要看这句会不会输出
}

int main() {
    int res;
    int value;
    void * mess;
    pthread_t mythread;

    res = pthread_create(&mythread, NULL, thread_func, NULL);
    if (res != 0) {
        printf("new thread failed!\n");
        return 0;
    }    //创建线程之后sleep两秒
    sleep(2);
    // 然后立即发送cancel信号,在thread_fun函数睡眠的5秒内,强制终止程序运行
    res = pthread_cancel(mythread);
    if (res != 0) {
        printf("stop thread failed!\n");
        return 0;
    }

    res = pthread_join(mythread, &mess);
    if (res != 0) {
        printf("wait for thread failed!\n");
        return 0;
    }

    if (mess == PTHREAD_CANCELED) {
        printf("force stop thread sucess!\n");    // 判断是否有cancel信号终止
    }else {
        printf("force stop thread failed!\n");
    }

    printf("main thread will return!\n");
    pthread_exit(NULL);
}

编译和运行结果:

[klelee@arch first]$ gcc exit_return.c -o exit_return.out -lpthread
[klelee@arch first]$ ./exit_return.out 
https://klelee.com
force stop thread sucess!
main thread will return!
[klelee@arch first]$ 

通过运行结果可以看到,thread_fun()函数被提前终止了!

cancel信号处理机制

并不是所有的cancel信号都会被线程处理。很常见的现象就是当目标线程接收到cancel信号的时候,正处于循环当中,这种情况就不会立即响应cancel信号去终止线程

事实上,对于默认属性的线程,当有线程借助 pthread_cancel() 函数向它发送 Cancel 信号时,它并不会立即结束执行,而是选择在一个适当的时机结束执行。

所谓适当的时机,POSIX 标准中规定,当线程执行一些特殊的函数时,会响应 Cancel 信号并终止执行,比如常见的 pthread_join()、pthread_testcancel()、sleep()、system() 等,POSIX 标准称此类函数为“cancellation points”(中文可译为“取消点”)。

此外,<pthread.h> 头文件还提供有 pthread_setcancelstate() 和 pthread_setcanceltype() 这两个函数,我们可以手动修改目标线程处理 Cancel 信号的方式。

pthread_setcancelstate()函数

借助 pthread_setcancelstate() 函数,我们可以令目标线程处理 Cancal 信号,也可以令目标线程不理会其它线程发来的 Cancel 信号。其函数声明如下:

int pthread_setcancelstate( int state , int * oldstate ); 

参数

  1. state 参数有两个可选值,分别是:
    1. PTHREAD_CANCEL_ENABLE(默认值):当前线程会处理其它线程发送的 Cancel 信号;
    2. PTHREAD_CANCEL_DISABLE:当前线程不理会其它线程发送的 Cancel 信号,直到线程状态重新调整为 PTHREAD_CANCEL_ENABLE 后,才处理接收到的 Cancel 信号。
  2. oldtate 参数用于接收线程先前所遵循的 state 值,通常用于对线程进行重置。如果不需要接收此参数的值,置为 NULL 即可。

返回值

pthread_setcancelstate() 函数执行成功时,返回数字 0,反之返回非零数。

pthread_setcanceltype()函数

当线程会对 Cancel 信号进行处理时,我们可以借助 pthread_setcanceltype() 函数设置线程响应 Cancel 信号的时机。语法格式如下:

int pthread_setcanceltype( int type , int * oldtype );

参数

(1) type 参数有两个可选值,分别是:

  • PTHREAD_CANCEL_DEFERRED(默认值):当线程执行到某个可作为取消点的函数时终止执行;
  • PTHREAD_CANCEL_ASYNCHRONOUS:线程接收到 Cancel 信号后立即结束执行。

(2) oldtype 参数用于接收线程先前所遵循的 type 值,如果不需要接收该值,置为 NULL 即可。

返回值

pthread_setcanceltype() 函数执行成功时,返回数字 0,反之返回非零数。

实例

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
void * thread_Fun(void * arg) {
    printf("新建线程开始执行\n");
    int res;
    //设置线程为可取消状态
    res = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    if (res != 0) {
        printf("修改线程可取消状态失败\n");
        return  NULL;
    }
    //设置线程接收到 Cancel 信号后立即结束执行
    res = pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    if (res != 0) {
        printf("修改线程响应 Cancel 信号的方式失败\n");
        return  NULL;
    }
    while (1);
    return NULL;
}
int main()
{
    pthread_t myThread;
    void * mess;
    int value;
    int res;
    res = pthread_create(&myThread, NULL, thread_Fun, NULL);
    if (res != 0) {
        printf("线程创建失败\n");
        return 0;
    }
    sleep(1);
    //向 myThread 线程发送 Cancel 信号
    res = pthread_cancel(myThread);
    if (res != 0) {
        printf("终止 myThread 线程失败\n");
        return 0;
    }
    //等待 myThread 线程执行结束,获取返回值
    res = pthread_join(myThread, &mess);
    if (res != 0) {
        printf("等待线程失败\n");
        return 0;
    }
    if (mess == PTHREAD_CANCELED) {
        printf("myThread 线程被强制终止\n");
    }
    else {
        printf("error\n");
    }
    return 0;
}

编译与运行结果

[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
新建线程开始执行
myThread 线程被强制终止

获取线程函数的返回值

在上面的函数中我们用到了一个新的函数pthread_join ,该函数能够获取某个线程执行结束时返回的数据。接下来我们详细看看它的使用方法。

pthread_join函数

该函数声明在pthread.h头文件中,其函数声明如下:

int pthread_join(pthread_t thread, void ** retval);

参数

该函数有两个返回值:

  • thread 参数用于指定接收哪个线程的返回值
  • retval 这里用一个二级指针直线了返回值地址的地址。如果 thread 线程没有返回值,又或者我们不需要接收 thread 线程的返回值,可以将 retval 参数置为 NULL。

pthread_join() 函数会一直阻塞调用它的线程,直至目标线程执行结束(接收到目标线程的返回值),阻塞状态才会解除。

返回值

该函数拥有int类型的返回值。

  • 0 值: 表示函数成功等到目标线程运行结束(成功拿到返回值)。
  • 非零值:也就是等待目标线程返回失败,不同原因有不同的返回值,常见原因如下:
    • EDEADLK:检测到线程发生了死锁。
    • EINVAL:分为两种情况,要么目标线程本身不允许其它线程获取它的返回值,要么事先就已经有线程调用 pthread_join() 函数获取到了目标线程的返回值。
    • ESRCH:找不到指定的 thread 线程。

相关宏都在errno.h中做了定义!

回复

使用道具 举报

WangChong | 2023-9-15 20:52:15 | 显示全部楼层
有学习资料吗
回复 支持 反对

使用道具 举报

可乐klelee | 2023-9-15 21:06:19 | 显示全部楼层

C语言中文网C语言部分讲的还不错,但是好多都要会员。
回复 支持 反对

使用道具 举报

WYG | 2023-9-15 21:39:04 | 显示全部楼层
打卡
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则