Python多线程入门
操作系统的设计
可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
线程和进程
进程就是处于运行中的程序,并且具有一定独立的功能。比如说,我们在电脑上打开一个软件,就是开启了一个进程,更具体的来说,Windows 系统你可以通过资源管理器进行查看当前电脑启动的进程数。所以也可以说进程是操作系统进行资源分配和调度的一个独立单位。
线程是进程的组成部分,一个进程可以包含多个线程,多个线程可以共用这个进程的资源,相比于进程,线程更加轻量级。
线程的几种状态
线程状态一共有五种,包括如下:
- 新建
- 就绪
- 运行
- 阻塞
- 死亡
它们之间的关系如下图所示:
实现方式
接下来,我们就来看看如何在 Python 里面实现多线程。总的来说,如果你了解过其他语言实现多线程的方式,比如说 Java的话,那对于理解 Python 实现多线程是非常有帮助的。Python 实现多线程有两种方式:
使用 threading 模块的 Thread 类的构造器创建线程
继承 threading 模块的 Thread 类创造线程类
我们先用第一种方式来编写一个多线程程序,即使用 threading 模块的 Thread 类的构造器创建线程。
1 | #!/usr/bin/python |
看起来是不是很简单,很我们平常写的 Python 程序并没有特别大的不同,但是还是有很一些情况是需要注意的,其中最重要的就是 threading.Thread()
,我在这里重点介绍下。
首先它是一个类,我们可以通过 type(threading.Thread)
来进行查看,它的构造函数如下所示:
__init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
group 应该为None,这个我们不用管,它是为了日后扩展 ThreadGroup 类实现而保留的一个参数。
target 是我们需要重视的一个参数, 我们想让哪个函数并发执行,这个函数就是 target 的参数值,注意只写函数名,不需要写 ()。
name 是线程名称,默认情况下,由”Thread-N”的格式构成一个唯一的名称,其中 N 是小的十进制数。
args 是用于调用目标函数的参数元祖, 注意是元祖, 如果你只想传一个参数的话,也应该这样写 (args1,), 而不是 (args)。
kwargs 是用于调用目标函数的关键字参数字典。默认是 {}。
daemon 用于设置该线程是否为守护模式,如果是 None, 线程默认将继承当前线程的守护模式属性。
一般来说,我们需要注意的就是 target
参数、args
参数,其他的参数用到的时候可以再查。
另一点需要我们需要注意的一点就是启动线程的方法是 start
方法,可能你也知道线程也有 run
方法,这一块也会在第二种方式中进行介绍,但是启动线程的方法是 start
方法,要不然就变成了单线程程序。
接下来我们来看下如何使用第二种方式实现多线程,即继承 threading 模块的 Thread 类创造线程类。
1 | #! /usr/bin/python |
第二种方式就是继承 Threading.Thread
类。然后重载 run()
方法。
其实我看来的话,感觉第二种方式更适合在项目中使用,因为它更加模块化,比较清晰。
另外还有一个方法需要注意的就是 join()
方法,它的作用就是协调主线程和子线程的,调用 join()
后,当前线程就会阻塞,或者来说,暂停运行,执行子线程,等子线程执行完成后,主线程再接着运行。
生产者、消费者模型
提到多线程,最著名的就是生产者、消费者模型了,那应该如何实现呢?
说实话,我当初最开始学习生产者、消费者模型的时候,心里是有点犯嘀咕的,感觉涉及到线程间的通信,太好解决。但是查阅了一些资料后,发现还是可以理解的。
生产者、消费者二者不属于竞争关系,更多的是一种捕食关系,生产者生产资源,消费者进行消费,就像圣湖中的牛吃草一样。
不知道这时候你有没有想到一种数据结构,那就是队列,队列呢是一种操作受限的线性表,它只允许在队尾入队,在队头出队,也就是先进先出 (FIFO) 策略。
生产者、消费者模型,不就是生产者生产元素,放到队尾,然后消费者从队头消费元素嘛。
只不过有时候会出现特殊的情况
- 队列空了,消费者还要消费数据
- 队列满了,生产者还要生产数据
这是我们需要重点考虑了,解决了以上两点,这个模型也就实现了。
接下来我们就来看看 Python 如何实现吧!
1 | #!/usr/bin/python |
看了上面的代码,不知道你有没有一种错觉,你不是说要考虑上面的两种情况,但是你并没有考虑啊。
确实,我没有考虑,那是因为 Queue
在设计实现的时候已经替我们考虑好了,我们直接使用就好了。
具体就是 task_done()
函数,它在队列为空时会自动阻塞当前线程
而队列在满的时候再添加元素也会阻塞当前线程,这就实现了上面我们提到的那两种情况。
接下来呢,我再给你讲解一个例子,带你看看如何使用锁。
银行取钱问题
从银行取钱的基本流程大致可以分为以下几个步骤:
- 用户输入账户、密码,系统判断当前的账户、密码是否匹配。
- 用户输入取款金额
- 系统判断账户余额是否大于取款金额
- 如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。
乍一看,这就是日常生活中的取款操作啊,但是把它放到多线程并发的情况下,就可能会出现问题。不信的话,你可以试着写下多线程的程序,然后再看下我的程序。
1 | #!/usr/bin/python |
如果你想尝试下不加锁的情况下是否会出现问题,你可以把我的程序进行修改,把加锁的那部分去掉,然后尝试运行下。
这里呢,不是说每次运行都会出现问题,可能你运行了十次也都没有出现问题,但是呢,这个安全隐患是确确实实存在的,不容忽视。
原文作者: 贺同学
原文链接: http://clarkhedi.github.io/2020/11/25/python-duo-xian-cheng-ru-men/
版权声明: 转载请注明出处(必须保留原文作者署名原文链接)