美文网首页Python语言Python惠大人工智能
一篇文章搞懂python多线程(理论+实践)

一篇文章搞懂python多线程(理论+实践)

作者: 真理和红鬃烈马 | 来源:发表于2018-10-31 18:36 被阅读36次

    python多线程

    进程和线程是操作系统领域非常重要的概念,对于二者之间的联系与区别,本文不做过多阐述,这方面资料网上有非常多,如有需要请先自行查阅。

    1 基础知识之“鸡肋”的python多线程和GIL

    Python是一种解释型语言,而对于python主流也是官方的解释器CPython来说,每一个进程都会持有一个全局解释锁GIL(Global Interpreter Lock)。一个进程运行python代码时,同一时刻只能有一个线程获得这个GIL锁,如果该进程内的其他线程想要运行时,就必须要等待当前线程阻塞的时候释放全局解释锁,而不能多个线程同时运行在CPU。这点和java多线程运行在多核是不同的。正因如此,导致了python多线程的效率并不能提高单线程执行程序的效率。

    代码如下所示

    
    import time
    
    # 单线程运行 3 * 100000 次乘法
    
    a = 1
    
    b = 1
    
    c = 1
    
    begin = time.time()
    
    for i in range(100000):
    
        a *=2
    
        b *=2
    
        c *=2
    
    end = time.time()
    
    print("共消耗时间 %.2f 秒" % (end - begin)) 
    
    

    输出为

    共消耗时间 0.61 秒

    import time
    import threading
    
    # 创建3个线程各运行 100000 次乘法
    def multiply_op(n):
        for i in range(100000):
            n *= 2
    
    a = 1
    b = 1
    c = 1
    
    begin = time.time()
    
    # 创建3个线程分别对a,b,c运行100000次乘法
    t1 = threading.Thread(target=multiply_op, args=(a,))
    t2 = threading.Thread(target=multiply_op, args=(b,))
    t3 = threading.Thread(target=multiply_op, args=(c,))
    
    # 启动三个线程
    t1.start()
    t2.start()
    t3.start()
    
    # 等待三个线程运行结束
    t1.join()
    t2.join()
    t3.join()
    
    end = time.time()
    
    print("共消耗时间 %.2f 秒" % (end - begin))
    

    输出为

    共消耗时间 0.61 秒

    上述多线程实现的代码如果看不懂的话没有关系,因为我会在后面进行讲解,在这里我们只需要观察到结果,也就是多线程实现的效率并没有对单线程有所提高,这是因为多个线程在轮流获得GIL,并不是并发执行。事实上多线程因为增加了各个线程之间切换时调度资源的时间,反而比起单线程程序效率有所下降。

    这样看来,python中的多线程确实如人们所说十分“鸡肋”,但是既然如此“鸡肋”,是不是python多线程就真的一无是处呢?答案当然是否定的。python多线程经常应用于IO频繁的程序,例如爬虫程序,我们都知道爬虫程序经常会在请求网站后自身阻塞等待回送请求,这就是一个很好的进行线程调度的时机。

    2 python多线程实战

    Python的标准库提供了两个模块:thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

    2.1 简单实例

    import time, threading
    
    def loop() -> None:
        print('thread ', threading.current_thread().name, ' is running...')
        n = 0
        while n < 5:
            n = n + 1
            print('thread ', threading.current_thread().name, ': n: ', n)
            time.sleep(1)
        print('thread', threading.current_thread().name, ' ended.')
    
    print('thread ', threading.current_thread().name, ' is running...')
    
    t = threading.Thread(target=loop, name='LoopThread')
    t.start()
    # join()函数是让其他方法阻塞而等待调用该方法的线程运行结束
    # 结束可以是正常或非正常终止,或者是通过传入timeout参数设定其他线程阻塞的时间
    t.join()
    print('thread', threading.current_thread().name, ' ended.')
    

    以上是实现python多线程的一个简单样例,其中程序使用主线程运行程序,直到我们利用threading模块创建了一个新的线程t,创建线程调用的函数传入的参数中,target参数就是我们要让这个线程执行的函数,name指定的就是这个线程的名称,创建完成后,我们要使用start()函数启动它,这时候如果没有其他操作的话,该线程将与主线程一起运行,共同请求GIL,而我们在名为LoopThread的线程启动后,紧接着调用了join()函数,这个函数的作用在于将使其他此刻存在的线程等待这个线程运行结束后再继续执行。所以我们的输出结果如下所示:

    thread MainThread is running...
    thread LoopThread is running...
    thread LoopThread : n: 1
    thread LoopThread : n: 2
    thread LoopThread : n: 3
    thread LoopThread : n: 4
    thread LoopThread : n: 5
    thread LoopThread ended.
    thread MainThread ended.

    2.2 python多线程之自旋锁、可重入锁

    首先恭喜你已经掌握了基本的python多线程开发,但是你还不能高兴的太早,因为还有许许多多的问题等待我们去解决。对操作系统稍有了解的同学们应该都明白,并发程序中最重要的就是资源共享问题,就比如我们两个线程在共享同一个变量的时候,如何做到不发生错误。
    首先来看一段代码:

    import threading
    
    balance = 0
    
    # 操作银行账户中的余额
    def op_cash(n):
        global balance
        # 存钱
        balance = balance + n
        # 取钱
        balance = balance - n
    
    def run_thread(n):
        for i in range(10000000):
            op_cash(n)
    
    
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(balance)
    

    这段程序模拟了两个人对同一个银行账户进行存取款的操作,第一个人t1每次存五块钱并且取五块钱,而第二个人每次存八块钱取八块钱。那么你认为结果应该是多少呢?理想情况下,我们觉得结果应当为0。但是我们来看一下真正的输出结果:

    40

    是不是感到很惊讶呢?这个让人惊讶的结果就是由于我们在多线程对一个共享变量进行操作时线程不安全导致的。比如现在余额是0,两个人同时对余额进行操作,第一个人存五块钱并且先完成了操作,现在余额是五块钱,但是第二个人存八块钱,这样就直接把第一个人存的五块钱覆盖掉,现在的余额就是八块钱。

    所以这就是我们这一节要急待解决的问题。其实看到这里有很多同学其实已经有了解决问题的答案,那就是——加锁。完全正确,那我们就马上来探索一下python多线程中的锁吧。
    同样我们先写一段简单的代码进行讲解:

    import threading
    
    balance = 0
    lock = threading.Lock()
    
    # 操作银行账户中的余额
    def op_cash(n):
        global balance
        # 存钱
        balance = balance + n
        # 取钱
        balance = balance - n
    
    def run_thread(n):
        for i in range(10000000):
            with lock:
                op_cash(n)
    
    
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    
    print(balance)
    

    我们首先利用threading模块获得了一个锁lock,随后在执行操作账户余额的函数时加了锁,这样就保证了多个线程同一时刻只能有一个线程进行账户余额的操作,这就保证了线程的安全,保证了共享变量不会出现我们意想不到的结果。是不是很简单呢~

    在你洋洋得意学会加锁的时候,我们再来考虑一段代码:

    import threading
    
    lock = threading.Lock()
    
    def unreentrancelock_caller():
        with lock:
            print('unreentrancelock_caller')
            print('Thread ', threading.current_thread().name, ' with lock')
            unreentrancelock_callee()
    
    def unreentrancelock_callee():
        with lock:
            print('unreentrancelock_callee')
            print('Thread ', threading.current_thread().name, ' with lock')
    
    t1 = threading.Thread(name='unreentrancelock_caller', target=unreentrancelock_caller)
    
    t1.start()
    t1.join()
    print('End.')
    

    其中我们执行unreentrancelock_caller()函数,在这个函数中,我们将继续调用unreentrancelock_callee()函数。随后我们执行这段程序,发现输出是这样的:

    unreentrancelock_caller
    Thread unreentrancelock_caller with lock

    但是注意观察,程序执行结束了吗?并没有。这是为什么呢?这是因为我们这里获得的lock是一个不可重入锁,也就是自旋锁,当我们的程序执行到unreentrancelock_callee()中请求lock的时候,我们发现我们其实已经在unreentrancelock_caller()中获取过一次了,所以现在lock在caller手里,callee自然获取不到,这就导致了,caller要想继续执行,就必须等待callee执行完毕,但是callee要想继续执行,就必须等待caller释放lock,这就造成了死锁,从而程序挂起。
    那么,我们是否有办法让这段程序继续执行下去呢?答案是使用可重入锁。

    import threading
    
    rlock = threading.RLock()
    
    def reentrancelock_caller():
        with rlock:
            print('reentrancelock_caller')
            print('Thread ', threading.current_thread().name, ' with rlock')
            reentrancelock_callee()
    
    def reentrancelock_callee():
        with rlock:
            print('reentrancelock_callee')
            print('Thread ', threading.current_thread().name, ' with rlock')
    
    t1 = threading.Thread(name='reentrancelock_caller', target=reentrancelock_caller)
    
    t1.start()
    t1.join()
    print('End.')
    

    同样,threading模块为我们封装了RLock,我们可以直接利用获得到的可重入锁进行使用,执行结果如下

    reentrancelock_caller
    Thread reentrancelock_caller with rlock
    reentrancelock_callee
    Thread reentrancelock_caller with rlock
    End.

    是不是感觉可重入锁的使用也很方便呢~

    2.3 python多线程之定时任务

    在threading模块中,博主自认为还有一个比较实用的功能拿来分享一下,那就是Timer执行定时任务,比如我们当前有一个任务我们不需要他马上执行,而是定时执行,我们就可以用到它了~

    import time
    from threading import Timer
    
    def timer_test():
        print("共经历时间 %.2f 秒" % (time.time() - begin))
    
    my_timer = Timer(5, timer_test)
    begin = time.time()
    
    my_timer.start()
    

    在新建我们的my_timer实例时,传入的第一个参数为需要定时的时间,而第二个参数是我们要执行的函数名。
    输出为

    共经历时间 5.00 秒

    2.4 python多线程总结

    通过本文的介绍,相信你已经对python多线程的知识有了一定的了解,正如我们所见,python多线程并不能像java多线程一样同时运行在CPU多个核心上,其运用的场合主要为IO密集型程序。
    那么也许你有这样的问题,那面对计算密集型的程序时我们该怎么办呢?我们是不是必须要使用java解决问题呢?答案是我们可以使用python多进程,对于这一部分内容,博主将在下一篇博文进行讨论。

    相关文章

      网友评论

        本文标题:一篇文章搞懂python多线程(理论+实践)

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