玩蛇(Python) - 并发编程之多线程

一、线程简介

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。每个线程自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。

Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以多线程在Python中只能交替执行,无论多少个线程,也只能用到1个核。设计上以通过多进程实现多核任务,如果想要同时运行多个 I/O 密集型任务,则多线程仍然是一个合适的模型。

Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行。

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定。

二、Python多线程编程实例

2.1 Thread语法

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

应当始终使用关键字参数调用此构造函数。 参数如下:

group:应为 None, 保留给将来实现 ThreadGroup 类的扩展使用。

target: 是用于 run() 方法调用的可调用对象。默认是 None,表示不需要调用任何方法。

name: 是线程名称。 在默认情况下,会以 "Thread-N" 的形式构造唯一名称,其中 N 为一个较小的十进制数值,或是 "Thread-N (target)" 的形式,其中 "target" 为 target.__name__,如果指定了 target 参数的话。

args: 是用于发起调用目标函数的参数列表或元组。 默认为 ()。

kwargs: 是用于调用目标函数的关键字参数字典。默认是 {}。

如果不是 None,daemon 参数将显式地设置该线程是否为守护模式。 如果是 None (默认值),线程将继承当前线程的守护模式属性。

如果子类型重载了构造函数,它一定要确保在做任何事前,先发起调用基类构造器(Thread.__init__())。

daemon:一个布尔值,表示这个线程是否是一个守护线程(True)或不是(False)。 这个值必须在调用 start() 之前设置,否则会引发 RuntimeError 。它的初始值继承自创建线程;主线程不是一个守护线程,因此所有在主线程中创建的线程默认为 daemon = False

2.2 Thread方法

  • start()

开始线程活动。它在一个线程里最多只能被调用一次。 它安排对象的 run() 方法在一个独立的控制线程中被调用。如果同一个线程对象中调用这个方法的次数大于一次,会抛出 RuntimeError 。

  • run()

代表线程活动的方法。你可以在子类型里重载这个方法。 标准的 run() 方法会对作为 target 参数传递给该对象构造器的可调用对象(如果存在)发起调用,并附带从 args 和 kwargs 参数分别获取的位置和关键字参数。使用列表或元组作为传给 Thread 的 args 参数可以达成同样的效果。

  • join(timeout=None)

等待,直到线程终结。这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结。不管是正常终结还是抛出未处理异常,或者直到发生超时,超时选项是可选的。

当 timeout 参数存在而且不是 None 时,它应该是一个用于指定操作超时的以秒为单位的浮点数或者分数。因为 join() 总是返回 None ,所以你一定要在 join() 后调用 is_alive() 才能判断是否发生超时。如果线程仍然存活,则 join() 超时。

当 timeout 参数不存在或者是 None ,这个操作会阻塞直到线程终结。

一个线程可以被合并多次。如果尝试加入当前线程会导致死锁, join() 会引起 RuntimeError 异常。如果尝试 join() 一个尚未开始的线程,也会抛出相同的异常。

  • is_alive()

返回线程是否存活。当 run() 方法刚开始直到 run() 方法刚结束,这个方法返回 True 。模块函数 enumerate() 返回包含所有存活线程的列表。

2.3 定时器对象

表示一个操作应该在等待一定的时间之后运行,相当于一个定时器。 Timer 类是 Thread 类的子类,因此可以像一个自定义线程一样工作。

与线程一样,定时器也是通过调用其 Timer.start 方法来启动的。 定时器可以通过调用 cancel() 方法来停止(在其动作开始之前)。 定时器在执行其行动之前要等待的时间间隔可能与用户指定的时间间隔不完全相同。

  • class threading.Timer(interval, function, args=None, kwargs=None)

创建一个定时器,在经过 interval 秒的间隔事件后,将会用参数 args 和关键字参数 kwargs 调用 function。如果 args 为 None (默认值),则会使用一个空列表。如果 kwargs 为 None (默认值),则会使用一个空字典。

  • cancel()

停止定时器并取消执行计时器将要执行的操作。仅当计时器仍处于等待状态时有效。

