Python多线程


Python多线程

  • 模块:Threading

  • 概念:
    • 线程:CPU执行程序的基本单位
    • 父线程:与子线程而言是相对的。调用别的线程的程序(线程叫做父线程)
    • 子线程:被别的程序所调用,则是调用者的子线程
    • 守护进程:也称之为后台进程,即主进程(前台进程)结束,守护进程也结束
    • 主线程:最外层的线程(最初运行的程序)
  • 注意:使用Pycharm时,运行多线程程序需要使用Terminal或者Run控制台,而不是Python Console否则得到的将不是原本的多线程结果。(比如守护线程会失效)。参考博客

一下用简单的几个例子来讲解Python多线程的用法。

第一个例子:展示多线程与普通程序的区别

"""
    最简单的多线程demo
    展示多线程程序与普通程序的区别
"""

import time
from threading import Thread


def demo1(doing):
    print(f"I want to {doing}, {time.ctime()}")
    time.sleep(2)
    print(f"\n{doing} 结束")

def main():
    print(f"开始, {time.ctime()}")
    t1 = Thread(target=demo1, args=("听歌",))
    t2 = Thread(target=demo1, args=("吃饭",))
    t1.start()
    t2.start()

    print(f"结束, {time.ctime()}")


if __name__ == '__main__':
    main()
    
"""
开始, Thu May  6 20:22:43 2021
I want to 听歌, Thu May  6 20:22:43 2021
I want to 吃饭, Thu May  6 20:22:43 2021
结束, Thu May  6 20:22:43 2021
听歌 结束, Thu May  6 20:22:45 2021
吃饭 结束, Thu May  6 20:22:45 2021
"""

输出格式经过了略微的调整,原始输出肯能比较乱,原因之后会讲。

由上可以看出,原本需要4秒才能执行完的程序,上面只用了2秒。即实现了并行,同时"听歌"和"吃饭"。

Python实现多线程使用的是threading库,使用Thread类创建线程对象,Thread对象有如下比较常用的方法:

  • Thread(target=None, name=None, args=(), daemon=None)
    以上为比较常用的参数,不是所有参数,详细参数可参考文末的链接官网教程
    target 是指明该线程运行的函数名
    name 是我们给该线程取的名称
    args 是调用该函数需要传递的参数元组,末尾加逗号
    daemon 设置该线程是否为守护线程

  • start()
    表示启动线程,让其开始工作。该方法每个对象只能够调用一次,否则抛出RuntimeError,它安排对象的 run()方法在一个独立的控制进程中调用。

  • run()
    运行target传递过去的函数

  • join(timeout=None)
    这个会阻塞调用该线程的线程,如果子线程调用该方法,主线程就算将自己的程序运行完了也不会结束,而是 会等到这个子线程运行完再结束。

  • setName() 和 getName() 分别是设置名字和获取名字

  • isDaemon() 和 setDaemon() 分别是判断是否是守护进程和设置守护进程

第二个例子:使用join()方法和将线程模块化

import threading
import time


class MyThreading(threading.Thread):
    def __init__(self, name):
        super(MyThreading, self).__init__()

        self.name = name

    def run(self):
        print(f"开始线程: {self.name}, {time.ctime()}")
        time.sleep(2)
        print(f"退出线程: {self.name}, {time.ctime()}")


class Test:
    def __init__(self):
        pass

    def main_threading(self):
        print(f"主线程开始运行, {time.ctime()}")
        Thread1 = MyThreading("Thread1")
        Thread2 = MyThreading("Thread2")

        Thread1.start()
        Thread2.start()

        Thread1.join()
        Thread2.join()

        print(f"主线程结束运行, {time.ctime()}")


if __name__ == '__main__':
    test = Test()
    test.main_threading()
    
"""
主线程开始运行, Thu May  6 20:46:30 2021
开始线程: Thread1, Thu May  6 20:46:30 2021
开始线程: Thread2, Thu May  6 20:46:30 2021
退出线程: Thread2, Thu May  6 20:46:32 2021
退出线程: Thread1, Thu May  6 20:46:32 2021
主线程结束运行, Thu May  6 20:46:32 2021
"""

由第一个例子可以知道主线程本应该在子线程结束前结束,但是用了子线程调用join()后会阻塞父线程,致使在子线程结束完后才可结束。

第三个例子:守护线程的使用

以上例子中的子线程结束时间都是根据自身程序的运行时间觉得,而与父线程的是否结束无关,但我们在日常使用时,经常会有父线程结束,子线程就必须结束的一些线程,这种线程我们称之为守护线程,或者后台进程,如我们的垃圾回收功能就是。

我们修改第二个例子中的Test()类

class Test:
    def __init__(self):
        pass

    def main_threading(self):
        print(f"主线程开始运行, {time.ctime()}")
        Thread1 = MyThreading("Thread1")
        Thread2 = MyThreading("Thread2")
        Thread1.setDaemon(True)
        Thread2.setDaemon(True)
        Thread1.start()
        Thread2.start()

        print(f"主线程结束运行, {time.ctime()}")

