1.线程

主要根据这篇文章进行总结学习https://subingwen.cn/linux/thread/

1.线程概述

线程是轻量级进程,在Linux环境下线程的本质仍是进程。操作系统会以进程为单位,分配系统资源,可以这样理解,进程是资源分配的最小单位,线程是操作系统调度执行的最小单位。
先从概念上了解一下线程和进程之间的区别:

  • 进程有自己独立的地址空间, 多个线程共用同一个地址空间。(进程住单间,线程合租房子)
    • 线程更加节省系统资源, 效率不仅可以保持的, 而且能够更高。(开销少)
    • 在一个地址空间中多个线程独享: 每个线程都有属于自己的栈区, 寄存器(内核中管理的)。(不是所有东西都是共享的,比如床,栈区是用来存储临时变量的)
    • 在一个地址空间中多个线程共享: 代码段, 堆区, 全局数据区, 打开的文件(文件描述符表)都是线程共享的
  • 线程是程序的最小执行单位, 进程是操作系统中最小的资源分配单位
    • 每个进程对应一个虚拟地址空间,一个进程只能抢一个CPU时间片。(线程虽然占用的系统资源少了,但是效率并没有降低)
    • 一个地址空间中可以划分出多个线程, 在有效的资源基础上, 能够抢更多的CPU时间片
      image

CPU的调度和切换: 线程的上下文切换比进程要快的多
上下文切换:进程/线程分时复用CPU时间片,在切换之前会将上一个任务的状态进行保存, 下次切换回这个任务的时候, 加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换。
线程更加廉价, 启动速度更快, 退出也快, 对系统资源的冲击小。
在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是线程并不是越多越好,如何控制线程的个数呢?
文件IO操作:文件IO对CPU是使用率不高, 因此可以分时复用CPU时间片, 线程的个数 = 2 * CPU核心数 (效率最高)
处理复杂的算法(主要是CPU进行运算, 压力大),线程的个数 = CPU的核心数 (效率最高)

2.创建线程

2.1 线程函数
每一个线程都有一个唯一的线程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,创建失败返回对应的错误号

主线程执行完毕之后就会释放对应的虚拟地址空间,子线程也就不存在了。

点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* callback(void* arg)
{
        for(int i=0; i<5; i++)
        {
                printf("子线程:i = %d\n", i);
        }
        printf("子线程: %ld\n", pthread_self());
        return NULL;
}

int main()
{
        pthread_t tid;
        pthread_create(&tid, NULL, callback, NULL);
        for(int i=0; i<5; i++)
        {
                printf("主线程:i = %d\n", i);
        }
        printf("主线程: %ld\n", pthread_self());
        sleep(3);//让主线程休眠释放CPU资源
        return 0;
}

3.线程退出

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

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

参数: 线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为NULL

点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* callback(void* arg)
{
        for(int i=0; i<5; i++)
        {
                printf("子线程:i = %d\n", i);
        }
        printf("子线程: %ld\n", pthread_self());
        return NULL;
}

int main()
{
        pthread_t tid;
        pthread_create(&tid, NULL, callback, NULL);
        printf("主线程: %ld\n", pthread_self());
        pthread_exit(NULL);
        //sleep(3);
        return 0;
}

运行结果
主线程: 139917743331136
子线程:i = 0
子线程:i = 1
子线程:i = 2
子线程:i = 3
子线程:i = 4
子线程: 139917734844160
主线程退出后,地址空间没有被释放,子线程还是可以向下执行,最后虚拟地址空间被系统回收。

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回收子线程数据

接收数据有很多种处理方式,下面来列举几种:

4.2.1使用子线程栈

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

4.2.2使用全局变量

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

4.2.3 使用主线程栈

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

点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

struct Test
{
        int num;
        int age;
};

struct Test t;//这句,如果在线程里会出现情况1,在这里会出现情况2

void* callback(void* arg)
{
        for(int i=0; i<5; i++)
        {
                printf("子线程:i = %d\n", i);
        }
        printf("子线程: %ld\n", pthread_self());

        t.num = 100;
        t.age = 6;

        pthread_exit(&t);

        return

主线程的栈空间传递给子线程

点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

struct Test
{
        int num;
        int age;
};

void* callback(void* arg)
{
        for(int i=0; i<5; i++)
        {
                printf("子线程:i = %d\n", i);
        }
        printf("子线程: %ld\n", pthread_self());
        struct Test* t = (struct Test*)arg;
        t->num = 100;
        t->age = 6;

        pthread_exit(t);

        return NULL;
}

int main()
{
        struct Test t;
        pthread_t tid;
        pthread_create(&tid, NULL, callback, &t);
        printf("主线程: %ld\n", pthread_self());

        void* ptr;
        pthread_join(tid, &ptr);
        printf("num = %d, age = %d\n", t.num, t.age);
        return 0;
}

情况1
主线程: 139746439665472
子线程:i = 0
子线程:i = 1
子线程:i = 2
子线程:i = 3
子线程:i = 4
子线程: 139746431178496
num = 1071878144, age = 32537
原因是,如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。

情况2
子线程:i = 0
子线程:i = 1
子线程:i = 2
子线程:i = 3
子线程:i = 4
子线程: 139644614100736
主线程: 139644622587712
num = 100, age = 6

5.线程分离

在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用pthread_join()只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。
在线程库函数中为我们提供了线程分离函数pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()就回收不到子线程资源了。
**正常情况下主线程退出的时候会释放子线程的资源,但是线程分离之后主线程退出的时候就不会释放子线程的资源

#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

struct Test
{
        int num;
        int age;
};

void* callback(void* arg)
{
        for(int i=0; i<5; i++)
        {
                printf("子线程:i = %d\n", i);
        }
        printf("子线程: %ld\n", pthread_self());
        struct Test* t = (struct Test*)arg;
        t->num = 100;
        t->age = 6;

        pthread_exit(t);

        return NULL;
}

int main()
{
        struct Test t;
        pthread_t tid;
        pthread_create(&tid, NULL, callback, &t);
        printf("主线程: %ld\n", pthread_self());

        pthread_detach(tid);

        pthread_exit(NULL);

        return 0;
}

6.其他线程函数

6.1 线程取消

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

  • 在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,这时候线程B是死不了的
  • 在线程B中进行一次系统调用(从用户区切换到内核区),否则线程B可以一直运行。
#include <pthread.h>
// 参数是子线程的线程ID
int pthread_cancel(pthread_t thread);

参数:要杀死的线程的线程ID
返回值:函数调用成功返回0,调用失败返回非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

posted @ 2026-01-13 21:08  r5ett  阅读(2)  评论(0)    收藏  举报