每个用户进程有自己的地址空间。
线程是操作系统与多线程编程的基础知识。
系统为每个用户进程创建一个 task_struct
来描述该进程:该结构体中包含了一个指针指向该进程的虚拟地址空间映射表:
实际上 task_struct
和地址空间映射表一起用来表示一个进程。由于进程的地址空间是私有的,因此在进程间上下文切换时,系统开销比较大,因此,为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程。在同一个进程中创建的线程共享该进程的地址空间,Linux里同样用task_struct
来描述一个线程。线程和进程都参与统一的调度。
通常线程指的是共享相同地址空间的多个任务:
线程:
1> 线程是轻量级的进程
2> 线程的体现是函数
-- 在一个进程中,多个函数通知执行(多线程的任务)
3> 线程依赖于进程
4> 线程是CPU调度和执行的最小单位
5> 同一个进程中的线程,共享该进程的地址空间
6> 多线程通过第三方的线程库来实现 -- pthread
7> 线程创建的上限受栈空间影响
ulimit -a
stack size (kbytes, -s) 8192
1- 一个进程中的多个线程共享以下资源
可执行的指令
静态数据
进程中打开的文件描述符
信号处理函数
当前工作目录
用户ID
用户组ID
2- 每个线程私有的资源如下
线程ID (TID) -- gettid() pthread_self()
PC(程序计数器)和相关寄存器
堆栈
局部变量
返回地址
错误号 (errno)
信号掩码和优先级 0-144
执行状态和属性
多线程通过第三方的线程库来实现:New POSIX Thread Library (NPTL)
- 是早期
Linux Threads
的改进 - 采用1:1的线程模型
- 显著的提高了运行效率
- 信号处理效率更高
接下来我们会详细的介绍线程共享资源和私有资源,还有线程库如pthread
,以及线程的限制如栈空间。
线程(Thread)
线程是操作系统进行 CPU 调度和执行的最小单位,是轻量级的进程(Lightweight Process),允许多个执行流在同一个进程内并发运行。
一、线程的核心特性
- 轻量级进程
- 线程共享进程的地址空间和资源,创建、切换、销毁的开销远小于进程。
- 示例:一个浏览器进程可包含多个线程(渲染线程、网络线程、UI线程)。
- 线程的体现是函数
- 线程的执行入口是一个函数,多个线程对应多个并行执行的函数。
- 示例代码(POSIX线程):
#include <pthread.h>
void *thread_func(void *arg) {
printf("Thread ID: %lu\n", pthread_self());
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL);
return 0;
}
- 依赖进程存在
- 线程无法独立存在,必须依附于进程。
- 进程终止时,所有线程会被强制终止。
- CPU 调度的最小单位
- 操作系统调度器直接管理线程,而非进程。
- 线程的优先级和调度策略可独立设置(如 SCHED_FIFO, SCHED_RR)。
- 共享进程地址空间
- 同一进程的所有线程共享:
- 代码段:可执行指令。
- 数据段:全局变量、静态变量。
- 堆栈:动态分配的内存。
- 文件描述符:打开的文件、网络套接字。
- 同一进程的所有线程共享:
- 第三方线程库实现
- POSIX 线程库(
pthread
):Linux/Unix 标准实现。 - Windows API:
CreateThread
函数。
- POSIX 线程库(
- 线程创建限制
- 线程数量受 栈空间大小 限制(通过
ulimit -s
查看)。 - 默认栈大小(Linux 通常为 8MB),可通过属性调整:
- 线程数量受 栈空间大小 限制(通过
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 2 * 1024 * 1024); // 2MB
pthread_create(&tid, &attr, thread_func, NULL);
二、线程的共享资源与私有资源
- 共享资源(进程级别)
资源类型 | 说明 |
---|---|
可执行指令 | 进程的代码段(.text) |
静态数据 | 全局变量、静态变量(.data, .bss) |
文件描述符 | 打开的文件、管道、套接字 |
信号处理函数 | signal() 或 sigaction() 注册的处理函数 |
当前工作目录 | 进程的 cwd (可通过 chdir() 修改) |
用户ID和组ID | 进程的权限身份 |
- 私有资源(线程级别)
资源类型 | 说明 |
---|---|
线程ID(TID) | 内核级ID(gettid() )或用户级ID(pthread_self() ) |
程序计数器(PC) | 当前执行的指令地址 |
寄存器状态 | CPU 寄存器的当前值(如 EAX, EBX) |
堆栈 | 存储局部变量、函数调用链 |
局部变量 | 函数内定义的自动变量 |
错误号(errno) | 线程独立的错误状态码 |
信号掩码 | pthread_sigmask() 设置的阻塞信号集合 |
调度优先级 | 范围通常为 0(最低)到 99(实时优先级) |
执行状态 | 运行、就绪、阻塞等 |
三、线程与进程的对比
特性 | 进程 | 线程 |
---|---|---|
资源开销 | 高(独立地址空间、文件描述符) | 低(共享进程资源) |
创建速度 | 慢(需复制父进程资源) | 快(仅分配栈和少量数据结构) |
通信机制 | 复杂(管道、共享内存等) | 简单(共享全局变量) |
容错性 | 高(一个进程崩溃不影响其他) | 低(一个线程崩溃导致进程终止) |
CPU 利用率 | 低(上下文切换开销大) | 高(切换快速) |
线程的应用场景:
- 高并发服务器:每个客户端连接由一个线程处理(如 Web 服务器 Apache)。
- 实时数据处理:数据采集线程 + 处理线程 + 存储线程(如音视频流处理)。
- GUI 应用程序:UI 主线程 + 后台计算线程(避免界面卡顿)。
- 并行计算:多线程分割任务加速计算(如矩阵运算)。
四、线程的操作(线程API都是pthread_ 开头)
- 创建新线程
#include <pthread.h>
typedef void *(*start_routine) (void *) //一个函数指针指向指针函数
要求:
1> 返回值类型 void *
2> 参数列表 只能有一个 void * 类型的参数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread:线程的ID号
attr:线程的属性 NULL 表示使用默认属性
start_routine:线程处理函数 -- 专门处理多线程任务的
arg:传递给线程处理函数的参数 NULL 表示不需要传参
返回值:
成功: 0
失败:一个错误号
Compile and link with -pthread.
编译线程的程序需要加 -lpthread
gcc 1-pthread_creat.c -lpthread
进程的结束是判断main函数是否结束,如果main函数结束,
进程会直接结束,会直接回收其他线程的内容
创建进程:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#if 0
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread:线程的ID号
attr:线程的属性 NULL 表示使用默认属性
start_routine:线程处理函数 -- 专门处理多线程任务的
arg:传递给线程处理函数的参数 NULL 表示不需要传参
返回值:
成功: 0
失败:一个错误号
#endif
typedef struct
{
int a;
float b;
}Stu_t;
//线程处理函数:(名称自定义)
//返回值类型必须是void *
//参数列表只能有一个 void *类型的参数
void *pthread_task(void *arg)
{
//printf("收到大哥的指示:%c\n",*(char *)arg); //接收单个字符
//printf("收到大哥的指示:%d\n",*(int *)arg); //接收int数据
//printf("收到大哥的指示:%s\n",(char *)arg); //接收字符串
Stu_t *temp = (Stu_t *)arg;
printf("%d %f \n",temp->a,temp->b); //接收一个结构体
return (void *)0;
}
int main()
{
int n = 7;
char c = 'a';
char str[] = "hello\n";
Stu_t s1 = {10,12.34};
//func();
//开新线程
pthread_t tid; //接收新线程的tid
//int ret = pthread_create(&tid,NULL,pthread_task,NULL ); // 这句话之后线程处理函数开始运行
//int ret = pthread_create(&tid,NULL,pthread_task, &c); //传递单个字符
//int ret = pthread_create(&tid,NULL,pthread_task, &n); //传递一个int 类型的数据
//int ret = pthread_create(&tid,NULL,pthread_task, str); //传递一个字符串
int ret = pthread_create(&tid,NULL,pthread_task, &s1); //传递一个结构体
if(ret != 0)
{
perror("pthread_create");
return -1;
}
sleep(1);
}
- 线程的退出
#include <pthread.h>
void pthread_exit(void *retval);
// 参数:
// retval:遗言
// 返回值:无
Compile and link with -pthread.
- 线程的等待收尸
#include <pthread.h>
//阻塞当前线程,等到指定线程结束
int pthread_join(pthread_t thread, void **retval);
// 参数:
// thread:线程号
// retval:接收线程的遗言 NULL 表示不关系
// 返回值:
// 成功: 0
// 失败:一个错误号
Compile and link with -pthread.
杀死进程与收尸:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#if 0
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread:线程的ID号
attr:线程的属性 NULL 表示使用默认属性
start_routine:线程处理函数 -- 专门处理多线程任务的
arg:传递给线程处理函数的参数 NULL 表示不需要传参
返回值:
成功: 0
失败:一个错误号
#endif
//线程处理函数:(名称自定义)
//返回值类型必须是void *
//参数列表只能有一个 void *类型的参数
void *pthread_task(void *arg)
{
int n = 7;
while(n--)
{
printf("笑死,我是你爹\n");
sleep(1);
}
// return (void *)0;
//pthread_exit(NULL); //退出当前线程,没有遗言
//pthread_exit("穿山甲到底说了啥"); //退出当前线程,有遗言
static int a = 100;
pthread_exit((void *)(&a));
}
int main()
{
int n = 7;
//开新线程
pthread_t tid;
//开启新线程 pthread_task 采用默认属性,不传递参数
int ret = pthread_create(&tid,NULL,pthread_task,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
while(n--)
{
printf("我是老大\n");
}
//pthread_join(tid,NULL); //阻塞当前线程,等待指定tid的线程结束 NULL不关系遗言
//int pthread_join(pthread_t thread, void **retval);
#if 0
char *retval = NULL; //避免野指针的出现
pthread_join(tid,(void **)&retval); //接收遗言
printf("%s\n",retval);
#endif
#if 1
int *retval;
pthread_join(tid,(void **)&retval); //接收遗言
printf("%d\n",*retval);
#endif
printf("------------------------------\n");
pthread_exit(0);
}
- 线程的属性设置
分离属性:线程结束后,线程自动回收自己空间。
非分离属性(默认): 线程结束后,最后同一由main()
线程收尸。
如何设置设置线程属性?
1> 先创建线程,在设置
#include <pthread.h>
//将指定的线程设置为分离属性
int pthread_detach(pthread_t thread);
参数:
thread:线程的ID号
返回值:
成功: 0
失败:一个错误号
Compile and link with -pthread.
非分离属性(默认)示例代码:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#if 0
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread:线程的ID号
attr:线程的属性 NULL 表示使用默认属性
start_routine:线程处理函数 -- 专门处理多线程任务的
arg:传递给线程处理函数的参数 NULL 表示不需要传参
返回值:
成功: 0
失败:一个错误号
#endif
//线程处理函数:(名称自定义)
//返回值类型必须是void *
//参数列表只能有一个 void *类型的参数
void *pthread_task(void *arg)
{
printf("%ld -- 开启\n",pthread_self()); //pthread_self()获取自己的tid
printf("%ld -- 关闭\n",pthread_self());
}
int main()
{
//func();
//开新线程
pthread_t tid;
int count = 0;
while(1)
{
//开启新线程 pthread_task 采用默认属性,不传递参数
int ret = pthread_create(&tid,NULL,pthread_task,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
printf("cout = %d\n",count ++);
}
}
分离属性示例代码:
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#if 0
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread:线程的ID号
attr:线程的属性 NULL 表示使用默认属性
start_routine:线程处理函数 -- 专门处理多线程任务的
arg:传递给线程处理函数的参数 NULL 表示不需要传参
返回值:
成功: 0
失败:一个错误号
#endif
//线程处理函数:(名称自定义)
//返回值类型必须是void *
//参数列表只能有一个 void *类型的参数
void *pthread_task(void *arg)
{
printf("%ld -- 开启\n",pthread_self()); //pthread_self()获取自己的tid
printf("%ld -- 关闭\n",pthread_self());
}
int main()
{
//func();
//开新线程
pthread_t tid;
int count = 0;
while(1)
{
//开启新线程 pthread_task 采用默认属性,不传递参数
int ret = pthread_create(&tid,NULL,pthread_task,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
//int pthread_detach(pthread_t thread);
pthread_detach(tid); //将指定tid的线程变为分离属性
printf("cout = %d\n",count ++);
}
}
2> 先设置属性,在创建线程
#include <pthread.h>
//设置初始属性
int pthread_attr_init(pthread_attr_t *attr);
参数:
attr:线程属性
//销毁属性
int pthread_attr_destroy(pthread_attr_t *attr);
#include <pthread.h>
//设置线程的分离属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
参数:
attr:线程属性
detachstate:要设置的属性
PTHREAD_CREATE_DETACHED:设置线程的分离属性
PTHREAD_CREATE_JOINABLE:设置线程的非分离属性
返回值:
成功: 0
失败:一个错误号
Compile and link with -pthread.
pthread_attr_init.c(先设置属性,在创建线程)
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#if 0
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread:线程的ID号
attr:线程的属性 NULL 表示使用默认属性
start_routine:线程处理函数 -- 专门处理多线程任务的
arg:传递给线程处理函数的参数 NULL 表示不需要传参
返回值:
成功: 0
失败:一个错误号
#endif
//线程处理函数:(名称自定义)
//返回值类型必须是void *
//参数列表只能有一个 void *类型的参数
void *pthread_task(void *arg)
{
printf("%ld -- 开启\n",pthread_self()); //pthread_self()获取自己的tid
printf("%ld -- 关闭\n",pthread_self());
}
int main()
{
//1- 初始化线程属性
//int pthread_attr_init(pthread_attr_t *attr);
pthread_attr_t attr; //存放线程属性的参数
if(pthread_attr_init(&attr)!= 0) //将默认属性给到attr
{
perror("pthread_attr_init");
return -1;
}
//2-设置线程的分离属性
//int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
/*
PTHREAD_CREATE_DETACHED:设置线程的分离属性
PTHREAD_CREATE_JOINABLE:设置线程的非分离属性
*/
if(pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED)!= 0) //将线程属性设置为分离属性
{
perror("pthread_attr_setdetachstate");
return -1;
}
//开新线程
pthread_t tid;
int count = 0;
while(1)
{
//开启新线程 pthread_task 采用指定的属性 -- attr,不传递参数
int ret = pthread_create(&tid,&attr,pthread_task,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
printf("cout = %d\n",count ++);
}
}
五、线程的同步以及互斥
线程同步机制
由于线程共享内存,需同步机制避免竞态条件:
多线程共享同一个进程的地址空间,优点:线程间很容易进行通信,通过全局变量实现数据共享和交换。缺点:多个线程同时访问共享对象时需要引入同步和互斥机制。
同步(synchronization) 指的是多个任务(线程)按照约定的顺序相互配合完成一件事情,1968年,Edsgar Dijkstra 基于信号量的概念提出了一种同步机制,由信号量来决定线程是继续运行还是阻塞等待。
信号量代表某一类资源,其值表示系统中该资源的数量。信号量是一个受保护的变量,只能通过三种操作来访问:
初始化
P操作(申请资源)
P(S) 含义如下: if (信号量的值大于0) { 申请资源的任务继续运行; 信号量的值减一;} else { 申请资源的任务阻塞;}
V操作(释放资源)
V(S) 含义如下: if (没有任务在等待该资源) { 信号量的值加一;} else { 唤醒第一个等待的任务,让其继续运行}
注意:信号量的值为非负整数。
Posix Semaphore API
- posix中定义了两类信号量:
- 无名信号量(基于内存的信号量)
- 有名信号量
pthread
库常用的信号量操作函数如下:
- int sem_init(sem_t *sem, int pshared, unsigned int value);
- int sem_wait(sem_t *sem); // P操作
- int sem_post(sem_t *sem); // V操作
- int sem_trywait(sem_t *sem);
- int sem_getvalue(sem_t *sem, int *svalue);
线程间互斥
引入互斥(mutual exclusion)锁 的目的是用来保证共享数据操作的完整性。互斥锁主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,任何时刻最多只能有一个线程能访问该资源。线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。
- 互斥锁(Mutex)
pthread_mutex_t lock;
pthread_mutex_lock(&lock);
// 临界区
pthread_mutex_unlock(&lock);
线程的互斥操作 —— 互斥锁:
线程的互斥操作 -- 互斥锁
1> 申请互斥锁
#include <pthread.h>
//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex:互斥锁变量
attr:互斥锁的属性 NULL 表示默认属性
返回值:
成功:0
出错:-1
2> 上锁 然后 解锁
#include <pthread.h>
//给临界资源上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//给临界资源解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:互斥锁变量
返回值:
成功:0
失败:错误号
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
pthread_mutex_t mutex1; //互斥锁变量
int a = 0;
int b = 0;
//线程处理函数:(名称自定义)
//返回值类型必须是void *
//参数列表只能有一个 void *类型的参数
void *pthread_task1(void *arg)
{
while(1)
{
//上锁 ---》 让其变成临界资源
pthread_mutex_lock(&mutex1);
/*临界区*/
a++;
b++;
/*---------*/
//解锁 -- 》解除临界资源
pthread_mutex_unlock(&mutex1);
}
}
void *pthread_task2(void *arg)
{
while(1)
{
if(a == b)
{
printf("a = b = %d\n",a);
}
else
{
printf("a = %d,b = %d\n",a,b);
}
}
}
int main()
{
//申请互斥锁
//int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
if(pthread_mutex_init(&mutex1,NULL) == -1)
{
fprintf(stderr,"pthread_mutex_init error\n");
return -1;
}
//开新线程
pthread_t tid1,tid2;
//开启新线程 pthread_task 采用默认属性,不传递参数
int ret = pthread_create(&tid1,NULL,pthread_task1,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid1,NULL,pthread_task2,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
}
- 条件变量(Condition Variable)
pthread_cond_t cond;
pthread_cond_wait(&cond, &lock); // 等待条件
pthread_cond_signal(&cond); // 通知一个线程
- 信号量(Semaphore)
sem_t sem;
sem_wait(&sem); // P操作
sem_post(&sem); // V操作
线程的同步机制:
1- 线程的同步
--- 信号量 : 信号量的值为非负整数
1> 信号量的初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:信号量的变量
pshared:信号量的使用范围
0 :线程专用
非0 :进程也可以使用
value:信号量的值
返回值:
成功:0
失败:-1 ,并且设置错误号
Link with -pthread.
2> 执行PV操作
---- p操作 -1
#include <semaphore.h>
int sem_wait(sem_t *sem);
参数:
sem:信号量的变量;
返回值:
成功:0
失败:-1 ,并且设置错误号
---- V操作 +1
#include <semaphore.h>
int sem_post(sem_t *sem);
参数:
sem:信号量的变量;
返回值:
成功:0
失败:-1 ,并且设置错误号
Link with -pthread.
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
sem_t sem1,sem2; //信号量的变量 最好设置为全局变量
//线程处理函数:(名称自定义)
//返回值类型必须是void *
//参数列表只能有一个 void *类型的参数
void *pthread_task1(void *arg)
{
while(1)
{
printf("我是老大,都听我的\n");
sem_post(&sem1); //v操作 +1
//sleep(1);
}
}
void *pthread_task2(void *arg)
{
while(1)
{
sem_wait(&sem1); // p操作 -1
printf("我是你爹,不停你的\n");
//sleep(1);
}
}
int main()
{
//初始化信号量
//int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem_init(变量,适用范围,初始值)
sem_init(&sem1,0,0);
//func();
//开新线程
pthread_t tid1,tid2;
//开启新线程 pthread_task 采用默认属性,不传递参数
int ret = pthread_create(&tid1,NULL,pthread_task1,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
ret = pthread_create(&tid1,NULL,pthread_task2,NULL ); // 这句话之后线程处理函数开始运行
if(ret != 0)
{
perror("pthread_create");
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
}
综上。希望该内容能对你有帮助,感谢!
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!