Linux系统编程:线程控制
目录
一. 线程的创建
1.1 pthread_create函数
1.2 线程id的本质
二. 多线程中的异常和程序替换
2.1 多线程程序异常
2.2 多线程中的程序替换
三. 线程等待
四. 线程的终止和分离
4.1 线程函数return
4.2 线程取消 pthread_cancel
4.3 线程退出 pthread_exit
4.4 线程分离 pthread_detach
五. 总结
一. 线程的创建
1.1 pthread_create函数
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)(void*), void *args)
函数功能:创建新线程
函数参数:
thread -- 输出型参数,用于获取新线程的id
attr -- 设置线程属性,一般采用nullptr,表示为默认属性
start_routine -- 新创建线程的入口函数
args -- 传入start_routine函数的参数
返回值:成功返回0,失败返回对应错误码
关于pthread系列函数的错误检查问题:
- 一般的Linux系统调用相关函数,都是成功返回0,失败返回-1。
- 但函数pthread系列函数不是,这些函数都是成功返回0,失败返回错误码,不对全局错误码进行设置。
代码1.1演示了如何通过pthread_create函数创建线程,在主函数中,分别以%lld和%x的方式输出子线程id,图1.1为代码的运行结果。
代码1.1:创建线程
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>// 新建线程的入口函数
void *threadRoutine(void *args)
{while(true){std::cout << (char*)args << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid; // 接收子线程id的输出型参数// 调用pthread_create函数创建线程// tid接收新线程的id,nullptr表示新线程为默认属性// 新线程的入口函数设为threadRoutine,参数为"thread 1"int n = pthread_create(&tid, nullptr, threadRoutine, (char*)"thread 1");if(n != 0) // 检验新线程是否创建成功{std::cout << "error:" << strerror(n) << std::endl;exit(1);}while(true){printf("main thread, tid = %lld 0x%x\n", tid, tid);sleep(1);}return 0;
}

1.2 线程id的本质
如1.2所示,在Linux的线程库pthread中,提供了用于维护每个线程的属性字段,包括描述线程的结构体struct pthread、线程的局部存储、线程栈等,用于对每个线程的维护。
每个线程在线程库中用于维护它的属性字段的起始地址,就是这个线程的id,换言之,线程id就是动态库(地址空间共享区)的一个地址,Linux为64位环境,因此,代码1.1输出的线程id会很大,这个值就对应地址空间共享区的位置。
为了保证每个线程的栈区是独立的,Linux采用的方法是线程栈在用户层提供,这样每个线程都会在动态线程库内部分得一块属于自身的“栈区”,这样就可以保证线程栈的独立性,而主线程的栈区,就使用进程地址空间本身的栈区。
Linux保证线程栈区独立性的方法:
- 子线程的栈区在用户层提供。
- 主线程栈区采用地址空间本身的栈区。
线程id的本质:地址空间共享区的一个地址。

二. 多线程中的异常和程序替换
2.1 多线程程序异常
在多线程程序中,如果某个线程在执行期间出现了异常,那么整个进程都可能会退出,在多线程场景下,任意一个线程出现异常,其影响范围都是整个进程。
如代码2.1创建了2个子线程,其中threadRun2函数中人为创造除0错误引发异常,发现整个进程都退出了,不会出现只有一个线程终止的现象。
结论:任意一个线程出现异常,其影响范围都是整个进程,会造成整个进程的退出。
代码2.1:多线程程序异常
#include <iostream>
#include <pthread.h>
#include <unistd.h>void *threadRoutine1(void *args)
{while(true){std::cout << (char*)args << std::endl;sleep(1);}return nullptr;
}void *threadRoutine2(void *args)
{while(true){std::cout << "thread 2, 除0错误!" << std::endl;int a = 10;a /= 0; }return nullptr;
}int main()
{pthread_t tid1, tid2;//先后创建线程1和2pthread_create(&tid1, nullptr, threadRoutine1, (void*)"thread 1");sleep(1);pthread_create(&tid2, nullptr, threadRoutine2, (void*)"thread 2");while(true){std::cout << "main thread ... ... " << std::endl;sleep(1);}return 0;
}

2.2 多线程中的程序替换
与多线程中线程异常类似,多线程中某个线程如果进行了程序替换,那么并不会出现这个线程去运行新的程序,其他线程正常执行原来的工作的情况,而是整个进程都被替换去执行新的程序。
代码2.2在threadRoutine1函数中通过execl去执行系统指令ls,运行代码我们发现,在子线程中进行程序替换后,主线程也不再继续运行了,进程执行完ls指令,就终止了。
结论:多线程程序替换是整个进程都被替换,而不是只替换一个线程。
代码2.2:多线程程序替换
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>void *threadRoutine1(void *args)
{while(true){std::cout << (char*)args << std::endl;execl("/bin/ls", "ls", nullptr); // 子线程中进行程序替换exit(0);}return nullptr;
}int main()
{pthread_t tid;// 创建线程int n = pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");if(n != 0) {// 检验线程创建成功与否std::cout << strerror(n) << std::endl;exit(1);}while(true){std::cout << "main thread" << std::endl;sleep(1);}return 0;
}

三. 线程等待
线程等待与进程等待类似,主线程需要等待子线程退出,以获取子线程的返回值。如果主线程不等待子线程,而主线程也不退出,那么子线程就会处于“僵尸状态”,其task_struct一直得不到释放,引起内存泄漏。
- 通过pthread_join函数,可以实现对线程的等待。
- 线程等待只能是阻塞等待,不能非阻塞等待。
pthread_join函数 -- 等待线程
函数原型:int pthread_join(pthread_t thread, void **ret);
函数参数:
thread -- 等待线程的id
ret -- 输出型参数,获取线程函数的返回值
返回值:成功返回0,失败返回错误码
在代码3.1中, 线程函数threadRoutine中在堆区new了5个int型数据的空间,并赋值为1~5,线程函数返回指向这块堆区资源的指针,主线程等待子线程退出,主线程可以看到这块资源。注意线程函数返回值的类型为void*,使用返回值的时候要注意强制类型转换。
代码3.1:pthread_join线程等待
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>void *threadRoutine(void *args)
{std::cout << (char*)args << std::endl;int *pa = new int[5];for(int i = 0; i < 5; ++i){pa[i] = i + 1;}return (void*)pa;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");int *pa = nullptr;// 等待线程退出,pa接收线程函数返回值pthread_join(tid, (void**)&pa);// 获取线程函数返回值指向的空间内的资源std::cout << "thread exit" << std::endl;for(int i = 0; i < 5; ++i){printf("pa[%d] = %d\n", i, pa[i]);}delete[] pa;return 0;
}

四. 线程的终止和分离
可以实现线程终止的方法有:
- 线程函数return。
- 由另一个线程将当前线程取消pthread_cancel。
- 线程退出pthread_exit。
4.1 线程函数return
pthread_create函数的第三个参数start_routine为线程函数指针,新创建的线程就负责执行这个函数,如果这个函数运行完毕return退出,那么,线程就退出了。
但是这种方法对主线程不适用,如果主线程退出,就是进程终止了,全部线程都会退出。
结论:如果线程函数return,那么线程就退出了,但主线程return进程就退出了,不适用这种退出方式。
线程函数接收一个void*类型的参数,返回void*类型参数,如果线程函数运行到了return,那么这个线程就退出了,如代码3.1中的threadRoutine,就是采用return来终止线程的。
代码4.1验证了主线程退出的情况,设定线程函数为死循环IO输出,但是主线程在创建完子线程sleep(2)之后return,发现线程函数并没有继续运行,证明了主线程退出不适用于return这种方法来终止。
代码4.1:验证主线程不能通过return退出
// 线程函数死循环
void *threadRoutine1(void *args)
{while(true){std::cout << (char*)args << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;// 创建线程int n = pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");std::cout << "main thread" << std::endl;sleep(2); // 主线程sleep 2s后退出return 5;
}

4.2 线程取消 pthread_cancel
pthread_cancel函数可用于通过指定线程id,来取消线程。
pthread_cancel -- 取消线程
函数原型:int pthread_cancel(pthread_t thread)
函数参数:thread -- 被取消的线程的id
返回值:成功返回0,不成功返回非0的错误码
一般而言,采用主线程取消子线程的方式来取消线程,一个线程取消自身也是可以的,但一般不会这样做,pthread_cancel(pthread_self()) 可用于某个线程取消其自身,其中pthread_self函数的功能是获取线程自身的id。
- pthread_self函数 -- 获取线程自身的id。
如果一个线程被取消了,那么就无需在主线程中通过pthread_join对这个线程进行等待,但如果使用了pthread_join对被取消的线程进行等待,那么pthread_join的第二个输出型参数会记录到线程函数的返回值为-1。
结论:如果一个线程被pthread_cancel了,那么pthread_join会记录到线程函数返回(void*)-1。
在代码4.2中,通过pthread_cancel函数,取消子线程,然后pthread_join等待子线程,输出强转为long long类型的返回值ret,记录到ret的值为-1。
代码4.2:取消子线程并等待取消了的子线程
// 线程函数
void *threadRoutine1(void *args)
{while(true){std::cout << (char*)args << std::endl;sleep(1);}return (void*)10;
}int main()
{pthread_t tid;// 创建线程pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");std::cout << "main thread" << std::endl;sleep(2); pthread_cancel(tid); // 取消id为tid的子线程void *ret = nullptr;int n = pthread_join(tid, &ret); // 等待已经取消的线程退出std::cout << "ret : " << (long long)ret << std::endl;return 0;
}

4.3 线程退出 pthread_exit
pthread_exit 函数在线程函数中,可用于指定线程函数的返回值并退出线程,与return的功能基本完全相同,注意,exit不可用于退出线程,在任何一个线程中调用exit,都在让整个进程退出。
pthread_exit 函数 -- 让某个线程退出
函数原型:void pthread_exit(void *ret)
函数参数:ret -- 线程函数的退出码(返回值)
代码4.3在线程函数中调用pthread_exit终止线程,指定返回值为(void*)111,在主线程中等待子线程,并将线程函数返回值存入ret中,输出(long long)ret的值,证明子线程返回(void*)111。
代码4.3:通过pthread_exit终止线程
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>// 线程函数
void *threadRoutine1(void *args)
{int count = 0;while(true){std::cout << (char*)args << ", count:" << ++count << std::endl;if(count == 3) pthread_exit((void*)111);sleep(1);}return nullptr;
}int main()
{pthread_t tid;// 创建线程pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");std::cout << "main thread" << std::endl;sleep(5); void *ret = nullptr;pthread_join(tid, &ret); std::cout << "[main thread] child thread exit, ret:" << (long long)ret << std::endl;return 0;
}

4.4 线程分离 pthread_detach
严格意义上讲,pthread_detach并不算线程退出。即使一个线程函数中使用了pthread_detach(pthread_self())对其自身进行分离,线程函数在pthread_detach之后的代码也会正常被执行。
pthread_detach一般用于不需要关心退出状态的线程,被pthread_detach分离的子线程,即使主线程不等待子线程退出,子线程也不会出现僵尸问题。
一般来说,都是线程分离其自身,当然也可以通过主线程分离子线程,但不推荐这么做。
经pthread_detach分离之后的线程,不应当pthread_join等待,如果等待一个被分离的线程,那么pthread_join函数会返回错误码。
结论:(1).pthread_detach用于将不需要关系关系退出状态的子线程分离 (2).被分离的线程不应被等待,如果被等待,那么pthread_join会返回非0错误码。
代码4.4演示了经pthread_detach分离之后线程函数继续运行,等待被分离的线程失败的情景。
代码4.4:线程分离及等待被分离的线程
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>// 线程函数
void *threadRoutine1(void *args)
{// 子线程将其自身分离pthread_detach(pthread_self());int count = 0;while(true){std::cout << (char*)args << ", count:" << ++count << std::endl;if(count == 3) pthread_exit((void*)111);sleep(1);}return (void*)10;
}int main()
{pthread_t tid;// 创建线程pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");std::cout << "main thread" << std::endl;sleep(5); void *ret = nullptr;int n = pthread_join(tid, &ret); // 等待已经取消的线程退出 if(n != 0) // 检验是否等待成功{std::cout << "wait thread error -> " << strerror(n) << std::endl;}return 0;
}

五. 总结
- pthread_create函数可以创建子线程,关于线程的管理方法及属性字段,被记录在动态库里,线程id本质上就是地址空间共享区的某个地址。
- 由于Linux在系统层面不严格区分进程和线程,CPU调用只认PCB,因此为了保证每个线程栈空间的独立性,子线程的栈由用户层(动态库)提供,主线程的栈区就是地址空间的栈区。
- 在多线程中,任何一个线程出现异常,影响范围都是整个进程,如果在某个线程中调用exec系列函数替换程序,那么整个进程都会被替换掉。
- pthread_join的功能为在主线程中等待子线程,如果子线程没有被detach且不被主线程等待,那么子线程就会出现僵尸问题。
- 有三种方法可以终止线程:(1). 线程函数return,这种方法不适用于主线程。(2). pthread_exit 函数终止线程函数。(3). pthread_cancel 取消线程,被取消的线程不需要被等待,如果等待会记录到线程函数返回(void*)-1。
- 如果某个子线程的退出状态不需要关心,那么就可以通过pthread_detach分离子线程,分离后的线程不应被等待,如果被等待,那么pthread_join函数就会返回非零错误码。
相关文章:

Linux系统编程:线程控制
目录 一. 线程的创建 1.1 pthread_create函数 1.2 线程id的本质 二. 多线程中的异常和程序替换 2.1 多线程程序异常 2.2 多线程中的程序替换 三. 线程等待 四. 线程的终止和分离 4.1 线程函数return 4.2 线程取消 pthread_cancel 4.3 线程退出 pthread_exit 4.4 线程…...

基于Java+SpringBoot+Vue前后端分离纺织品企业财务管理系统设计和实现
博主介绍:✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专…...
搭建开发环境-Windows
写C# 的请出去。 然后,Windows 是最好的Linux发行版。搭建开发环境-WSLUbuntu...

【 Python 全栈开发 - 人工智能篇 - 45 】集成算法与聚类算法
文章目录 一、集成算法1.1 概念1.2 常用集成算法1.2.1 Bagging1.2.2 Boosting1.2.2.1 AdaBoost1.2.2.2 GBDT1.2.2.3 XgBoost 1.2.3 Stacking 二、聚类算法2.1 概念2.2 常用聚类算法2.2.1 K-means2.2.2 层次聚类2.2.3 DBSCAN算法2.2.4 AP聚类算法2.2.5 高斯混合模型聚类算法 一、…...
SSM商城项目实战:账户充值功能实现
SSM商城项目实战:账户充值功能实现 在一个电商平台中,用户账户充值是一个非常重要的功能。本文将介绍如何在SSM(SpringSpringMVCMyBatis)商城项目中实现账户充值功能。通过本文的指导,你将学会如何在项目中添加账户充…...
wireshark工具pcap文件转换
pcap详解_pcap_loop_小虎随笔的博客-CSDN博客 分析802.11无线报文hexdump内容:利用wireshark自带二进制工具text2pcap将hexdump内容转换为pcap文件..._weixin_30835933的博客-CSDN博客 text2pcap: 将hex转储文本转换为Wireshark可打开的pcap文件(wireshark,数据) …...

Python+TinyPNG熊猫网站自动化的压缩图片
前言 本篇在讲什么 PythonTinyPNG自动化处理图片 本篇需要什么 对Python语法有简单认知 依赖Python2.7环境 依赖TinyPNG工具 本篇的特色 具有全流程的图文教学 重实践,轻理论,快速上手 提供全流程的源码内容 ★提高阅读体验★ 👉…...

【Linux】socket 编程基础
文章目录 📕 网络间的通信📕 socket 是什么1. socket 套接字2. 套接字描述符3. 基本的 socket 接口函数3.1 头文件3.2 socket() 函数3.3 bind() 函数struct sockaddr主机序列与网络序列 3.4 listen() 函数3.5 connect() 函数3.6 accept() 函数IP 地址风格…...

openGauss学习笔记-51 openGauss 高级特性-列存储
文章目录 openGauss学习笔记-51 openGauss 高级特性-列存储51.1 语法格式51.2 参数说明51.3 示例 openGauss学习笔记-51 openGauss 高级特性-列存储 openGauss支持行列混合存储。行存储是指将表按行存储到硬盘分区上,列存储是指将表按列存储到硬盘分区上。 行、列…...

ReactNative 密码生成器实战
效果展示图 使用插件 Formik 负责表单校验、监听表单提交、数据校验错误信息展示 Yup 负责表单校验规则 分析页面 从上述的展示图我们可以看到的主要元素有:输入框、单选按钮和按钮。其中生成的密码长度不可能很大也不可能为负数和 0,所以我们可以限…...

开始MySQL之路——外键关联和多表联合查询详细概述
多表查询和外键关联 实际开发中,一个项目通常需要很多张表才能完成。例如,一个商城项目就需要分类表,商品表,订单表等多张表。且这些表的数据之间存在一定的关系,接下来我们将在单表的基础上,一起学习多表…...

无涯教程-PHP - intval() 函数
PHP 7引入了一个新函数 intdiv(),该函数对其操作数执行整数除法并将该除法返回为int。 <?php$valueintdiv(10,3);var_dump($value);print(" ");print($value); ?> 它产生以下浏览器输出- int(3) 3 PHP - intval() 函数 - 无涯教程网无涯教程网…...

2023年国赛数学建模思路 - 案例:粒子群算法
文章目录 1 什么是粒子群算法?2 举个例子3 还是一个例子算法流程算法实现建模资料 # 0 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 1 什么是粒子群算法? 粒子群算法(Pa…...

【1++的数据结构】之map与set(一)
👍作者主页:进击的1 🤩 专栏链接:【1的数据结构】 文章目录 一,关联式容器与键值对二,setset的使用 三,mapmap的使用 四,multiset与multimap 一,关联式容器与键值对 像l…...

Ubuntu断电重启后黑屏左上角光标闪烁,分辨率低解决办法,ubuntu系统display只有4:3 怎么办?太卡
这个问题主要是显卡驱动问题,按照步骤更新显卡驱动 1,选择metapackage 并且选择proprietary版本,选择版本号选择最新的版本。 2,具体步骤参考 前言 笔者在安装显卡驱动时并未遇到问题,主要是后续屏幕亮度无法调节&…...
Java 微服务当中POST form 、url、json的区别
在Java微服务的Controller中,你可以处理来自客户端的不同类型的POST请求,包括POST form、POST URL参数和POST JSON数据。以下是它们的区别以及在微服务Controller中的示例说明: POST Form 表单数据: 当客户端以表单方式提交数据…...
repo 常用命令汇总——202308
文章目录 1. 下载repo:2. 获取工程repo信息3. 下载代码4. 创建并切换本地分支5. repo forall6. repo upload7. repo list8. repo info9. repo help 1. 下载repo: 使用下面命令,具体版本号参考前面网页中显示的最新版本号。 curl http://git…...

[Linux]命令行参数和进程优先级
[Linux]命令行参数和进程优先级 文章目录 [Linux]命令行参数和进程优先级命令行参数命令行参数的概念命令函参数的接收编写代码验证 进程优先级进程优先级的概念PRI and NI使用top指令修改nice值 命令行参数 命令行参数的概念 命令行参数是指用于运行程序时在命令行输入的参数…...

Android13新特性之通知权限提升
Android13新特性之通知权限提升 随着移动通信的高速发展,保障通信的安全性变得尤为重要。在Android 13的最新版本中,通知权限的管理得到了进一步加强。为了实现安全的通信和确保用户的隐私,必须正确申请通知权限。本文将详细探讨如何在Andro…...

206. 反转链表 (简单系列)
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例 1: 输入:head [1,2,3,4,5] 输出:[5,4,3,2,1] 示例 2: 输入:head [1,2] 输出:[2,1] 示例 3: 输…...

网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...

RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...

盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...