前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python 线程初窥

python 线程初窥

作者头像
用户3147702
发布2022-06-27 13:29:29
2390
发布2022-06-27 13:29:29
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

上一篇文章中,我们详细介绍了 python 中的协程。 一文讲透 python 协程

python 通过 yeild 关键字让出 CPU 的执行,实现类似于系统中断的并发工作,这就是被称为“微线程”的 python 协程调度机制。 但是,这并不是真正意义上的并发,几乎在所有编程语言中,都提供了多线程并发的机制,python 也同样提供了多线程并发机制,本文我们就来详细介绍 python 中的线程机制。

2. python 中的线程

2.1. 操作系统中的线程

此前,我们曾经介绍过 linux 环境中的线程和相关的 api。

一个程序的一次执行就是一个进程,而进程中可能包含多个线程,线程是 CPU 调度的最小单位,同一个进程中的多个线程共享了进程中的程序文本、全局内存和堆内存、栈以及文件描述符等资源,而同一个计算机上的多个进程则共享文件系统、磁盘、打印机等硬件资源。 线程的切换相对于进程切换耗费的资源更少,因此效率更高,尤其是在同一个进程中的若干个线程之间切换,这是因为进程的切换需要执行系统陷阱、上下文切换和内存与高速缓存的刷新,而由于同一进程中的多个线程共享了这些资源,在线程切换过程中,系统无需对这些资源进行任何操作,因此可以获得更高的效率。 可见,线程调度是程序设计中一个非常重要且实用的技术。

2.2. thread 与 threading

python 标准库中维护线程的模块有两个 — thread 和 threading。 由于 thread。 模块在很多方面存在不尽如人意的问题,例如在多线程并发环境中,当主线程退出时,所有子线程会随之立即退出,甚至不会进行任何清理工作,这通常是无法接受的,所以一般并不建议使用。 在 python3 中 thread 模块已经被更名为 _thread 模块,以便从名字上说明其不被推荐使用。 如果你熟悉 java 的线程模型,你会发现 python 的线程模型与 java 的非常类似,没错,python 的线程模型就是参照 java 线程模型设计的,但 python 的线程目前还没有优先级,没有线程组,线程还不能被销毁、停止、暂停、恢复或中断。

2.2.1. threading 模块中的类

threading 模块包含下列对象:

threading 模块中的对象

对象

描述

Thread

执行线程对象

Timer

运行前等待一定时间的执行线程对象

Lock

锁对象

Condition

条件变量对象,用于描述线程同步中的条件变量

Event

事件对象,用于描述线程同步中的事件

Semaphore

信号量对象,用于描述线程同步中的计数器

BoundedSemaphore

存在阈值信号量对象

Barrier

栅栏对象,线程同步中让多个线程执行到指定位置

3. Thread 类

threading 模块中最重要的类就是 Thread 类。 每个 Thread 对象就是一个线程。 下面是 Thread 类中包含的属性和方法。

Thread 类属性及成员方法

属性

备注

name

线程名称

ident

线程标识符

deamon

bool 类型,表示该线程是否为守护线程

start()

开始执行线程

run()

用于定义线程功能,通常在子类中由开发者复写

join(timeout=None)

直到启动的线程终止或到超时时间前一直挂起

is_alive()

返回 bool 类型,表示该线程是否存活

4. python 线程的创建与终止

4.1. 创建线程

有两种方法可以创建线程,但更推荐第二种:

4.1.1. 以一个函数或一个可调用类实例为参数创建 Thread 对象

代码语言:javascript
复制
from threading import Thread
from time import sleep, ctime

def sleep_func(i):
    print('start_sleep[%s]' % i)
    sleep(i+1)
    print('end_sleep[%s]' % i)

if __name__ == '__main__':
    print('start at %s'% ctime())
    threads = list()
    for i in range(3):
        t = Thread(target=sleep_func, args=[i])
        threads.append(t)

    for i in range(3):
        threads[i].start()

    for i in range(3):
        threads[i].join()
    print('end at %s' % ctime())

打印出了:

start at Mon May 6 12:25:18 2019 start_sleep[0] start_sleep[1] start_sleep[2] end_sleep[0] end_sleep[1] end_sleep[2] end at Mon May 6 12:25:21 2019 并且我们观察到每过 1 秒便打印出一个 end_sleep,这说明他们确实是并行执行的。 本应共执行 1+2+3 = 6 秒的,由于线程的并发执行,实际只用了 3 秒。

4.1.2. 派生 Thread 并创建子类实例

代码语言:javascript
复制
from threading import Thread
from time import sleep, ctime

class myThread(Thread):
    def __init__(self, nsec):
        super().__init__()
        self.nsec = nsec

    def run(self):
        print('start_sleep[%s]' % self.nsec)
        sleep(self.nsec + 1)
        print('end_sleep[%s]' % self.nsec)

if __name__ == '__main__':
    print('start at %s'% ctime())
    threads = list()
    for i in range(5):
        t = myThread(i)
        threads.append(t)

    for thread in threads:
        thread.start()

    for thread in threads:
        thread.join()
    print('end at %s' % ctime())

运行与上面通过函数实现的例子是完全一致的。 由于类中可以添加私有成员来保存成员方法运行结果或其他数据,这个方法显得更为灵活。

