一文学会使用Python多线程

297 阅读6分钟

启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第36天,点击查看活动详情

Python开发效率高,但是执行效率低,Python学习成本低,但是执行效率低,Python使用场景广泛,但是执行效率低,Python的执行效率一直是被诟病的一个点,为啥呢,我们基于多线程来了解一下。

GIL

谈到Python的效率,一个绕不开的话题就是GIL(Global Interpreter Lock)全局解释器锁,为啥,因为他是导致Python无法实现真正的多线程的"罪魁祸首",具体情况我们从一个程序的运行来聊:

程序的代码(无论是脚本还是二进制)本身是保存在硬盘当中的,类似文本,没有任何功能和含义,那么运行程序就是开辟一个内存空间,然后把这个程序的代码拿到内存当中进行运行,那么这样的一次运行就是一个进程(又被称为重量级进程),大家可以在windows系统可以通过进程管理器(ctrl+shift+delete 然后选择任务管理器)看到当前运行的进程,但是在一个进程的运行过程当中,不是所有的功能都是依次运行的,存在着并行的可能,比如一个ppt,在播放音乐的同时也在播放画面,那么这个时候在进程当中就会出现一个一个执行的分支,每个分支就是一个线程(又被称为轻量级进程),这些线程共享进程的内存空间,完成进程的子任务,线程共享进程的内存,数据交换很方便,但是问题来了,由于共享导致了资源抢占,程序逻辑错误,比如: 本来运行音乐的线程被运行视频的线程抢占了资源,那么至少会出现画音不同步的情况。

遇到这样的问题怎么办呢,Python就提供了GIL全局解释器锁,对运行的线程进行加锁,保证资源不被抢占,但是同样的问题也就来了,这样单位时间内只能有一个线程运行,所以,效率......

异步并非

上面说了,由于GIL Python无法实现线程并行,那么Python居然还有多线程,啥逻辑,当然不是自己抽自己,而是采用了一种取巧的办法,异步并非:就是将要执行的线程进行切分,每个线程的事件被切分成时间片,然后运行时间片,当一个线程的一个时间片运行进入挂起或等待状态(比如io阻塞),这个时候去运行另外的一个线程的时间片,这样交替执行,时间片足够小,切换足够块,那么就会给大家一种线程在并行的感觉,实际上并没有,只是异步并发。

正常的执行顺序

image.png

并行执行

image.png

异步并非

image.png

到这里可能有的小伙伴会认为Python的多线程并不能提高程序运行的效率,其实不然,因为很多程序(尤其是io型程序)会出现阻塞的状态,这种状态下进程被占用,但是也没有处理数据,所以Python的多线程实际上对io型程序(文件读写,网络请求)的执行效率还是有很大提升的,但是对计算型程序的提升确实不明显。

thread 与 threading

提到Python多线程,好多小伙伴第一个想到的模块是threading模块,但是实际上更加底层的是thread模块,这个模块由于操作的难度比较高,所以不常被大家使用,大家常用的是threading模块,threading模块的使用一般有两种套路: 1、直接使用,其实这种方式是最好看懂的,有套路可以使用:

(1)首先定义好需要使用多线程运行的程序,这里把脚本看作一个进程,脚本里可以定义在这个进程下运行的线程,一般使用函数。

(2)定义好功能之后,创建线程加载函数,注意,这个时候不要启动线程,而是把线程放到一个容器当中,比如,列表。

(3)循环列表运行线程,对线程进行挂起,

import time
import threading

#首先定义好需要使用多线程运行的程序
def fun1():
    print("我是线程1的功能")
    time.sleep(3) #这里表示线程阻塞了3秒
    print("线程1功能运行结束")

def fun2():
    print("我是线程2的功能")
    time.sleep(2) #这里表示线程阻塞了2秒
    print("线程2功能运行结束")

#定义好功能之后,创建线程加载函数并把线程放到一个容器当中,比如,列表。
threads = [] #存放线程的容器
t1 = threading.Thread(target = fun1,args = ()) #创建线程,target是线程加载的函数,args是线程加载函数需要的参数
t2 = threading.Thread(target = fun2,args = ())

threads.append(t1)
threads.append(t2)

#循环列表运行线程,对线程进行挂起
for t in threads:
    t.start() #启动线程
    
for t in threads:
    t.join() #挂起线程

这里挂起的原因很简单,如果没有挂起或者守护线程,进程运行结束不会等待线程结束,会导致进程结束了,线程被迫终止或者线程还在运行,线程被迫终止自然程序没有运行完是不行的,线程依然运行,在进程结束,线程依旧运行的情况下,线程结束并不会释放内存,那么就出现了僵尸进程(嘿嘿嘿,怕不怕),就是内存被占用,但是啥也不干。

2、通过重写类实现多线程,这是官方提供的一种套路,编写线程更加的灵活:

(1) python threading类的Thread方法提供了可以供开发者从新的方法run,开发者通过从写run方法,可以定义自己的线程类:

源码(太长,我们截取看看):

class Thread:
    """A class that represents a thread of control.

    This class can be safely subclassed in a limited fashion. There are two ways
    to specify the activity: by passing a callable object to the constructor, or
    by overriding the run() method in a subclass.

    """
   ......

    def run(self):
        """Method representing the thread's activity.

        You may override this method in a subclass. The standard run() method
        invokes the callable object passed to the object's constructor as the
        target argument, if any, with sequential and keyword arguments taken
        from the args and kwargs arguments, respectively.

        """
        try:
            if self._target is not None:
                self._target(*self._args, **self._kwargs)
        finally:
            # Avoid a refcycle if the thread is running a function with
            # an argument that has a member that points to the thread.
            del self._target, self._args, self._kwargs

    .....

(2) 之后的操作和第一个套路类似啦

所以可以编写以下的代码:

import time
import threading
#自定义线程类
class MyThread(threading.Thread): #继承线程类
    def __init__(self,thread_num,sleep_time): 
        """
        自定义线程需要的参数
        thread_num 线程编号
        sleep_time 挂起时间
        """
        self.thread_num = thread_num
        self.sleep_time = sleep_time
        super(MyThread,self).__init__()  #在定义自己init的同时保留父类的init方法
        
    def run(self) -> None:
        """
        定义线程的功能
        """
        print("我是 %s 号线程"%self.thread_num)
        time.sleep(self.sleep_time)
        print("%s 号线程运行结束"%self.thread_num)

task_list = [2,3,1,5,4] #定义线程挂起的时间
threads = [] #容器

for i,t in enumerate(task_list): #enumerate 枚举,把序列和索引输出出来,这个是i代表的是索引,t代表task_list的元素
    mt = MyThread(i,t) #实例化线程
    threads.append(mt) #存放线程到容器

for t in threads:
    t.start() #启动线程

for t in threads:
    t.join() # 挂起线程

嘿嘿嘿,关于Python多线程的使用先聊这么多,之后我们再聊线程详细的原理和参数,欢迎各位大佬多多指点。