美文网首页PythonPython
python多任务--线程

python多任务--线程

作者: 小啊小狼 | 来源:发表于2020-10-14 09:47 被阅读0次

    一、基本概念

    什么是线程

    线程(Thread)也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

    并发和并行

    并发:

    指的是任务数多于cpu核数,通过操作系统的任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,为交替执行的状态,因为切换任务的速度相当快,看上去一起执行而已)

    特点

    • 微观角度:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。
    • 宏观角度:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理。


      image.png
    并行:

    指的是任务数小于等于cpu核数,即任务真的是一起执行的

    特点

    • 同一时刻发生,同时执行。
    • 不存在像并发那样竞争,等待的概念。


      image.png

    同步和异步

    同步(synchronous): 所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。

    简言之,要么成功都成功,失败都失败,两个任务的状态可以保持一致。

    异步(asynchronous):所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

    为什么要使用多线程?

    线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。

    因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程之中拥有独立的内存单元,而多个线程共享内存,从而极大的提升了程序的运行效率。

    线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性,多个线程共享一个进程的虚拟空间。线程的共享环境包括进程代码段、进程的共有数据等,利用这些共享的数据,线程之间很容易实现通信。

    操作系统在创建进程时,必须为改进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。因此,使用多线程来实现并发比使用多进程的性能高得要多。

    二、线程实现

    threading模块

    import time
    import datetime
    from threading import Thread
    
    # Thread类可以用来创建线程对象
    # target:指定执行线程执行的任务(任务函数)
    # args:kwargs: 接收任务函数的参数
    # name:指定线程的名字
    
    def work1(name):
        for i in range(4):
            time.sleep(1)
            print(f'{name}浇花的第{i + 1}秒')
    
    
    def work2(name):
        for i in range(3):
            time.sleep(1)
            print(f'{name}打墙的第{i + 1}秒')
    
    
    # 创建2个线程
    t1 = Thread(target=work1, args=('小狼',), name="线程一")
    t2 = Thread(target=work2, kwargs={"name": "liang"}, name='线程二')
    
    # 启动线程  :异步执行的状态
    begin_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
    print("begin_time:",begin_time)
    t1.start()
    t2.start()
    t1.join()  # 默认等待子线程1执行结束
    t2.join()  # 默认等待子线程2执行结束
    
    end_time = datetime.datetime.now().strftime('%H:%M:%S.%f')
    print("end_time:",end_time)
    # 主线程等待子线程执行结束之后再往下执行
    print("执行结束")
    
    #输出
    begin_time: 14:45:15.724900
    liang打墙的第1秒
    小狼浇花的第1秒
    小狼浇花的第2秒
    liang打墙的第2秒
    liang打墙的第3秒
    小狼浇花的第3秒
    小狼浇花的第4秒
    end_time: 14:45:19.729378
    执行结束
    

    可以看到,如果两个任务单线程分别执行,则会消耗7秒的时间,而运用多线程实现多个任务,耗费的总时间不是多个任务时间之和,而是单个运行时间最长的任务(4s)。

    自定义线程

    继承threading.Thread来自定义线程类,其本质是重构Thread类中的run方法,同时执行多个相同任务,就是创建多次线程对象

    import time
    from threading import Thread
    """
    通过线程类的形式来实现多线程
    """
    
    class MyThread(Thread):
        """自定义的线程类"""
        def __init__(self, name):
            super().__init__()
            self.name = name
        def run(self):
            """线程执行的任务函数"""
            for i in range(4):
                time.sleep(1)
                print(f'{self.name}浇花的第{i + 1}秒')
    
    m = MyThread('小狼')
    m2 = MyThread('liang')
    m.start()
    m2.start()
    m.join()
    m2.join()
    print('执行结束')
    
    #输入
    liang浇花的第1秒
    小狼浇花的第1秒
    liang浇花的第2秒
    小狼浇花的第2秒
    liang浇花的第3秒
    小狼浇花的第3秒
    小狼浇花的第4秒
    liang浇花的第4秒
    执行结束
    

    三、多线程特点

    守护线程

    里使用setDaemon(True)把所有的子线程都变成了主线程的守护线程,因此当主进程结束后,子线程也会随之结束。所以当主线程结束后,整个程序就退出了。

    import time
    from threading import Thread
    """
    通过线程类的形式来实现多线程
    """
    
    class MyThread(Thread):
        """自定义的线程类"""
        def __init__(self, name):
            super().__init__()
            self.name = name
        def run(self):
            """线程执行的任务函数"""
            for i in range(4):
                time.sleep(1)
                print(f'{self.name}浇花的第{i + 1}秒')
    
    m = MyThread('小狼')
    m2 = MyThread('liang')
    m.setDaemon(True)
    m2.setDaemon(True)
    m.start()
    m2.start()
    

    我们可以发现,设置守护线程之后,当主线程结束时,子线程也将立即结束,不再执行。

    主线程等待子线程结束

    为了让守护线程执行结束之后,主线程再结束,我们可以使用join方法,让主线程等待子线程执行。

    #此处自定义线程类代码与之前一致,省略
    m = MyThread('小狼')
    m2 = MyThread('liang')
    m.start()
    m2.start()
    m.join()
    m2.join()
    
    time.sleep(2.5)
    print('执行结束')
    

    多线程共享全局变量

    线程是进程的执行单元,进程是系统分配资源的最小单位,所以在同一个进程中的多线程是共享资源的。

    n = 0
    def work1():
        global n
        for i in range(10):
            n += 1
    
    def work2():
        global n
        for i in range(10):
            n += 1
    
    t1 = Thread(target=work1)
    t2 = Thread(target=work2)
    
    # 启动线程
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    
    print("n:", n)
    #输出
    n: 20
    

    可以看出,多线程之间是共享全局变量的

    互斥锁

    由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一时刻允许一个线程执行操作。

    由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。

    当把计算的数据很大,就会出现资源竞争,导致执行的结果不准确


    image.png

    为了方式上面情况的发生,就出现了互斥锁(Lock),在可能出现资源竞争的地方加上互斥锁,保证线程的安全

    import threading
    
    n = 100
    def work1():
        global n
        for i in range(500000):
            lock.acquire()
            n += 1
            lock.release()
    
    
    def work2():
        global n
        for i in range(500000):
            lock.acquire()
            n += 1
            lock.release()
    
    # 创建一把锁
    lock = threading.Lock()
    # 上锁
    t1 = threading.Thread(target=work1)
    t2 = threading.Thread(target=work2)
    
    # 启动线程
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("n:", n)
    

    三、GIL(Global Interpreter Lock)全局解释器锁

    在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。

    GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。

    Python多线程的工作过程:
    python在使用多线程的时候,调用的是c语言的原生线程。

    • 拿到公共数据
    • 申请GIL
    • python解释器调用os原生线程
    • os操作cpu执行运算
    • 当该线程执行时间到后,无论运算是否已经执行完,gil都被要求释放
    • 进而由其他进程重复上面的过程
    • 等其他进程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。

    python中线程的缺陷,以及适用场景:

    由于GIL锁的存在,python中的多线程在同一时间没办法同时执行(即没办法实现并行)

    1、CPU密集型代码(各种循环处理、计算等等),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。

    2、IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。

    练习:

    有10000个url地址(假设请求每个地址需要0.5秒),请设计程序一个程序,获取列表中的url地址,使用1000个线程去发送这10000个请求,计算出总耗时!

    # 计算时间的装饰器
    def decorator(func):
        def wrapper():
            # 函数执行之前获取系统时间
            start_time = time.time()
            func()
            # 函数执行之后获取系统时间
            end_time = time.time()
            print('执行时间为:', end_time - start_time)
            return end_time - start_time
    
        return wrapper
    
    #生成器生成10000个url地址
    urls_g = (f"https://www.baidu.com{j}" for j in range(10000))
    
    #定义线程类
    class MyThread(threading.Thread):
        #重写run方法,指定要执行的任务逻辑
        def run(self):
            while True:
                try:
                    url = next(urls_g)
                except StopIteration:
                    break
                else:
                    print(F'{self}发送请求{url}')
                    time.sleep(0.5)
    
    @decorator
    def main():
        t_list = []
        # 创建4个线程
        for i in range(1000):
            t = MyThread()  # 创建线程对象
            t.start()  # 开启该线程
            t_list.append(t)
    
        # 遍历所有线程对象,设置主线等待子线程执行完
        for t in t_list:
            t.join()
    
    res = main()
    

    最终执行时间为5.16秒,如果单线程执行的话至少需要5000多秒,体现了多线程在执行IO密集型任务时的性能效率

    相关文章

      网友评论

        本文标题:python多任务--线程

        本文链接:https://www.haomeiwen.com/subject/ufevpktx.html