Linux编程-线程

早期 Linux没有内核原生线程,只有进程。

  1. 进程的痛点 进程是资源分配的最小单位,创建进程fork()会复制页表、文件描述符、内存空间,开销大;进程间切换要刷新 TLB、更换地址空间,上下文切换开销高;多任务如果全部用多进程,系统性能损耗很大。

  2. 轻量级进程 LWP(Light Weight Process) Linux 内核没有单独的 "线程" 内核对象,Linux 下线程本质就是共享资源的进程 。 内核提供clo

    ne()系统调用,clone()可以选择性共享:虚拟地址空间、文件描述符、信号处理、用户 ID 等资源。

进程的空间开辟速度和开销量较大,侧重为一个资源分配的最小单位,而线程侧重于成为一个基本执行体,线程主要由程序计数器、相关寄存器、线程栈构成,开辟更快,更灵活。

1. 线程创建 pthread_create

复制代码
int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *),
                   void *arg);
  • thread:输出参数,保存用户态线程 ID pthread_t
  • attr:线程属性,NULL 使用默认属性,默认可以结合
  • start_routine:线程执行函数,**返回值 void*,参数 void*
  • arg:传给线程函数的参数;传多个参数传结构体指针
  • 返回值:成功返回 0,失败返回错误码

同一进程下的多个线程,资源共享,共用同一块进程空间,如果操作同一块资源会相互影响,线程之间的关系是对等的,并不存在父子关系。

复制代码
void *do_thread1(void *arg)
{
	int *var = arg;
	while(1)
	{
		printf("---do_thread1 run ---\n");
		(*var)++;
		printf("---thread1 var+1---\n---------------------\n");
		sleep(1);
	}

	return NULL;
}


int ret = pthread_create(&tid1,NULL,do_thread1,&var);
if(ret != 0)
{
	errno = ret;
	perror("pthread_create fail");
	return -1;
}

注意:Linux早期内核并不具备真正的线程支持,内核中只有进程这一调度实体,但可以借助clone()系统调用创建调用进程的拷贝,通过参数配置让新创建的实体与原进程共享地址空间等资源,内核依旧为其生成独立的task_struct进程控制块,由用户态线程库把这类轻量级进程封装成线程使用;后期NPTL结合CLONE_THREAD标志引入线程组,完善了POSIX线程语义,不过底层实现依旧基于clone()。

线程被创建后,内核调度,立刻就会执行 start_routine

三种结束线程执行的方式:

  1. 线程函数 return; → 正常退出
  2. 线程内部调用 pthread_exit(void *retval); → 主动退出
  3. 被别的线程取消 pthread_cancel(tid);

⚠️主线程 return 0;整个进程结束,所有线程全部销毁 ,不要用 return 退出主线程而子线程还在跑;主线程想等待子线程,用pthread_join

pthread_exit 线程主动退出

复制代码
void pthread_exit(void *retval);
  1. retval:线程退出返回值,给pthread_join接收;不能传局部变量地址!局部变量函数结束销毁。类似于进程的exit
  2. 在子线程调用:只退出当前线程,不影响其他线程

pthread_join 阻塞等待线程回收

复制代码
int pthread_join(pthread_t thread, void **retval);
  1. 阻塞等待指定线程结束,回收线程资源;类似进程的wait阻塞等待回收进程

  2. retval接收线程返回值:return的值 或 pthread_exit参数;传NULL代表不关心返回值。

  3. 默认非分离线程必须 join,否则产生线程僵尸,资源泄漏

    void *func(void arg)
    {
    pthread_exit((void
    )200);
    }

    int main()
    {
    pthread_t tid;
    void *res;
    pthread_create(&tid,NULL,func,NULL);
    pthread_join(tid, &res);
    printf("线程退出值:%ld\n",(long)res);
    return 0;
    }

在线程内部,单独调用exit退出并返回的效果,与直接return返回结束线程,并返回退出值的效果是一样的。如果需要带出的是一个字符串,告知主线程,子线程结束了,可以直接return然后接双引号,输入需要带回的字符串,这样的字符串会放在常量区,可以直接定义一个字符型指针接收。

pthread_cancel 取消线程(被动退出)

复制代码
int pthread_cancel(pthread_t thread);

传入参数为,需要控制的线程的tid参数,类似于kill命令控制进程的效果

  • 取消状态:pthread_setcancelstate ()

    • PTHREAD_CANCEL_ENABLE:允许被取消(默认)
    • PTHREAD_CANCEL_DISABLE:忽略 pthread_cancel(),取消请求挂起
  • 取消类型:pthread_setcanceltype ()

    • PTHREAD_CANCEL_ASYNCHRONOUS:异步取消,收到 cancel 信号尽量立刻终止线程,可以在任意指令点退出(风险高,资源容易泄漏)
    • PTHREAD_CANCEL_DEFERRED:延迟取消(系统默认),不会马上退出,线程执行到 ** 取消点 (cancellation point)** 才真正退出。

大部分系统调用是取消点:sleep、read、write、pthread_testcancel、pthread_cond_wait 等。 如果线程一直在做纯 CPU 运算、没有任何取消点,就算调用了pthread_cancel,线程会继续跑,不会退出。可以手动调用 pthread_testcancel() 设置人工取消点。

可结合 vs 分离态-----joinable /detached

这一组只管线程结束之后资源怎么处理完全不控制什么时候终止

  1. 可结合态(joinable,默认创建的时候选择NULL) :线程退出后,线程的描述符、退出状态保留,必须调用 pthread_join(),才会释放线程资源。如果线程被 cancel 终止,pthread_join 可以拿到 PTHREAD_CANCELED 返回值。
  2. 分离态(detached) :线程结束(不管是 return、pthread_exit、被 pthread_cancel 取消)之后,系统自动回收线程资源,不需要也不能 pthread_join

以下函数的返回值均为,成功返回0,不成功返回非0值!!!!

设置线程属性pthread_attr_t

复制代码
int pthread_attr_init(pthread_attr_t *attr);
//初始化线程属性结构体,把 attr 设置成默认属性值。

int pthread_attr_destroy(pthread_attr_t *attr);
//销毁属性对象,释放 attr 内部分配的资源,attr 变成无效状态。

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
//设置线程属性里的分离状态,只对后续 pthread_create 创建的线程生效。

使用流程:初始化属性 → 设置属性 → create 传入 attr → 销毁属性

初始化属性:pthread_attr_init;

复制代码
pthread_attr_t attr;
//定义attr变量
pthread_attr_init(&attr);
//初始化attr变量

设置属性:int pthread_attr_setdetachstate

复制代码
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// detachstate 两个宏:
// PTHREAD_CREATE_JOINABLE  可接合(默认,需要join)
// PTHREAD_CREATE_DETACHED   分离状态,不需要join,结束自动释放资源

创建线程的时候设置state: 若此时设置为不可结合态,则线程结束自动回收资源,不能调用 pthread_join (),join 会报错。

复制代码
pthread_create(&tid, &attr, thread_func, NULL);
//传入attr参数,不再使用NULL默认可结合的状态参数

销毁属性变量:

复制代码
pthread_attr_destroy(&attr);

线程创建之后,动态修改分离属性 pthread_detach

复制代码
int pthread_detach(pthread_t thread);

传入参数就是需要改变分离属性的线程对应的tid参数

  1. 将一个 ** 可接合 (joinable)** 线程设置成分离状态;
  2. 调用之后,该线程退出自动释放资源,不能再pthread_join
  3. 可以自己在线程内部调用 pthread_detach(pthread_self());

线程退出清理函数:pthread_cleanup

线程被 cancel 取消,或者 pthread_exit 退出,执行资源释放;return不会触发清理函数!!

复制代码
//注册清理函数
void pthread_cleanup_push(void (*routine)(void *), void *arg);

//执行清理函数
void pthread_cleanup_pop(int execute);

非0  执行清理函数
0 ,不执行清理	

两个函数必须成对出现,不然会造成语法不完整,编译报错。

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

pthread_t mtid;   // 保存主线程的线程ID

// 子线程执行函数
void *do_thread(void *arg)
{
	printf("---do_thread---tid = %ld\n",pthread_self());
	sleep(1);                 // 休眠1秒,让主线程先运行起来
	pthread_cancel(mtid);    // 发送取消请求,尝试取消主线程
	return NULL;
}

// 线程清理回调函数:线程被取消触发点退出时执行
void cleanup(void *arg)
{
	printf("cleanup ------\n");
}


