多线程(1): 线程的创建、回收、分离

1. 多线程概述

多线程在项目开发过程中使用频率非常高,因为使用多线程可以提高程序的并发性。提高程序并发性有两种方式:(1)多线程 (2)多进程。但是多线程对系统资源的消耗会更加少一些,并且线程和进程执行效率差不多。

  • 在执行系统应用程序时,应用程序需要占用cpu资源,电脑上cpu核是有限的, 假设PC上有8核CPU,但同时执行了100个应用程序。为什么看起来这100个执行程序可以同时运行呢?这其实是一个假象,cpu会把单位时间分为若干份,每一份就是一个cpu时间片,cpu每个时间片很短,是纳秒级别。cpu时间片分好之后,由系统进行调度,每个线程在执行的时候,都需要抢cpu的时间片,如果抢到了,这个线程就执行(运行态)。如果没有抢到,这个线程就处于就绪态,就绪态的线程没有cpu的使用权,因为它没有抢到cpu的时间片,但它会一直不停的去抢,抢到后就有就绪态变为运行态。时间片用完后,又重新变为就绪态,继续和其他线程抢时间片,这样循环往复。
  • 通过线程的快速切换,我们看到这个线程是一直在运行,其实这个线程也是在走走停停的,只不同我们肉眼无法识别这么短的时间。

linux的多线程在底层的实现方式和windows其实存在一些区别,因为linux在早期并没有多线程的概念。linux操作系统在创建之初,就是一个基于进程的操作系统。因为在windows中线程的使用频率和效率都非常高,因此就在linux中引入了线程的概念。由于linux内核在设计之初没有设计线程,因此linux系统就基于进程做出来线程。因袭在linux中线程就是一个轻量级的进程(LWP:light weight process)。

在 Linux 环境下线程的本质仍是进程。在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。操作系统会以进程为单位,分配系统资源,可以这样理解,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。

先从概念上了解一下线程和进程之间的区别:

  • 进程有自己独立的地址空间,多个线程共用同一个地址空间
    • 线程更加节省系统资源,效率不仅可以保持的,而且能够更高
    • 在一个地址空间中多个线程独享:每个线程都有属于自己的栈区,寄存器 (内核中管理的)
    • 在一个地址空间中多个线程共享:代码段,堆区,全局数据区,打开的文件 (文件描述符表) 都是线程共享的
  • 线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位
    • 每个进程对应一个虚拟地址空间,一个进程只能抢一个 CPU 时间片
    • 一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的 CPU 时间片

在这里插入图片描述

  • 如果在一个操作系统中只有一个线程或进程,那么没有线程跟它抢时间片,因此所有的时间片都是这个线程的,就会不间断一直在运行。
  • 如果有多个线程,cpu处理多个线程时,会把cpu时间划分为若干个时间片,每个线程抢一个时间片,抢到了就执行,执行完后放弃cpu使用权,然后又去抢cpu的时间片。
  • 上图多个线程有序交替执行是一种理想状态,因为各个线程抢占cpu时间片都是随机的,有可能线程1前3次都抢到了时间片。线程2在前10次都没有抢到cpu时间片。线程3在第2到5次抢到cpu时间片,这都是有可能的,每次的情况都是随机的。因此不能在各个线程的执行状态认为是一个有序的执行,它是一个随机无序的执行状态。

CPU 的调度和切换:线程的上下文切换比进程要快的多

  • 上下文切换:进程 / 线程分时复用 CPU 时间片,在切换之前会将上一个任务的状态进行保存(记录执行的位置),下次切换回这个任务的时候,加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换。
  • 线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。 因为多线程退出,最终只需要销毁一个地址空间,也就是消费一份资源。如果是多个进程退出呢,你需要销毁多分资源。因此在释放的时候,线程是否的也会更快。

