首页 > 系统相关 >Linux多线程

Linux多线程

时间:2023-11-22 11:22:24浏览次数:37  
标签:printf 多线程 Linux 主线 线程 pthread include ID

文章参考:爱编程的大丙 (subingwen.cn)

一. 线程概述

线程是一种轻量级的,在Linux环境下,由于Linux内核起初并未设计线程,只有进程,因此将线程本质上仍是进程。而在实际处理中,进程是操作系统最小的分配资源单位,而线程是操作系统最小的调度执行单位。

区别如下:

  • 空间上:每一个进程都有自己独立的空间,而线程可以多个线程共用一个空间。

    • 线程更加节省系统资源,效率上更有保证。
    • 一片地址空间上线程独自拥有的是:栈区、寄存器(内核管理)。
    • 一片地址空间上线程共享的部分:代码区、堆区、全局数据区、打开的文件。
  • 管理上:线程是程序最小的执行单位,而进程是操作系统分配资源的最小单位:

    • 一个进程对应一个虚拟地址空间,而一个虚拟地址空间只能抢一个CPU时间片。
    • 一个地址空间中可以有多个线程,因此在有限的自愿基础上,可以抢占更多的CPU时间片。
  • CPU调度和切换上:线程的上下文切换比进程要快的多。

    所谓上下文切换是指:进程/线程在使用CPU时遵循分时复用原则,在切换之前会将上一个人物的状态进行保存,以便下次切换回这个任务是,可以继续该任务。其中任务从保存到再次加载这个过程就是一次上下文切换。线程的启动、退出速度更快,对系统资源的冲击也更小。

线程数量考量:

线程数量并不是越多越好,过多的线程导致频繁的切换,还可能有一些线程一直都抢不到时间片。一般对于线程个数的使用,取决于使用场景:

  • 文件IO操作:文件IO对CPU的使用率不高(CPU包含控制器运算器,主要负责逻辑控制和运算),因此可以对CPU进行分时复用。当线程个数=CPU核心数*2时,效率最高。
  • 复杂运算:此时对CPU压力较大,线程的个数=CPU的核心数(效率最高)。

二. 创建线程

1. 函数

线程ID:

每一个线程都有一个唯一的ID,其类型为pthread_t,是一个无符号的长整型数,用于表示当前线程的ID。

  • 获取当前线程ID:

    #include <pthread.h>
    pthread_t pthread_self();	// 返回当前线程的线程ID
    // Compile and link with -pthread, 线程库的名字叫pthread, 全名: libpthread.so libptread.a。这是一个linux提供的动态链接库,注意在编译时指明库名
    

创建:

在一个进程中调用函数创建一个线程,该线程就成为了进程的子线程。需要注意的是:每一个子线程都有一个处理函数,用于指定该子线程的工作。

#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:处理函数,类型为函数指针,系统通过指针来调用该函数,从而让子线程进行工作。其返回值和参数类型都是void *,也就是泛型。

  • arg:处理函数的参数,将会作为实参传递到处理函数中。

  • -返回值:返回线程是否创建成功。成功返回0,失败会返回相应的错误编号。

2. 生命周期

当前进程创建除子线程后,当前进程就成为了主线程,其生命周期如下:

  • 主线程结束,则子线程也会死亡。因为主线程和子线程共用一片内存,而该内存一开始是是操作系统分配给主线程的,因此当主线程结束后,空间回收了,子线程自然也会随之死亡。
  • 子线程结束,不影响主线程继续运行。同样的道理,内存的存亡取决于主线程,而非子线程。

如何在子线程结束之前,不让主线程结束:

  • 阻塞线程:将主线程阻塞掉。例如使用sleep方法,让线程休眠一会。(注意:线程阻塞后会释放抢占的CPU资源,但不会释放锁资源。而通过wait,让线程进入等待状态,可以让线程释放CPU资源和锁资源。)
  • 线程退出:就是使用pthread_exit()函数,详情见第三节。

