Linux多线程(一) 线程的创建与控制

一、线程概述

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

线程与进程的区别:

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

二、创建线程

2.1、创建线程

c 复制代码
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
  • thread: 传出参数,是无符号长整形数,线程创建成功, 会将线程ID写入到这个指针指向的内存中

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

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

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

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

2.2、获得当前线程PID

pthread_t在Linux平台上是64位整数,但是这是一个跨平台的函数,在其他平台不知道是如何实现的,最好我们还是使用 pthread_t

c 复制代码
pthread_t pthread_self(void);

2.3、示例

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()
{
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);
    printf("子线程创建成功, 线程ID: %ld\n", tid);
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
        printf("i = %d\n", i);
    }
    return 0;
}

可以看到子线程并没有执行完毕,这是因为主线程创建好子线程后直接退出了。子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在。

虚拟地址空间的生命周期和主线程是一样的,与子线程无关

三、线程退出

c 复制代码
#include <pthread.h>
void pthread_exit(void *retval);
  • retval: 线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL

只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用

四、线程回收

线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做pthread_join,这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

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

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

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

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

4.1、使用子线程栈

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 定义结构
struct Persion {
    int id;
    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;
            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、使用全局变量

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

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 定义结构
struct Persion
{
    int id;
    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;
            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.3、使用主线程栈

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

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 定义结构
struct Persion {
    int id;
    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;
            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("age: %d, id: %d\n", p.age, p.id);
    printf("子线程资源被成功回收...\n");
    
    return 0;
}

五、线程分离

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。

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

c 复制代码
#include <pthread.h>
int pthread_detach(pthread_t thread);

下面一个例子

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);
    }
    // 设置子线程和主线程分离
    pthread_detach(tid);
    // 让主线程自己退出即可
    pthread_exit(NULL);
    return 0;
}

六、线程取消

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

  • 在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,这时候线程B是死不了的
  • 在线程B中进行一次系统调用(从用户区切换到内核区),否则线程B可以一直运行。

有一个解释很形象:这其实和七步断肠散、含笑半步癫的功效是一样的,吃了毒药不动或者不笑也没啥事儿

c 复制代码
#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);
  • thread:要杀死的线程的线程ID

示例:

c 复制代码
#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);
    }
    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;
}

关于系统调用有两种方式:

  • 直接调用Linux系统函数
  • 调用标准C库函数,为了实现某些功能,在Linux平台下标准C库函数会调用相关的系统函数

七、线程比较

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

c 复制代码
#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);
  • t1t2 是要比较的线程的线程ID
  • 返回值:如果两个线程ID相等返回非0值,如果不相等返回0
相关推荐
R-sz7 分钟前
解决k8s部署dashboard时一直处于Pending状态的问题
linux·容器·kubernetes
hunteritself17 分钟前
再谈ChatGPT降智:已蔓延到全端,附解决方案!
人工智能·gpt·算法·机器学习·chatgpt·openai
孙尚香蕉38 分钟前
深入探索哈夫曼编码与二叉树的遍历
数据结构·算法
Helene190039 分钟前
Leetcode 283-移动零
算法·leetcode·职场和发展
andyweike42 分钟前
数据结构-排序思想
数据结构·算法·排序算法
一只码代码的章鱼1 小时前
高精度算法:加减乘除 (学习笔记)
算法
2301_815389371 小时前
【笔记】在虚拟机中通过apache2给一个主机上配置多个web服务器
linux·服务器·笔记
go54631584651 小时前
磁盘调度算法
服务器·数据库·算法
檀越剑指大厂1 小时前
【Linux系列】sed命令的深入解析:如何使用sed删除文件内容
linux·运维·服务器
monkiro1 小时前
机器学习算法基础知识1:决策树
算法·决策树·机器学习