Linux软件编程学习笔记(五)——线程

一、什么是线程

进程内部的一条执行路径,是 CPU 调度的基本单位。

一个进程至少包含一个线程:

复制代码
进程
 │
 └── 主线程

一个进程也可以包含多个线程:

复制代码
             进程
               │
      ┌────────┼────────┐
      │        │        │
    线程1    线程2    线程3

例如一个程序同时需要:

复制代码
采集数据
保存数据
显示数据
记录日志

就可以设计成:

复制代码
进程
 │
 ├── 采集线程
 ├── 存储线程
 ├── 显示线程
 └── 日志线程

这也是多线程程序最典型的应用场景。


二、进程和线程的关系

可以把进程理解为一个"容器"。

复制代码
进程
┌──────────────────────────┐
│                          │
│   代码                    │
│   全局数据                │
│   堆                      │
│   打开的文件              │
│                          │
│   ┌──────┐ ┌──────┐      │
│   │线程1 │ │线程2 │ ...  │
│   └──────┘ └──────┘      │
│                          │
└──────────────────────────┘

多个线程属于同一个进程,因此:

同一进程中的多个线程共享进程的大部分资源。


三、线程和进程的主要区别

对比 进程 线程
资源 资源分配的基本单位 不独立拥有完整进程资源
调度 调度单位之一 CPU 调度的基本单位
地址空间 独立 同一进程中的线程共享
创建开销 较大 较小
通信 相对复杂 共享内存,通信方便
数据共享 需要 IPC 可以直接访问共享变量
稳定性 相对独立 一个线程出问题可能影响整个进程

最重要的一句话:

复制代码
进程:资源分配的基本单位
线程:CPU调度的基本单位

四、为什么需要线程

假设有一个程序:

复制代码
主程序
 │
 ├── 采集数据
 ├── 保存数据
 ├── 显示数据
 └── 记录日志

如果全部使用一个线程:

复制代码
采集
 ↓
保存
 ↓
显示
 ↓
日志
 ↓
采集
 ↓
...

那么任务之间容易相互等待。

使用多线程后:

复制代码
                 进程
                   │
        ┌──────────┼──────────┐
        ↓          ↓          ↓
     采集线程    保存线程    显示线程
        │          │          │
        └──────────┼──────────┘
                   ↓
                日志线程

多个线程可以并发执行。


五、Linux 中的线程

Linux 中常用的是:

复制代码
POSIX线程

也叫:

复制代码
pthread

使用线程相关函数时,需要包含:

复制代码
#include <pthread.h>

编译时通常需要:

复制代码
gcc xxx.c -o xxx -pthread

或者:

复制代码
gcc xxx.c -o xxx -lpthread

推荐使用:

复制代码
gcc xxx.c -o xxx -pthread

六、第一个线程程序

最基本的线程创建函数:

复制代码
pthread_create()

函数原型:

复制代码
int pthread_create(
    pthread_t *thread,
    const pthread_attr_t *attr,
    void *(*start_routine)(void *),
    void *arg
);

参数:

复制代码
thread
   ↓
保存线程ID

attr
   ↓
线程属性
通常使用 NULL

start_routine
   ↓
线程执行函数

arg
   ↓
传给线程函数的参数

七、创建线程

最简单的例子:

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

void *thread_fun(void *arg)
{
    printf("子线程开始执行\n");

    return NULL;
}

int main(void)
{
    pthread_t tid;

    pthread_create(&tid, NULL, thread_fun, NULL);

    printf("主线程执行\n");

    sleep(1);

    return 0;
}

编译:

复制代码
gcc test.c -o test -pthread

运行:

复制代码
./test

八、pthread_create() 的返回值

注意:

复制代码
pthread_create()

与很多 Linux 系统调用的错误处理方式不同。

成功:

复制代码
0

失败:

复制代码
非0

所以通常写:

复制代码
int ret = 0;

ret = pthread_create(&tid, NULL, thread_fun, NULL);

if(ret != 0)
{
    printf("pthread_create failed\n");
    return -1;
}

九、线程函数

线程函数的格式非常固定:

复制代码
void *thread_fun(void *arg)
{
    // 线程执行代码

    return NULL;
}

为什么是:

复制代码
void *

因为线程函数可以接收和返回任意类型的数据。

参数:

复制代码
void *arg

用于接收创建线程时传递的数据。


十、pthread_create() 的执行过程

例如:

复制代码
pthread_create(&tid, NULL, thread_fun, NULL);

可以理解成:

复制代码
主线程
   │
   │ pthread_create()
   ↓
创建新线程
   │
   ↓
thread_fun()
   │
   ↓
执行线程代码

十一、主线程和子线程

一个程序启动后,首先会存在一个:

复制代码
主线程

当调用:

复制代码
pthread_create()

以后:

复制代码
             主线程
                │
          pthread_create()
                │
          ┌─────┴─────┐
          ↓           ↓
        主线程       新线程

此后两个线程都可以执行。


十二、线程执行顺序是不确定的

例如:

复制代码
printf("A\n");

pthread_create(...);

printf("B\n");

新线程中:

复制代码
printf("C\n");

最终可能:

复制代码
A
B
C

也可能:

复制代码
A
C
B

因为:

线程的具体执行顺序由操作系统调度决定。

所以不能根据代码书写顺序,简单判断多个线程的输出顺序。


十三、pthread_self() ------ 获取线程ID

线程可以使用:

复制代码
pthread_self()

获取自己的线程 ID。

函数:

复制代码
pthread_t pthread_self(void);

例如:

复制代码
printf("TID = %lu\n",
       (unsigned long)pthread_self());

十四、线程ID

进程有:

复制代码
PID

线程也有:

复制代码
TID

可以简单理解:

复制代码
进程
 ↓
PID

线程
 ↓
TID

例如:

复制代码
void *thread_fun(void *arg)
{
    printf("线程ID = %#lx\n",
           (unsigned long)pthread_self());

    return NULL;
}

十五、pthread_join() ------ 等待线程

线程创建之后,主线程可能先执行结束。

例如:

复制代码
pthread_create(...);

return 0;

如果主线程直接退出:

复制代码
主线程结束
 ↓
整个进程结束
 ↓
其他线程也会结束

因此需要一种机制:

让主线程等待子线程执行完成。

这就是:

复制代码
pthread_join()

十六、pthread_join() 函数

函数原型:

复制代码
int pthread_join(
    pthread_t thread,
    void **retval
);

最简单的使用方式:

复制代码
pthread_join(tid, NULL);

表示:

等待指定线程结束。


十七、pthread_join() 示例

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

void *thread_fun(void *arg)
{
    printf("子线程开始\n");

    sleep(3);

    printf("子线程结束\n");

    return NULL;
}

int main(void)
{
    pthread_t tid;

    pthread_create(&tid, NULL, thread_fun, NULL);

    pthread_join(tid, NULL);

    printf("主线程结束\n");

    return 0;
}

执行:

复制代码
子线程开始

等待 3 秒:

复制代码
子线程结束
主线程结束

十八、pthread_join() 的作用

主要有两个作用:

1. 等待线程结束

复制代码
pthread_join(tid, NULL);

2. 获取线程退出信息

如果线程返回了数据:

复制代码
return ...

主线程可以通过:

复制代码
pthread_join()

获取。


十九、线程返回值

例如:

复制代码
void *thread_fun(void *arg)
{
    int *p = NULL;

    p = malloc(sizeof(int));

    *p = 100;

    return p;
}

主线程:

复制代码
void *retval = NULL;

pthread_join(tid, &retval);

printf("result = %d\n",
       *(int *)retval);

free(retval);

这里需要注意:

返回的数据必须保证在线程结束以后仍然有效。

不能直接返回线程栈上的局部变量地址。

错误:

复制代码
void *thread_fun(void *arg)
{
    int num = 100;

    return &num;
}

因为:

复制代码
num
 ↓
线程函数结束
 ↓
栈空间失效

返回这个地址是不安全的。


二十、pthread_exit() ------ 线程退出

线程可以使用:

复制代码
pthread_exit()

主动结束。

函数:

复制代码
void pthread_exit(void *retval);

例如:

复制代码
void *thread_fun(void *arg)
{
    printf("线程执行\n");

    pthread_exit(NULL);
}

二十一、return 和 pthread_exit

在线程函数中:

复制代码
return NULL;

和:

复制代码
pthread_exit(NULL);

都可以结束当前线程。

例如:

复制代码
void *thread_fun(void *arg)
{
    printf("hello\n");

    return NULL;
}

或者:

复制代码
void *thread_fun(void *arg)
{
    printf("hello\n");

    pthread_exit(NULL);
}

都可以。


二十二、pthread_cancel() ------ 取消线程

线程还可以被其他线程取消:

复制代码
pthread_cancel(tid);

作用:

请求取消指定线程。

例如:

复制代码
pthread_cancel(tid);

需要注意:

pthread_cancel() 是取消请求,并不应该简单理解为立即强制杀死线程。


二十三、线程传参

前面:

复制代码
pthread_create(
    &tid,
    NULL,
    thread_fun,
    NULL
);

最后一个参数:

复制代码
NULL

就是传给线程函数的:

复制代码
void *arg

如果需要传递数据:

复制代码
pthread_create(
    &tid,
    NULL,
    thread_fun,
    &num
);

线程函数:

复制代码
void *thread_fun(void *arg)
{
    int num = *(int *)arg;

    printf("num = %d\n", num);

    return NULL;
}

二十四、线程传递一个整数

完整示例:

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

void *thread_fun(void *arg)
{
    int num = *(int *)arg;

    printf("num = %d\n", num);

    return NULL;
}

int main(void)
{
    pthread_t tid;
    int num = 100;

    pthread_create(&tid,
                   NULL,
                   thread_fun,
                   &num);

    pthread_join(tid, NULL);

    return 0;
}

运行:

复制代码
num = 100

二十五、线程传递结构体

实际项目中通常需要传递多个参数。

这时候可以使用结构体。

复制代码
typedef struct
{
    int id;
    char name[32];
} User;

创建变量:

复制代码
User user =
{
    1001,
    "Tom"
};

传给线程:

复制代码
pthread_create(
    &tid,
    NULL,
    thread_fun,
    &user
);

线程中:

复制代码
void *thread_fun(void *arg)
{
    User *user = (User *)arg;

    printf("id = %d\n", user->id);
    printf("name = %s\n", user->name);

    return NULL;
}

二十六、创建多个线程

例如:

复制代码
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;

分别创建:

复制代码
pthread_create(&tid1, NULL, thread_fun1, NULL);
pthread_create(&tid2, NULL, thread_fun2, NULL);
pthread_create(&tid3, NULL, thread_fun3, NULL);

然后等待:

复制代码
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);

整体结构:

复制代码
                主线程
                  │
       ┌──────────┼──────────┐
       ↓          ↓          ↓
    线程1       线程2       线程3
       │          │          │
       └──────────┼──────────┘
                  ↓
              pthread_join
                  ↓
                结束

二十七、多线程共享数据

线程和进程最大的区别之一:

同一个进程中的多个线程共享进程的地址空间。

例如:

复制代码
int num = 0;

多个线程都可以访问:

复制代码
              num
               │
       ┌───────┼───────┐
       ↓       ↓       ↓
     线程1   线程2   线程3

这既是线程的优势,也是线程产生问题的重要原因。


二十八、线程共享哪些资源

同一进程中的线程通常共享:

复制代码
代码段
全局变量
静态变量
堆
打开的文件描述符
进程地址空间

但是每个线程也有自己独立的部分,例如:

复制代码
线程ID
寄存器
栈
程序计数器

可以简单记忆:

复制代码
共享:
代码、全局变量、堆、文件

独立:
栈、寄存器、线程ID

相关推荐
吴声子夜歌15 分钟前
Java——数组和集合(一)
java·开发语言
呆呆敲代码的小Y23 分钟前
Lua 上值(UpValue)
开发语言·junit·lua·lua上值·upvalue
呆呆敲代码的小Y27 分钟前
【游戏开发】C# 中的迭代器
java·开发语言·c#·迭代器
benchmark_cc29 分钟前
K 线数据断层会对回测造成什么影响?从一个缺失交易日看懂量化策略为什么会失真
大数据·开发语言·数据分析·量化·股票数据·quantdash·量化数据源
benchmark_cc34 分钟前
数据 API 稳定性为什么会影响量化策略?从数据获取到信号执行的完整分析
开发语言·python·数据分析·量化·股票数据·quantdash·量化数据源
学习星球39 分钟前
空天地一体化网络(NTN)深度解析:从Starlink D2C到3GPP NTN,卫星直连手机是如何实现的?
网络·人工智能·算法·智能手机·php
2601_962180331 小时前
python——Django 框架
开发语言·python·django
长江后浪博客1 小时前
Conda环境下测试Intel NPU:Python版本如何选择?
开发语言·python·conda·openvino·intel npu
Chester_19991 小时前
CSP202206C.角色授权
开发语言·数据结构·c++·蓝桥杯