3. 实例

  • 代码如下:

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>
    
    // 子线程任务函数
    void* work(void* args){
        printf("我是子线程,ID=%ld\n", pthread_self());
        for (int i = 0; i < 10; ++i) {
            printf("child.i == %d \n", i);
        }
        return NULL;
    }
    
    int main(){
       // 创建一个子线程
        pthread_t tid;
        pthread_create(&tid, NULL, work, NULL);
        // 主线程输出
        printf("主线程ID==%ld\n", pthread_self());
        for (int i = 0; i < 10; i++) {
            printf("main.i == %d\n", i);
        }
        sleep(1);   // 位于unistd头文件中的函数,用于线程休眠,单位是秒,防止主线程内存释放过早导致子线程死亡。
        return 0;
    }
    
  • 编译:

    gcc thread.c -o app 
    

    结果发现报错:

    /usr/bin/ld: /tmp/ccvJmTRf.o: in function `main':
    thread.c:(.text+0x8d): undefined reference to `pthread_create'
    collect2: error: ld returned 1 exit status
    

    原因是pthread.h使用了linux提供的动态库,因此在编译时需要指定动态库的名字(这里因为是linux提供的,也就是说动态库文件已经在相应目录下了,因此无需指定目录),库的名字是:libpthread.so。将名字掐头去尾,因此指令改为:

    gcc thread.c -o app -l pthread
    

三. 线程退出

1. 概述

在编写多线程程序时,有时我们有如下需求:让线程退出的同时不释放虚拟地址空间(针对主线程),从而避免子线程随之死亡。这时我们就可以使用线程库中的线程退出函数。它可以让线程结束的同时不影响其它线程的正常运行,不论是在主线程还是在子线程中都可以使用。

注意:默认属性的线程执行结束之后并不会立即释放占用的资源,直到整个进程执行结束,所有线程的资源以及整个进程占用的资源才会被操作系统回收。

2. 函数

#include <pthread.h>
void pthread_exit(void *retval);

其中:

  • retavl:传出参数,用于在线程退出时携带数据,当前子线程的主线程会得到该数据。如果不需要,可以定义为NULL

3. 实例

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

// 子线程任务函数
void* work(void* args){
    printf("我是子线程,ID=%ld\n", pthread_self());
    for (int i = 0; i < 10; ++i) {
        if (i == 6) {
            pthread_exit(NULL);
        }
        printf("当前线程ID=%ld,i == %d \n", pthread_self(),i);
    }
    return NULL;
}

int main(){
   // 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, work, NULL);
    printf("子线程创建成功,ID==%ld\n", tid);
    // 主线程输出
    printf("主线程ID==%ld\n", pthread_self());
    for (int i = 0; i < 10; i++) {
        printf("当前线程ID=%ld, i == %d\n", pthread_self(), i);
    }
    // 主线程通过线程退出函数退出,进程的地址空间不会被释放
    pthread_exit(NULL);
    return 0;
}

四. 线程回收

1. 函数

线程和进程一样,子线程退出的时候其内核资源主要由主线程进行回收,线程库中提供的函数为pthread_join()

函数原型:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  • 参数:
    • thread:需要回收线程的ID。
    • retval:双重指针,是一个传出参数,这个地址中存储了pthread_exit()传递出的数据,如果不需要该参数,可以指定为NULL。
  • 特点:
    • 是一个阻塞函数,通过该函数回收一个子线程,如果该子线程还在运行,那么主线程就会被堵塞。
    • 该函数一次只能回收一个子线程资源,如果有多个,只能一个一个的回收

2. 回收子线程数据

pthread_join函数可以通过传出参数,获取pthread_exit传出的数据,但问题在于:pthread_exit会让子线程结束,此时子线程的数据还存在吗?如果存在,在哪?如果不存在,又怎么向主线程传输数据?通过下面的讨论,可以得知:

2.1 通过子线程栈

代码如下:

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

struct Node{
    int id;
    char name[20];
};
// 子线程任务函数
void* work(void* args){
    printf("我是子线程,ID=%ld\n", pthread_self());
    for (int i = 0; i < 10; ++i) {
        if (i == 6) {
            // 将数据存储在栈区
            struct Node node;
            node.id = 2;
            strcpy(node.name, "liSi");
            pthread_exit(&node);
        }
    }
    return NULL; 
}

int main(){
   // 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, work, NULL);
    printf("子线程创建成功,ID==%ld\n", tid);
    // 主线程输出
    printf("主线程ID==%ld\n", pthread_self());
    // join回收子线程资源
    void *retval = NULL;    // 传出的数据,注意使用void类型
    pthread_join(tid, &retval);
    struct Node *data = (struct Node*)retval;
    printf("子传出数据:id = %d, name = %s\n", data->id, data->name);
    printf("子线程资源成功回收\n");
    return 0;
}

编译运行:

gcc thread.c -o app -l pthread
beasts777@ubuntu:~/Coding/Practice/Thread$ ./app
子线程创建成功,ID==140107111216896
主线程ID==140107111221056
我是子线程,ID=140107111216896
子传出数据:id = 0, name = 		# 可以看到,这里的数据与设定的数据不一致
子线程资源成功回收