2.4 进度条编程示例

2.4.1 源码

#-*- coding: UTF-8 -*-
import threading
import sys
import time
import os
#常数定义
PROGRESS_BAR_LENGTH = 101
#全局变量定义
COUNT = 1
class ThreadProgressBar (threading.Thread):
    def __init__(self, name):
        threading.Thread.__init__(self)
        self.name = name
    def run(self):
        print ("\n开始线程:" + self.name)
        normal_progress_bar()
        print ("\n退出线程:" + self.name)
def normal_progress_bar():
    global COUNT
    start_time = time.time()
    while(True):
        COUNT = COUNT + 1
        #进度100时退出        
        if COUNT >= PROGRESS_BAR_LENGTH:
            break
       
        end_time = time.time()
        #\r 表示将光标的位置回退到本行的开头位置,重新打印
        print("\r", end="")
        #带百分比进度,打印方块
        print("Download progress: {}%: ".format(COUNT), "▋" * (COUNT // 2),'[{:.2f}s]'.format(end_time-start_time), end="")
        sys.stdout.flush()
       
        #模拟IO读写时间
        time.sleep(0.05)             
   
if __name__ == '__main__':
    #打印一个提示语
    #正常处理,初始化全局变量
    COUNT = 1
    print("正常,执行进度演示".center(PROGRESS_BAR_LENGTH // 2,"-"))
    normal_progress_bar()
   
    #打印一个提示语
    print('')
    print("多线程,执行进度演示".center(PROGRESS_BAR_LENGTH // 2,"*"))
    #多线程处理,初始化全局变量
    COUNT = 1
    # 创建新线程
    threadList = []
    for i in range(os.cpu_count()):
        t = ThreadProgressBar(i)
        t.start()
        threadList.append(t)
    for t in threadList:
        t.join()

2.4.2 验证(单线程执行4.99秒,多线程执行0.31秒)

GitHub - ShanGouXueHui/PythonCases: Typical python algorithm samples

##山狗学会 License Start##

转载请注明出处,"今日头条"创作者"山狗学会“ ,注明出处即授权,未注明出处罚款100万元

主页链接:山狗学会主页

##山狗学会 License End##

相关文章

python 锁Lock功能及多线程程序锁的使用和常见功能示例

锁(Lock)是Python中的一个同步原语,用于线程之间的互斥访问。它可以用来保护共享资源,确保在任意时刻只有一个线程可以访问共享资源,从而避免多线程并发访问引发的数据竞争和不一致性。下面分别详细说...

一文扫盲!Python 多线程的正确打开方式

一、多线程:程序世界的 "多面手"(一)啥是多线程?咱先打个比方,你去餐厅吃饭,一个服务员同时接待好几桌客人,每桌客人就是一个 "线程",服务员同时处理多桌事务就是 &...

python 多线程程序加锁、解锁、锁应用场景示例

锁(Lock)是Python中的一个同步原语,用于线程之间的互斥访问。它可以用来保护共享资源,确保在任意时刻只有一个线程可以访问共享资源,从而避免多线程并发访问引发的数据竞争和不一致性。下面分别详细说...

Python中的“锁”艺术:解锁Lock与RLock的秘密

Python中的“锁”艺术:解锁Lock与RLock的秘密引言随着计算机性能的不断提升以及多核处理器的普及,多线程编程已成为现代软件开发不可或缺的一部分。然而,当多个线程试图同时修改同一份数据时,就可...

24-2-Python多线程-线程操作(python多线程怎么用)

2-线程操作在Python程序中,可以通过“_thread”和“threading(推荐使用)”这两个模块来处理线程。在Python 3程序中,thread模块已废弃。可以使用 threading 模...

Python 如何通过 threading 模块实现多线程。

先熟悉下相关概念多线程是并发编程的一种方式,多线程在 CPU 密集型任务中无法充分利用多核性能,但在 I/O 操作(如文件读写、网络请求)等待期间,线程会释放 GIL,此时其他线程可以运行。GIL是P...