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
"""

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


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


参考链接:廖雪峰的博客阮一峰的博客虫师的博客官网教程