"""
    主线程开始运行, Thu May  6 20:48:56 2021
    开始线程: Thread1, Thu May  6 20:48:56 2021
    开始线程: Thread2, Thu May  6 20:48:56 2021
    主线程结束运行, Thu May  6 20:48:56 2021
"""

由运行结果可以看出,父线程结束,子线程就无条件中止了。

第四个例子:

现在的CPU都是多核的,但是python的多线程是无法使用多核,而实现并行的方式是线程切换,只要速度足够快,在某一段时间内,我们就会认为是并行的。这就是所谓的并发,而某一时刻多个线程同时工作则称之为并行。我们可以用程序输出来演示线程切换过程。

import time
from threading import Thread, current_thread


def run(n):
    for i in range(n):
        print(current_thread().name + " " + str(i))


thread = Thread(target=run, args=(20,), name="ChildThread")
# 如果设置子线程为守护进程,可以查看守护进程的结束与主线程的结束时间
# thread.setDaemon(True)
thread.start()

for i in range(10):
    print(current_thread().name + " " + str(i))

"""
ChildThread 0
ChildThread 1
ChildThread 2
ChildThread 3
ChildThread 4
MainThread 0
MainThread 1
MainThread 2
MainThread 3
MainThread 4
MainThread 5
MainThread 6
MainThread 7
ChildThread 5
ChildThread 6
ChildThread 7
ChildThread 8
ChildThread 9
ChildThread 10
ChildThread 11
ChildThread 12
ChildThread 13
MainThread 8
MainThread 9
ChildThread 14
ChildThread 15
ChildThread 16
ChildThread 17
ChildThread 18
ChildThread 19
Process finished with exit code 0
"""

从运行结果我们就可以看出线程的切换。

第五个例子:

由于线程是切换的,那么所以某一个线程在执行时可能被打断。加上python多线程使用的是同一块内存,那么资源是共享的,所以当多个线程再某一段时间内,同时访问某一资源,对其进行修改的话可能会导致混乱。

from threading import Thread

var = 0


def change(n):
    global var
    var += n
    var -= n


def run_thread(n):
    for i in range(1000000):
        change(n)


thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(var)
"""
11
"""
# 结果不一定

"""
    理论上应该是0,但是当两个线程交替执行,当循环次数足够多时,结果就不一定是0
    为了解决这个问题,采用了锁的办法
"""

第六个例子:就是解决上述问题的办法,使用锁

from threading import Lock, Thread

var = 0
lock = Lock()

def change(n):
    global var
    var += n
    var -= n


def run_thread(n):
    for i in range(20000000):
        lock.acquire()
        try:
            change(n)
        finally:
            lock.release()


thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(var)

"""
这样得到的结果就一定是0
"""

可以这么理解这个锁:人相当于线程,锁相当于门锁,一个人进厕所会锁上门让别人不能进来打扰,只有当他解锁出来后别的人才可以进去。

Threading.Lock类的主要方法如下:

  • acquire(blocking=True, timeout=-1)
    • 这个方法用于获取锁。获取的方式可以是阻塞方式,也可以是非阻塞方式
    • 差别就是,如果别的线程使用了锁,而当前线程想获取锁时,可以干等着一直等到使用了锁的线程释放了锁(解锁),再获取锁并锁定。这样称之为阻塞方式
    • 而在非阻塞的情况下(也就是blocking设置为False时),调用acquire方法,若是锁可获得,则返回True,并且上锁,若是锁被别的线程占用,则返回False。
    • timeout 就是设置阻塞等待的时间,若是在timeout设置的时间内没有获得锁则返回False,否则返回True并获取锁,再上锁。若是timeout=-1,那么将会无限等待。
  • release()
    • 这个方法就是释放锁
    • 若是在未锁定时调用,会返回RuntimeError

Python线程使用场景

由于Python的多线程受到了GIL的原因,Python的多线程无法实现真正的并行,所以多线程程序是通过频繁的线程切换来实现的。

但是线程切换会带来不小的开销,频繁的线程切换会造成资源的浪费。所以多线程在Python里似乎意味着并没有带来性能的提升,甚至降低了程序的性能。

但是有没有一种操作是正好需要频繁切换线程才能提高性能的呢?

答案是有的,那就是I/O操作,因为I/O操作不需要一直依靠CPU,只需要CPU向磁盘发起通知,然后就可以去执行其他的命令,等到数据准备好了再读取数据就好了,就不需要CPU一直等待I/O操作直至完成。

所以Python多线程的使用场景就很明确了:

  • 不适用于CPU计算密集型,而是适用于I/O密集型

以上就是我对python多线程的一些领悟,如有错误请积极指出,谢谢。


参考链接:廖雪峰的博客阮一峰的博客虫师的博客官网教程多线程使用场景


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM