我记不住的线程pthreads及NPTL

背景:平时所能接触的语言是C、Java和Python,写代码的时候需要对多线程的使用和原理有一定的理解才能写出优质的代码,所以这里说一下POSIX thread和pthreads以及C/Java/Python所用的线程模型以及具体的实现及测试。

1. 线程产生和历史

单进程-->>多进程-->>多线程

进程是对程序运行过程中所涉及的数据、代码、资源和执行信息的封装,起到管理的作用。

早期计算机只支持单进程,其实就是独占CPU和内存等资源进行批处理作业,执行完一个再次执行下一个,此时CPU的处理速度是一个瓶颈,都是计算密集型的作业。

后面演化出了多进程,可以在写文档的同时听歌曲,使用并发(concurrency)的方式执行,从人的角度来看感觉多个程序是并行(parallelism)的,其实对于计算机来说是通过时间片和切换进程来实现交替运行,这会导致上下文切换。

多线程的引入是为了进一步在设计、性能、易用性上优化多进程,最早也称作子进程(sub-process)或轻量级进程(light-weight process),是把一个进程拆分成多个线程,进程负责线程共享资源的管理,线程负责执行,以及后续的上下文切换也变为了线程的切换,原来进程负责的栈、程序计数器、寄存器现在有线程负责,如下图所示。而多线程可以使一个程序运行在多个CPU核上,使代码的执行效率更高。如果只是支持多进程,只会让多进程在多个CPU核上并行执行,而各个进程的内部的各类逻辑不能并行执行。在引入了多线程后,不仅多个程序可以并行执行,各个程序的内部也可以并行执行,也就是说 颗粒度更小了。

当处理多个计算密集型的作业的时候会有很多线程运行,会导致各个线程运行的时间片较短,会进行频繁的切换上下文,线程的切换包括了上下文的切换耗时和线程调度的耗时,同时CPU缓存也会失效,所以频繁的切换上下文会导致程序执行变慢,即 不是线程越多,程序执行的就会越快。

2. 线程模型

线程模型:用户线程如何映射到内核线程,用户线程在用户空间,内核线程在内核空间中,

1:1 (kernel-level threading) 一个用户线程对应一个内核线程,优点是简单是真正的线程,缺点是当进行系统调用的时候会进行用户态和内核态之间的上下文切换,相对耗时。线程的调度由操作系统完成。

M:1 (user-level threading) 多个用户线程对应一个内核线程,多个用户线程的调度是由虚拟机完成优点是应用程序操作用户线程无需进行系统调用的上下文切换,缺点是无法完全利用多核处理器,同时当一个用户线程堵塞会被操作系统的系统调度让出时间片,也就是说这个线程不再Ready了不再分配时间片了,导致其他用户线程也无法工作了。所以为了避免这种情况,可以使用非堵塞的方法或函数进行。这些用户线程也可以称作协程。

M:N (hybrid threading) 多个用户线程对应多个内核线程,较为复杂,go语言的线程就使用这个模型来实现也称作为协程。

3. 任务类型(cpu_bound/io_bound)

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低.计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差

4. 操作系统的提供的线程以及C语言的使用

POSIX(Portable Operating System Interface)的出现是为了统一各类操作系统的接口,使程序能更好的移植。

POSIX threads 也称作为Pthreads,一个单进程可以包含多个线程,多个线程共享同样的全局内存包括全局变量和堆段,但是每个线程有自己的栈存放一些局部变量。

POSIX thread(pthread)不是一个具体的关于线程的实现,而是一个API规范和标准,它在<pthread.h>中定义了各类以pthread_开头的函数,用于线程的创建、取消、退出、join等。

而LinuxThreads和NPTL(Native POSIX Thread Library)是依据POSIX Thread规范进行具体实现的两种不同的方式。自glibc 2.3.2版本开始NPTL就能使用了,NPTL是一种更现代的实现方式以及更好的性能

The current version of POSIX.1 is IEEE Std 1003.1-2017.

gcc xxx.c -lpthread 

gcc xxx.c -pthread

我们直接man 7 pthreads来查看一下关于pthreads的介绍和描述。

如何查看当前Linux系统中我们的线程具体的实现是哪一种呢?

 而自glibc 2.3.2版本开始NPTL就能使用了,我们先看一下glibc使用的是哪个版本。

查看glibc的版本号?

ldd --version

或找到一个命令,这里假设ls,我们使用ldd命令分析ls命令的依赖:

然后我们执行一下libc.so.6,注意这个库是可以直接执行的

或者直接执行 $(ldd /bin/ls | grep libc.so |awk '{print $3}') 来进行版本的查看

查看线程的实现方式?

$ getconf GNU_LIBPTHREAD_VERSION

5. 操作系统的线程和Java的线程的区别和联系

Java对线程的实现有两种方法,第一个是GreenThread,第二个是NativeThread。Java早期实现是GreenThread已经被废弃,Java更现代的实现是NativeThread。

GreenThread采用了线程模型M:1(user-level threading),开发这个线程库的团队叫做GreenTeam,所以这个线程库也称作GreenThread,这个线程库存在于早期的JDK1.1/1.2里面。

Java后期提供的NativeThread线程库采用的是1:1 (kernel-level threading),只不过是对操作系统提供的操作线程的系统调用的二次封装,线程的调度完全有操作系统完成,因此Java线程库非常简单。但是因为Java是跨平台的,所以需要屏蔽各种操作系统之间的差异,例如Windows和Linux,需要做一个中间层并重新定义线程的状态和优先级。

Linux操作系统的线程状态:New、Ready、Running、Waiting、Terminated

Java的线程状态:New、Runnable、Waiting、Timed_Waiting、Blocked、Terminated

也就是说Java重新定义了线程的状态、线程优先级、触发条件以及与操作系统的线程状态的映射关系

6. 操作系统的线程和Python的线程的区别和联系

Python的线程库也是采用了1:1 (kernel-level threading)也是对操作系统的线程的系统调用进行的二次封装,线程的调度由操作系统完成,区别在于Python存在GIL锁(Global Interpreter Lock),多线程在Python中只能交替运行,无法利用多核的CPU,即有多个线程运行,只能在一个核心上交替运行,无法使用多核并行处理,多线程处理计算密集型CPU_bound任务,如下图所示:

import threading, multiprocessing

print(multiprocessing.cpu_count())
def loop():
    x = 0
    while True:
        x = x ^ 1

for i in range(multiprocessing.cpu_count()):
    t = threading.Thread(target=loop)
    t.start()

上图为以进程进行查看,整体是28.2us,下图按 H 以线程进行查看,共4个线程在运行,基本都是在25%左右,也就是说这4个线程只跑在了这一个核心上,无法多核心并行处理。

我使用的是四核心处理器,但是我使用Python多线程死循环执行计算密集型任务CPU-bound,跑不满这四个核心。

而使用C语言或Java语言进行执行可以跑满四个核心,我使用C语言及Java跑了一下,如下图所示:

使用C语言运行计算密集型任务(CPU_bound),CPU能跑满98.3us,下面的进程能跑到384.9,说明咱们四个核心已经跑满了,同时我们以 线程方式查看一下,按H即可显示如下图所示:

四个线程也可以跑满,几乎在95%左右。

同理我们使用Java语言进行测试:

CPU能跑满99.1us, 下面的进程能跑满到383.6,说明咱们使用Java语言跑计算密集型任务(CPU_bound)四个核心已经跑满了。

GIL的作用使开发者在写代码时无需担心线程安全的问题,同时限制了多线程在执行计算密集型CPU-bound的性能。而对于io-bound(http请求响应或文件读写)来说,GIL的影响不大仍然可以使用多线程提升性能。 GIL只影响的是计算密集型的性能,对io密集型来说无多大影响。

对于同样的io密集型任务:

对于io操作来看,进程和线程的区别不大,但是进程的开销和代价更大,为了节省资源使用线程来处理io操作更优。

对于同样的cpu密集型任务:

对于cpu密集型来看,python的进程比线程更优,在python线程处理cpu密集型任务时候GIL无法利用多核来提升执行效率,此时使用python的进程更优。

总之:python语言 Multithreading for IO-bound tasks. Multiprocessing for CPU-bound tasks.

Python也在移除GIL,短期计划是在版本3.13或3.14进行移除。

参考资料:

1. https://en.wikipedia.org/wiki/Green_thread

2. https://en.wikipedia.org/wiki/Pthreads

3. https://en.wikipedia.org/wiki/Thread_(computing)

4. Vonage API Developer

5. Difference Between Multithreading vs Multiprocessing in Python - GeeksforGeeks

6. Operating Systems: Threads

7. https://www.cs.rochester.edu/u/ferguson/csc/c/c-for-java-programmers.pdf

8. 进程 vs. 线程 - 廖雪峰的官方网站

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Penguinbupt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值