此时发现:输出的数据不正确。这是由于在多线程中,多个线程公用一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存。当子线程退出时,该线程在栈内的空间就被回收了,写入到该线程站内的数据自然也就找不到了。

2.2 使用主线程栈

分析:

虽然每个线程都有自己的栈空间,但位于同一个地址空间的多个线程可以互相访问对方的栈空间。因此我们可以让子线程中返回的数据存储在主线程的栈区中,这让即使子线程的栈空间被回收,数据依旧不会丢失。

代码:

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

struct Node{
    int id;
    char name[20];
};
// 子线程任务函数
void* work(void* args){
    // 获取主线程传递过来的参数
    struct Node * node = (struct Node*)args;
    printf("我是子线程,ID=%ld\n", pthread_self());
    for (int i = 0; i < 10; ++i) {
        if (i == 6) {
            // 将数据存储在栈区
            node->id = 2;
            strcpy(node->name, "liSi");
            pthread_exit(node);
        }
    }
    return NULL; 
}

int main(){
   // 创建一个子线程
    pthread_t tid;
    // 主线程在栈空间内开辟一块空间,并交给子线程的工作函数使用。
    struct Node temp;
    pthread_create(&tid, NULL, work, &temp);
    printf("子线程创建成功,ID==%ld\n", tid);
    // 主线程输出
    printf("主线程ID==%ld\n", pthread_self());
    // join回收子线程资源
    void *retval = NULL;    // 传出的数据,注意使用void类型
    pthread_join(tid, &retval);
    struct Node *data = (struct Node*)retval;
    printf("子传出数据:id = %d, name = %s\n", data->id, data->name);
    printf("子线程资源成功回收\n");
    return 0;
}

编译运行:

# 编译运行
gcc thread.c -o app -l pthread
./app
# 输出结果
子线程创建成功,ID==140224493106944
主线程ID==140224493111104
我是子线程,ID=140224493106944
子传出数据:id = 2, name = liSi
子线程资源成功回收

程序执行成功。

2.3 其它方式

  • 使用堆区
  • 使用全局变量

五. 线程分离

1. 需求分析

根据第四节中的描述,主线程使用pthread_join()回收子线程,如果子线程还没有退出,会导致主线程堵塞。但有时主线程有自己的任务,我们不希望主线程被阻塞,这会导致主线程的任务无法顺利进行,怎么办?

线程库中为我们提供了线程分离函数pthread_detach(),调用该函数后,子线程可以和主线程分离,当子线程退出时,其占用的内核资源就被系统的其它进程接管并回收。也就是说,此时子线程的内核资源不再由主线程继续回收,因此主线程也就无需使用pthread_join()函数,用了也会收不到子线程的资源了。

注意:这种分离只是状态上的分离,内存上还是和主线程共用一处,因此如果主线程return导致内存被回收,子线程仍会死亡。

2. 函数

#include <pthread.h>
int pthread_detch(pthread_t thread);

其中:

  • thread参数:要与主线程分离的子线程ID。
  • 返回值:分离成功为0,分离失败返回错误号。

3. 实例

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

struct Node{
    int id;
    char name[20];
};
// 子线程任务函数
void* work(void* args){
    // 获取主线程传递过来的参数
    printf("我是子线程,ID=%ld\n", pthread_self());
    for (int i = 0; i < 10; ++i) {
        printf("子线程输出:%d\n", i);
    }
    return NULL; 
}

int main(){
   // 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, work, NULL);
    printf("子线程创建成功,ID==%ld\n", tid);
    // 主线程输出
    printf("主线程ID==%ld\n", pthread_self());
    for (int i = 0; i < 5; ++i) {
        printf("主线程输出:%d\n", i);
    }
    // 线程分离
    pthread_detach(tid);
    // 主线程退出
    pthread_exit(NULL);
    return 0;
}

经测试,运行成功。

六. 其它线程函数

1. 线程取消

1.1 简介

顾名思义,就是在某些特定的情况下在一个线程中杀死另一个线程。函数起作用分为两步:

  1. 在线程A中调用线程取消函数pthread_cancel(),杀死线程B,这时候线程B是死不了的。
  2. 在线程B中进行一次系统调用(从用户态转到内核态),否则线程B不会被结束。

系统调用:有两种实现方式:

  • 直接调用Linux系统函数。
  • 调用标准C库函数,而有些C库函数会调用相关的系统函数。如printf(),会调用输出的系统函数。

1.2 函数

#include <pthread.h>
int pthread_cancel(pthread_t thread);

其中:

  • thread参数:要杀死的线程的ID。
  • 返回值:调用成功返回0,调用失败返回错误码。