int main(int argc, const char *argv[])
{
	mtid = pthread_self();   // 获取主线程ID存入全局变量
	pthread_t tid;
	// 创建子线程,执行do_thread,传入参数NULL
	int ret = pthread_create(&tid,NULL,do_thread,NULL);
	if(ret != 0)
	{
		errno = ret;       // pthread函数错误码存在返回值,赋值给errno给perror打印
		perror("pthread_create fail");
		return -1;
	}

	// 注册线程清理处理函数
	// push:把cleanup压入清理栈;第二个参数为传给cleanup的实参
	pthread_cleanup_push(cleanup,NULL);

	printf("tid = %ld\n",tid);                // 打印子线程id
	printf("main_tid = %ld\n",pthread_self());// 打印主线程id

	sleep(3);

//	pthread_exit(NULL);   // 如果调用pthread_exit,主线程主动退出,会触发cleanup

	// 死循环休眠,主线程阻塞在此
	while(1)
	sleep(1);

	// pop弹出清理栈,参数非0:执行注册的cleanup函数;0:不执行直接丢弃
	// 注意:pthread_cleanup_push / pop 是宏,必须成对出现,不能中间return跳出
	pthread_cleanup_pop(1);
	return 0;
}

进程与线程的区别:

进程:操作系统资源分配的最小单位。每个进程拥有独立虚拟地址空间、文件描述符、信号处理、堆、栈、代码段。

线程 :CPU 调度的最小单位。一个进程内的多条执行流,共享进程绝大部分资源,每个线程仅有自己私有栈、寄存器、线程局部存储。

对比项 进程 (process) 线程 (thread,pthread)
资源拥有 独立地址空间,独占堆、全局变量、fd、信号 共享进程地址空间,共享堆、全局变量、文件描述符;仅私有栈、寄存器、TLS
创建方式 fork()系统调用 pthread_create()库函数
开销 大。拷贝页表,虚拟内存复制,耗时高 小。仅新建栈,复用进程资源,创建销毁快
上下文切换 切换页表,刷新 TLB,开销很大 同一进程内切换,不用换页表,开销小
内存隔离 进程之间完全隔离,一个进程崩溃,其他进程不受影响 同进程所有线程共享地址空间;任意一个线程崩溃,整个进程直接退出
通信方式 (IPC) 管道、socket、消息队列、共享内存、信号 直接读写全局变量 / 堆内存,但必须加锁pthread_mutex;也可用条件变量
返回回收 父进程wait()/waitpid()回收子进程,防止僵尸进程 pthread_join()回收,或设置分离属性 detach
信号处理 信号发给整个进程,由进程内某一个线程响应 信号是进程级别,线程可设置掩码

高频坑点

  1. 线程没有独立内存:线程内野指针越界,整个进程直接段错误崩溃;而进程崩溃不会影响别的进程。
  2. 全局变量、malloc 堆内存全部线程共享;局部栈变量是线程私有
  3. fork 只会复制调用线程,子进程里面只有一个线程,多线程程序慎用 fork。
  4. 进程间不能直接指针传递,虚拟地址空间隔离;同进程线程之间指针可以直接传。
  5. 僵尸进程:fork 子进程退出父不 wait;僵尸线程:pthread 不 join 也不 detach。

线程进程如何选择

  • 需要强隔离、一个任务挂掉不影响主程序 → 多进程
  • 任务之间大量数据交互,追求切换速度,不怕一个任务崩全部挂 → 多线程
相关推荐
Linux-lucky7 小时前
21-Linux学习之旅之HTTPS和安全加固
linux·运维·学习·ubuntu·https
清水白石0088 小时前
Python 死锁排查全攻略:从线程卡死到锁依赖定位与工程化修复
linux·网络·python
ao-weilai9 小时前
Linux基础:Ext系列文件系统
linux·运维·服务器
qq_349447959 小时前
Linux系统,安装git,从使用git下载仓库(GitHub, Gitee, GitLab 等),并且使用ssh密钥,可以直接执行git pull
linux·git·ssh
GlobalSign数字证书12 小时前
小企业零运维 TLS 架构设计:证书生命周期管理(CLM)与自动化轮换策略的技术选型
运维·自动化
mCell12 小时前
GitHub Actions 玩法大赏:一台远程主机的七种活法
linux·github·agent
盐焗鹌鹑蛋12 小时前
【Linux】动静态库的制作与使用
linux
wuyk55513 小时前
Python 第三章:列表 List
服务器·开发语言·python