铺垫
之前我们提到,Linux不直接对线程进行调度,而是对轻量级进程进行调度。但用户就想像Windows那样直接对线程进程控制。所以,就有了pthread库来封装了一层。
那么想要进行线程控制,要用pthread库。(pthread库是原生线程库,即系统自带的)
原本的线程叫主线程,新创建的叫新线程。
创建线程
pthread_create函数
用于创建新线程来并发执行一个函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
pthread_t *thread:
指向一个线程标识符 pthread_t 的指针,用于存储新创建的线程的 ID。
这个 ID 以后可以用于其他线程操作,如等待线程结束或取消线程。
const pthread_attr_t *attr:
指向线程属性对象的指针。通过该参数可以设置线程的属性(如是否可被 join、栈大小等)。
如果传入 NULL,则使用默认的线程属性。
void *(*start_routine)(void *):
这是线程要执行的函数的指针。该函数的返回值类型为 void*,参数类型为 void*,
即它接受一个通用的指针参数,并返回一个通用指针作为结果。
void *arg:
传递给 start_routine 函数的参数,它是一个通用指针。
可以通过它传递任意类型的数据到线程函数中。
LWP和PID
我们通过显示线程的详细信息,能看到PID和LWP,
LWP是轻量级进程的ID,实际上我们在调度的时候,使用的都是LWP ID。
getid?
我们用getid()来获得两个线程的id,发现得到的是PID,LWP并没有提供直接获取的方式,比较麻烦。
线程运行与结束顺序
是新线程先运行还是老线程先运行,由调度器决定。
但是当主线程结束之后,进程就会退出,所有线程也就退出了
等待进程
pthread_join 函数
int pthread_join(pthread_t thread, void **retval);
pthread_t thread:
要等待的线程的标识符,即需要 pthread_join 的线程 ID。
这个 ID 是由 pthread_create 创建线程时返回的 pthread_t 类型变量。
void **retval:
这是一个指向指针的指针,用于存储目标线程的返回值。
线程的返回值可以通过 pthread_exit 返回,也可以是线程函数的返回值。如果你不关心返回值,可以传递 NULL。
同一进程下的多线程
- 同一个进程的线程,资源是共享的
- 多线程中,任意一个线程出问题,则进程退出
- 并且不用考虑
退出
pthread_exit
用pthread_exit来退出某个进程,谁调用这个函数就退出谁
pthread_cancel
当我们确定这个进程已经启动的时候,我们可以调用pthread_cancel函数,来取消这个线程
线程优点
- 创建一个新线程的代价要比创建一个新进程小得多(只要创建新的PCB就够了)
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多(与cache有关)
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
cache
线程切换比进程好的关键原因就是cache
cache就是CPU中的缓存,
他会默认存储正在执行代码的下一行代码的地址,方便执行。
线程切换不会影响缓存(前面提到了:因为同一进程中的线程共享相同的地址空间和全局数据。),而进程切换之后,缓存一般需要重新加载。所以
计算密集型和i/o密集型
计算密集型:解压,压缩之类的,使用cpu的资源。
建议cpu有几核,就创建几个线程,太多线程切换起来还要时间
i/o密集型:下载之类的。
可以多开几个,来减少等待时间。
缺点
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响(因为大部分资源是共享的)
线程用途
线程与进程
线程独占(私有)
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器(一组线程执行的硬件上下文数据,每个线程还是单独的执行流)
- 栈( 临时数据会压入栈,如果不区分就会很混乱)
- errno
- 信号屏蔽字
- 调度优先级
线程共享
各线程还共享以下进程资源和环境:
- 代码和全局数据
- 文件描述符表(记录打开文件的个数)
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
在创建新的线程的时候,我们需要为每个线程在堆上开辟一段属于自己的开间,再释放掉。这样就避免
线程的使用
传参和返回值
我们不仅可以给线程传递信息,也可以传递对象来执行某些任务。
cpp11的thread
thread库就是封装了Linux的pthread库
为什么要封装
为了使cpp具有跨平台性(使其在vs下也能跑)
Windows
Windows下不用包含pthread库
其他语言
大部分语言都要用到pthread库
线程分离
当我们不关心新线程的执行信息的时候,我们可以将其设为分离状态。要用到pthread_detach函数
int pthread_detach(pthread_t thread);
thread: 这是一个类型为 pthread_t 的线程标识符,表示需要设置为分离状态的线程。
如果操作成功,pthread_detach 返回 0。
如果失败,返回一个错误码。
注意
分离后的线程不需要join,否则会出错
讨论
那新线程和主线程之间还会相互影响吗,
会的
主线程退出,新线程也会退出,资源依然共享
结论
分离只是一种工作状态,不在意执行结果
底层仍是同一个进程
所以大部分应用软件就是主线程死循环(常驻任务),有新任务的时候,就创建新线程。