一、什么是线程
进程内部的一条执行路径,是 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
↓
线程函数结束
↓
栈空间失效
返回这个地址是不安全的。
二十、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