1.3 实例

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

2. 线程比较

Linux中线程ID本质是一个无符号长整形,因此可以使用操作符进行直接比较。但是线程库是可以跨平台使用的,在某些平台上,pthread_t并不是一个单纯的整形,因此为了应对这种情况,在对线程ID进行比较时需要使用比较函数。

#include <pthread.h>
int pthread_equal(pthead_t t1, pthread_t t2);

其中:

  • t1、t2:参与比较的两个线程的ID。
  • 返回值:两个线程相等返回非0值。如果不相等,返回0。

标签:printf,多线程,Linux,主线,线程,pthread,include,ID
From: https://www.cnblogs.com/beasts777/p/17848567.html

相关文章

  • linux11.08课堂随笔
    第5章进程管理一、静态查看进程ps命令可以查看静态进程,仅仅是捕捉某一个瞬间某一个进程的状态,类似于给进程制作快照。1.查看进程psaux2.查看CPU占用率psaux--sort-%cpu3.查看UID、PID、PPID等信息ps-ef快速查找psaxo命令自定义显示的字段4.查看指定进程PID(1)cat......
  • Linux第三次博客
     这次主要讲了第四章的内容——文件权限。 其中,讲了基本权限UGO。U是属主(owner),G是属组(group),O其他用户(other)。三类用户分别设置了三种基本权限,三种权限为r读取(read)数字设定为4可读取文件内容,w写入(write)数字设定为2可修改文件内容,x执行(execute)数字设定为1可将文件作为命令执......
  • 初始Linux
    探索Linux:开源世界的支柱在当今科技的前沿,Linux操作系统一直是开源世界的支柱和关键元素。它不仅仅是一个操作系统,更是一个哲学,一种思想的象征。通过开源的特性,Linux向世界宣示着自由、透明和合作的力量。Linux的起源1980年代末,芬兰大学生LinusTorvalds开始着手创建一个......
  • Linux文件管理
    一:文件目录 根目录下常见的目录:bin:普通用户使用的命令(存放二进制可执行文件(ls,cat,mkdir等))boot:存放系统启动相关的文件dev:设备文件(硬件)etc:配置文件home:普通用户的文件root:root(超级管理)用户的HOMEsbin:管理员使用的命令tmp:临时文件usr:系统文件,相当于C:\Windowsva......
  • Linux进程管理
    5.1初识进程进程是已启动的可执行程序的运行实例。进程有以下组成部分。.已分配内存的地址空间。·安全属性,包括所有权凭据和特权。●程序代码的一个或多个执行线程。·进程状态。每个进程都有唯一的进程标识PID,一个PID只能标识一个进程,PPID为父进程ID,需要给该进程分配系......
  • linux读书笔记第6章
    在Linux的第6章中,主要学习了I/O重定向和管道的内容。以下是关于这两个主题的学习总结:1.I/O重定向:Linux中的I/O重定向是一种机制,可以将标准输入、标准输出和标准错误输出从默认的设备(通常是终端)重定向到其他地方。可以使用符号">"来将输出重定向到文件中,使用符号">>"来追加输......
  • 学习linux文件操作
    这节课开始学习文件和文件夹的创建、复制、移动和删除。touch命令让我能够创建新文件,cp和mv命令使我可以复制和移动文件或目录。对于文件删除,rm命令虽然强大,但也需要小心使用,以免误删重要文件。Linux的文件权限系统也是我学习的重要部分。chmod命令允许我更改文件的权限,而chown命......
  • linux用户管理
    用户ID(UID)在用户ID中0是超级用户的ID,只要UID是0就是超级用户。初始组ID(GID)为更加灵活的管理用户的权限,Linux里还采用用户组的概念。管理用户/组1创建用户qf01useraddqf012.创建用户组hrgroupaddhr3.将用户添加到指定用户组useraddqf01-Ghruseradd【选项】用......
  • Linux课堂知识总结4
    在此次课堂学习中,我掌握了基本权限用法,掌握了高级权限用法,权限的意义在于允许某一个用户或某个用户组以规定方式去访问某个文件。三种基本权限读权限r写权限w执行权限x对文件来说r:可读取文件的内容w:可修改文件的内容x:可执行文件的内容对目录来说r:可列出目录中的文件列......
  • Linux操作系统 no.7
    进程管理:1.查看进程:psaux 2.ps-ef命令可以查看UID,PID,PPID等信息。 3.top命令可以查看实时动态进程 4.kill命令可以用来终止指定程序5. 6.作业控制:创建一个sleep进程,使用CTRL+c可以终止程序 ......