4.1.3. start 方法

只有在 Thread 对象的 start 方法被调用后才会开始线程活动。 start 方法在一个线程里最多只能被调用一次,否则会抛出 RuntimeError。 start 最终执行的逻辑代码就是 Thread 类的 run 方法。

4.1.4. join 方法

join 方法有一个可选的 timeout 参数,这个方法会阻塞调用这个方法的线程,直到被调用 join() 的线程终结或达到 timeout 秒数。 当然,一个线程可以被 join 很多次,但 join 当前线程会导致死锁。 如果被 join 的线程不处于 alive 状态,则会引起 RuntimeError 异常。

4.2. 线程的终止

在线程中,可以通过 sys.exit() 方法或抛出 SystemExit 异常来使线程退出。 但是,在线程外,你不能直接终止一个线程。

5. threading 模块提供的函数

除了最重要的 Thread 类,threading 模块中还提供了下面的几个有用的函数。

threading 模块提供的函数

函数

说明

active_count()

返回当前活动的 Thread 对象个数

current_thread()

返回当前线程的 Thread 对象

enumerate()

返回当前活动的 Thread 对象列表

settrace(func)

为所有线程设置一个 trace 函数

setprofile(func)

为所有线程设置一个 profile 函数

local()

创建或获取线程本地数据对象

stack_size(size=0)

返回新创建线程的栈大小或为后续创建的线程设定栈的大小 为 size

get_ident()

返回当前线程的 “线程标识符”,它是一个非零的整数

main_thread()

返回主 Thread 对象。一般情况下,主线程是Python解释器开始时创建的线程

6. python 线程与 GIL

上面我们通过一个实际的例子已经看到,三个线程分别 sleep 1、2、3 秒,执行结果却只话费了 3 秒,足以见得并发环境下的性能优势。 然而,众所周知,python 解释器有多个版本的实现,其中 CPython 以其优秀的执行效率而被广泛使用,也成为了 python 的默认解释器,另一个被广泛使用的是 PyPy 解释器,这两个解释器都有一个先天缺陷,那就是并非线程安全,这是出于在高性能 和复杂度之间做出的让步。 由于 python 解释器 CPython、PyPy 的实现限制,导致实际执行中会设置全局解释安全锁(GIL),一次只允许使用一个线程执行 Python 字节码,因此,一个 Python 进程通常不能同时使用多个 CPU 核心,多线程的程序也并不总是真的在并发执行的,但这并不是 python 语言本身的限制,Jython 与 IronPython 并没有这样的限制。 即便如此,所有标准库中的阻塞式 IO 操作,在等待操作系统返回结果时都会释放 GIL,因此对于 IO 密集型程序,使用多线程并发是可以有效提升性能的,例如我们可以让多个线程可以同时等待或接收 IO操作的返回数据或者在一个线程执行下载任务的同时,另一个线程负责显示下载进度。 time.sleep 操作也是一样,time.sleep 操作会立即释放 GIL 锁,并让线程阻塞等待参数传入的秒数,直到此后才再次请求获取 GIL 锁,这就是上文例子中多线程并发缩短了执行时间的原因。 但对于 CPU 密集型程序,python 线程则显得有些无力,不过这并不是没有办法去优化,我们后文会详细介绍。

6.1. 并发计算斐波那契数列

斐波那契数列的计算是一个典型的 CPU 密集型操作,下面的例子展示了分别在串行环境与并发环境下计算10次斐波那契数列第 100000 个元素的耗时:

代码语言:javascript
复制
import time
from threading import Thread

class fibThread(Thread):
    def __init__(self, stop):
        super().__init__()
        self.stop = stop
        self.result = None

    def run(self):
        self.result = fib(self.stop)

def fib(stop):
    a = 0
    b = 1
    for _ in range(stop):
        a, b = a + b, a
    return a

if __name__ == '__main__':
    stime = time.time()
    for _ in range(10):
        fib(100000)
    print('serial time %ss' % (time.time() - stime))

    stime = time.time()
    threads = list()
    for _ in range(10):
        t = fibThread(100000)
        threads.append(t)

    for thread in threads:
        thread.start()

    for thread in threads:
        thread.join()
    print('concurrent time %ss' % (time.time() - stime))

打印出了:

serial time 0.9864494800567627s concurrent time 0.9564151763916016s

虽然从结果上,多线程并发确实有着略微的性能提升,但远远没有达到我们预期的优化 90% 这个问题如何进一步优化,敬请关注接下来的文章。

7. 参考资料

《python 核心编程》。 https://docs.python.org/zh-cn/3.6/library/threading.html。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. python 中的线程
    • 2.1. 操作系统中的线程
      • 2.2. thread 与 threading
        • 2.2.1. threading 模块中的类
    • 3. Thread 类
    • 4. python 线程的创建与终止
      • 4.1. 创建线程
        • 4.1.1. 以一个函数或一个可调用类实例为参数创建 Thread 对象
        • 4.1.2. 派生 Thread 并创建子类实例
        • 4.1.3. start 方法
        • 4.1.4. join 方法
      • 4.2. 线程的终止
      • 5. threading 模块提供的函数
      • 6. python 线程与 GIL
        • 6.1. 并发计算斐波那契数列
        • 7. 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档