在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是线程并不是越多越好,如何控制线程的个数呢?

  • 文件 IO 操作:文件 IO 对 CPU 是使用率不高,因此可以分时复用 CPU 时间片,线程的个数 = 2 * CPU 核心数 (效率最高)

  • 处理复杂的算法 (主要是 CPU 进行运算,压力大),线程的个数 = CPU 的核心数 (效率最高)

2. 创建线程

注意:我们创建的线程是子线程,而不是主线程,因为主线程默认是存在的,因为启动一个程序,就得到了一个进程,这个进程中调用了线程创建函数,得到了子线程,原来的进程就退化为主线程。因此从一个单进程的应用程序退化为一个多线程的应用程序,但程序中主线程退出之后,地址空间也就销毁了,地址空间不存在子线程就随之被销毁了。

每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t,这个 ID 是一个无符号长整形数,如果想要得到当前线程的线程 ID,可以调用如下函数:

pthread_t pthread_self(void);	// 返回当前线程的线程ID

在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
// Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a

参数:

  • thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中

  • attr: 线程的属性,一般情况下使用默认属性即可,写 NULL

  • start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。

  • arg: 作为实参传递到 start_routine 指针指向的函数内部

  • 返回值:线程创建成功返回 0,创建失败返回对应的错误号

2.1 创建线程

下面是创建线程的示例代码,在创建过程中一定要保证编写的线程函数与规定的函数指针类型一致:void *(*start_routine) (void *):

// pthread_create.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
    
    
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
    
    
        printf("child == i: = %d\n", i);
    }
    return NULL;
}

int main()
{
    
    
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
    
    
        printf("i = %d\n", i);
    }
    
    // 休息, 休息一会儿...
    // sleep(1);
    
    return 0;
}

程序的执行逻辑

  • 当启动程序后,程序的入口肯定是main函数,然后在主线程中,执行 pthread_create创建子线程,创建子线程后,它就会执行它的任务函数working, 从working函数的第一行,向下执行。执行完毕后,子线程就退出了。
  • 主线程的执行会从main函数向下执行,包含创建子线程,然后执行for循环,直到执行完毕。但是主线程不会执行子线程的任务函数,任务函数是在子线程中处理的。

编译测试程序,会看到如下错误信息:

$ gcc pthread_create.c 
/tmp/cctkubA6.o: In function `main':
pthread_create.c:(.text+0x7f): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status

错误原因是因为编译器链接不到线程库文件(动态库),需要在编译的时候通过参数指定出来,动态库名为 libpthread.so 需要使用的参数为 -l,根据规则掐头去尾最终形态应该写成:-lpthread(参数和参数值中间可以有空格)。正确的编译命令为:

# pthread_create 函数的定义在某一个库中, 编译的时候需要加库名 pthread
$ gcc pthread_create.c -lpthread
$ ./a.out 
子线程创建成功, 线程ID: 139712560109312
我是主线程, 线程ID: 139712568477440
i = 0
i = 1
i = 2

在打印的日志输出中为什么子线程处理函数没有执行完毕呢(只看到了子线程的部分日志输出)?
主线程一直在运行,执行期间创建出了子线程,说明主线程有 CPU 时间片,在这个时间片内将代码执行完毕了,主线程就退出了。子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在。

得到的结论:在没有人为干预的情况下,虚拟地址空间的生命周期和主线程是一样的,与子线程无关。 目前的解决方案:让子线程执行完毕,主线程再退出,可以在主线程中添加挂起函数 sleep(); 主线程执行到sleep它就放弃了cpu的资源,此时子线程抢到cpu时间片,就会向下执行,执行完毕后,主线程睡醒,然后主线程抢到时间片继续向下执行。

3. 线程退出

在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。

其实这个函数主要是在主线程中使用,因为在一个地址空间中有多个线程,分为主线程和多个子线程,主线程是否退出不影响主线程执行,因为子线程退出地址空间依然还是存在的。但当主线程退出,虚拟地址空间就被整体释放了,那么子线程也就被一并释放了。

所以说如果想要让主线程退出,子线程依然可以继续运行,我们就需要调用线程退出函数,让主线程让主线程自己退出去,不要把虚拟地址空间释放掉。当子线程依次退出之后,程序执行完毕,虚拟地址空间就被操作系统回收了。

线程退出函数

#include <pthread.h>
void pthread_exit(void *retval);
  • 参数:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL; 至于说传递出的retval怎么接收,我们需要配合线程回收函数进行使用, 下文会进行介绍

下面是线程退出的示例代码,可以在任意线程的需要的位置调用该函数:

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

// 子线程的处理代码
void* working(void* arg)
{
    
    
    sleep(1);
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
   for(int i=0; i<9; ++i)
    {
    
    
        printf("child == i: = %d\n", i);
    }
    return NULL;
}

int main()
{
    
    
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
    
    
        printf("i = %d\n", i);
    }

    // 主线程调用退出函数退出, 地址空间不会被释放
    pthread_exit(NULL);
    
    return 0;
}
  • 使用了pthread_exit,主线程虽然退出了,但不会是否虚拟地址空间,子线程依然可以执行。

4. 线程回收

4.1 线程函数

这个线程回收函数叫pthread_join,用来回收子线程资源的,是由主线程来回收子线程资源的。主线程有一个义务,当子线程结束之后回收子线程资源。但并不是所有的资源都有主线程回收,回收的是内核中的资源,因为这多个线程共用同一个虚拟地址空间,对于堆区、栈区、代码段等数据,子线程结束后, 它就会自动释放,主线程主要是帮助子线程释放内核中的资源,这个子线程自己无法释放。

pthread_join(),这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收

另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,谁回收子线程谁就能得到数据,也就是主线程能得到回收的数据,函数原型如下:

#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);

参数:

  • thread: 要被回收的子线程的线程 ID

  • retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL

  • 返回值:线程回收成功返回 0,回收失败返回错误号。

4.2 回收子线程数据

在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出,在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式,下面来列举几种:

4.2.1 使用子线程栈

通过函数 pthread_exit(void retval); 可以得知,子线程退出的时候,需要将数据记录到一块内存中,通过参数传出的是存储数据的内存的地址,而不是具体数据,由因为参数是 void 类型,所有这个万能指针可以指向任意类型的内存地址。先来看第一种方式,将子线程退出数据保存在子线程自己的栈区:

// pthread_join.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 定义结构
struct Persion
{
    
    
    int id;
    char name[36];
    int age;
};

// 子线程的处理代码
void* working(void* arg)
{
    
    
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
    
    
        printf("child == i: = %d\n", i);
        if(i == 6)
        {
    
    
            struct Persion p;
            p.age  =12;
            strcpy(p.name, "tom");
            p.id = 100;
            // 该函数的参数将这个地址传递给了主线程的pthread_join()
            pthread_exit(&p);
        }
    }
    return NULL;	// 代码执行不到这个位置就退出了
}

int main()
{
    
    
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
    
    
        printf("i = %d\n", i);
    }

    // 阻塞等待子线程退出
    void* ptr = NULL;
    // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
    // 这个内存地址就是pthread_exit() 参数指向的内存
    pthread_join(tid, &ptr);
    // 打印信息
    struct Persion* pp = (struct Persion*)ptr;
    printf("子线程返回数据: name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
    printf("子线程资源被成功回收...\n");
    
    return 0;
}

  • 子线程通过pthread_exit(&p);,传递出结构体数据p, 子线程通过pthread_join来回收tid对应的子线程,并通过第二个参数接收子线程传递出的结构体数据
  • pthread_join是一个阻塞函数,主线程执行到这行会一直等待子线程执行完毕,子线程执行完毕后会进行回收,通过pthread_join第二个参数执行传递出来数据的地址; 然后对void* 数据类型的数据进行转换,这样主线程就得到子线程传递出来的数据了。

编译并执行测试程序:

# 编译代码
$ gcc pthread_join.c -lpthread
# 执行程序
$ ./a.out 
子线程创建成功, 线程ID: 140652794640128
我是主线程, 线程ID: 140652803008256
i = 0
i = 1
i = 2
我是子线程, 线程ID: 140652794640128
child == i: = 0
child == i: = 1
child == i: = 2
child == i: = 3
child == i: = 4
child == i: = 5
child == i: = 6
子线程返回数据: name: , age: 0, id: 0
子线程资源被成功回收...

通过打印的日志可以发现,在主线程中没有没有得到子线程返回的数据信息,具体原因是这样的:

  • 如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。
  • struct Persion p是子线程开辟的临时变量存在栈内存中,子线程退出就自动释放栈内存,栈内存的数据就被释放了。此时临时变量p已经不存在了,再去访问这块内存空间,就没有数据了。
  • 如何保证能得到回收的数据呢?那么就要保证struct Persion p不被释放,因此可以将这个变量定义为全局变量,或者静态变量就可以

4.2.2 使用全局变量

位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据,因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。在下面的例子中将数据存储到了全局变量中:

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

// 定义结构
struct Persion
{
    
    
    int id;
    char name[36];
    int age;
};

struct Persion p;	// 定义全局变量

// 子线程的处理代码
void* working(void* arg)
{
    
    
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
    
    
        printf("child == i: = %d\n", i);
        if(i == 6)
        {
    
    
            // 使用全局变量
            p.age  =12;
            strcpy(p.name, "tom");
            p.id = 100;
            // 该函数的参数将这个地址传递给了主线程的pthread_join()
            pthread_exit(&p);
        }
    }
    return NULL;
}

int main()
{
    
    
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
    
    
        printf("i = %d\n", i);
    }

    // 阻塞等待子线程退出
    void* ptr = NULL;
    // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
    // 这个内存地址就是pthread_exit() 参数指向的内存
    pthread_join(tid, &ptr);
    // 打印信息
    struct Persion* pp = (struct Persion*)ptr;
    printf("name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
    printf("子线程资源被成功回收...\n");
    
    return 0;
}

4.2.3 使用主线程栈

虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中:

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

// 定义结构
struct Persion
{
    
    
    int id;
    char name[36];
    int age;
};


// 子线程的处理代码
void* working(void* arg)
{
    
    
    struct Persion* p = (struct Persion*)arg;
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
    
    
        printf("child == i: = %d\n", i);
        if(i == 6)
        {
    
    
            // 使用主线程的栈内存
            p->age  =12;
            strcpy(p->name, "tom");
            p->id = 100;
            // 该函数的参数将这个地址传递给了主线程的pthread_join()
            pthread_exit(p);
        }
    }
    return NULL;
}

int main()
{
    
    
    // 1. 创建一个子线程
    pthread_t tid;

    struct Persion p;
    // 主线程的栈内存传递给子线程
    pthread_create(&tid, NULL, working, &p);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
    
    
        printf("i = %d\n", i);
    }

    // 阻塞等待子线程退出
    void* ptr = NULL;
    // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
    // 这个内存地址就是pthread_exit() 参数指向的内存
    pthread_join(tid, &ptr);
    // 打印信息
    printf("name: %s, age: %d, id: %d\n", p.name, p.age, p.id);
    printf("子线程资源被成功回收...\n");
    
    return 0;
}
  • 因此将struct Persion p定义在主线程,然后通过 pthread_create最后一个参数给子线程传递参数p, 这样子线程函数就能接收到p变量。
  • 然后子线程修改p的值,并通过pthread_exit将数据传递出来,当执行到 pthread_join,等待子线程执行完毕,并消耗栈内存,由于p是在主线程中的,此时主线程并没有销毁,因此主线程中依然可以保留p的数据

在上面的程序中,调用 pthread_create() 创建子线程,并将主线程中栈空间变量 p 的地址传递到了子线程中,在子线程中将要传递出的数据写入到了这块内存中。也就是说在程序的 main() 函数中,通过指针变量 ptr 或者通过结构体变量 p 都可以读出子线程传出的数据。

5. 线程分离

  • pthread_detach 线程分离函数,默认情况主线程和子线程之间是有关联的,就是在主线程退出的时候会释放子线程的资源,如果在线程分离后,主线程在退出时,就不会释放子线程对应的那块资源了。

  • 因为在有些情况下,线程如果不分离,你要想回收子线程资源就需要调用join,一调用join函数主线程就堵塞了。加入我们想让主线程执行一些其他的事情,又想让他回收子线程资源,你只要调用join就堵塞在那里,等待子线程执行完毕。因此就可以使用pthread_detach 让主线程和子线程进行分离

  • 虽然子线程和主线程分离了,但是当主线程退出之后,地址空间释放了,子线程也就不能运行了。

#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);

在线程库函数中为我们提供了线程分离函数 pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用 pthread_join() 就回收不到子线程资源了。

下面的代码中,在主线程中创建子线程,并调用线程分离函数,实现了主线程和子线程的分离:

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

// 子线程的处理代码
void* working(void* arg)
{
    
    
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
    
    
        printf("child == i: = %d\n", i);
    }
    return NULL;
}

int main()
{
    
    
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
    
    
        printf("i = %d\n", i);
    }

    // 设置子线程和主线程分离
    pthread_detach(tid);

    // 让主线程自己退出即可
    pthread_exit(NULL);
    
    return 0;
}
  • 需要传入子线程id, 指定哪个子线程与主线程分离 pthread_detach(tid), 这样子线程和主线程就可以各自执行各自的,没有join进行阻塞。
  • 为了使得主线程退出,不影响子线程分离,可以调用 pthread_exit(NULL)
  • 使用 pthread_detach相当于给主线程减负了,这样子线程的资源有系统进行回收,不需要子线程回收

6. 其他线程函数

6.1 线程取消

线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:

  • 在线程 A 中调用线程取消函数 pthread_cancel,指定杀死线程 B,这时候线程 B 是死不了的
  • 在线程 B 中进程一次系统调用(从用户区切换到内核区)也就是调用一次系统函数,否则线程 B 可以一直运行。系统调用无处不在,比如执行了printf就相当于间接调用了系统函数io
#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);
  • 参数:要杀死的线程的线程 ID
  • 返回值:函数调用成功返回 0,调用失败返回非 0 错误号。

在下面的示例代码中,主线程调用线程取消函数,只要在子线程中进行了系统调用,当子线程执行到这个位置就挂掉了。

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

// 子线程的处理代码
void* working(void* arg)
{
    
    
    int j=0;
    for(int i=0; i<9; ++i)
    {
    
    
        j++;
    }
    // 这个函数会调用系统函数, 因此这是个间接的系统调用
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
    
    
        printf(" child i: %d\n", i); //printf 进行了系统调用
    }

    return NULL;
}

int main()
{
    
    
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
    
    
        printf("i = %d\n", i);
    }

    // 杀死子线程, 如果子线程中做系统调用, 子线程就结束了
    pthread_cancel(tid);

    // 让主线程自己退出即可
    pthread_exit(NULL);
    
    return 0;
}

6.2 线程 ID 比较

在 Linux 中线程 ID 本质就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程的 ID,但是线程库是可以跨平台使用的,在某些平台上 pthread_t 可能不是一个单纯的整形,这中情况下比较两个线程的 ID 必须要使用比较函数,函数原型如下:

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
  • 参数:t1 和 t2 是要比较的线程的线程 ID
  • 返回值:如果两个线程 ID 相等返回非 0 值,如果不相等返回 0

猜你喜欢

转载自blog.csdn.net/weixin_38346042/article/details/131612746