Python从入门到精通(二十)Python并发编程的基本概念-线程的使用以及生命周期

Posted 码农飞哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python从入门到精通(二十)Python并发编程的基本概念-线程的使用以及生命周期相关的知识,希望对你有一定的参考价值。

您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦
本篇开始学习并发编程,本文将重点介绍线程的基本概念以及线程的生命周期。
干货满满,建议收藏,需要用到时常看看。 小伙伴们如有问题及需要,欢迎踊跃留言哦~ ~ ~。

前言

前面介绍的知识点都是基础知识点,没有涉及到并发编程。在实际开始中我们经常需要用到异步并发处理的场景。比如:读取一个很大的Excel,单个线程读取的话可能很慢,这时候就需要多线程并发处理了。
还有就是支付系统,订单支付成功之后需要异步通知第三方等等场景。

进程和线程

进程是什么?进程说白了就是应用程序的执行实例。你在电脑上听着歌儿,敲着代码,挂着微信。这些任务都是由不同的应用程序来执行。操作系统会给每个应用程序分配不同的进程。通过CPU的调度来使这些动作可以“同时”进行,这里说是同时进行实际上不是的,因为CPU在同一时间内只能有一条指令执行,但是因为CPU执行的速度太快了,给用户的感觉就是在同时进行。进程是可以占用物理内存的。

线程是进程的组成部分,一个进程可以拥有多个线程.CPU调度进程的最小粒度是线程。其中由主线程来完成从开始到结束的全部操作。其他线程在主线程运行时被创建或者结束。
主线程在程序初始化之后就会被创建。如果一个程序只有一个主线程就称为单线程,如果有多个线程则称之为多线程。 创建多个线程之后,每个线程的执行都是独立的。

线程的创建

Python创建线程的方式有两种: 创建线程都需要引入threading模块。首先让我们来看看Thread类的构造方法。

__init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):

此构造方法中,以上所有参数都是可选参数,即可以使用,也可以忽略。
其中各个参数的含义如下:

  1. group:指定所创建的线程隶属于哪个线程组。
  2. target:指定所创建的线程要调用的目标方法。
  3. args:以元组的方式,为target指定的方法传递参数,如果传入的是元组中有多个参数的话则传入方式是(arg1,arg2,....argn,)
  4. kwargs:以字典的方法,为target指定的方法传递参数。
  5. daemon:指定所创建的线程是否为后台线程。
  6. name: 指定线程的名称
  7. 第一种方式:就是直接调用Thread类的构造方法创建一个线程的实例。
import threading

# 定义线程要调用的方法
def async_fun(*add):
    for arc in add:
        print(threading.current_thread().getName() + " " + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

# 创建线程
thread = threading.Thread(target=async_fun, args=my_tuple)
# 启动线程
thread.start()

for i in range(4):
    print(threading.current_thread().getName() + "执行" + str(i)+ "次")

运行结果是:

Thread-1 码农飞哥MainThread执行0次
MainThread执行1次

MainThread执行2次
MainThread执行3次
Thread-1 好好学习
Thread-1 早日突破职业瓶颈

如上方法就是实例化一个线程Thread-1让他异步调用async_fun方法。可以看出主线程MainThread和线程Thread-1是交替调用的(每次的执行结果都不同)。这说明了这两个线程是交替获得CPU的执行权限的。需要特别注意的是线程必须要调用start()方法才能执行。
2. 第二种方式:就是继承threading.Thread。然后,重写run方法。

import threading

class MyThread(threading.Thread):
    def __init__(self, add):
        threading.Thread.__init__(self)
        self.add = add
        # 重写run()方法

    def run(self):
        for arc in self.add:
            print(threading.current_thread().getName() + " " + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

thread = MyThread(my_tuple)
thread.start()

for i in range(4):
    print(threading.current_thread().getName() + "执行" + str(i)+ "次")

运行结果是:

Thread-1 码农飞哥MainThread执行0次
MainThread执行1次

Thread-1 好好学习
Thread-1 早日突破职业瓶颈
MainThread执行2次
MainThread执行3次

这里定义了了MyThread类,并重写了run方法。run方法就是线程真正要执行的任务。相当于上例中async_fun函数的内容移到了run方法中。

线程的生命周期

说完了如何创建一个线程接下来让我们来看看线程的生命周期。一个线程一共有五个状态。
分别是新建状态(初始化状态),就绪状态,运行状态,阻塞状态以及死亡状态。状态之间的关系图如下:

  1. 新建状态:线程被刚刚创建,且未调用start()方法时的状态。即上面的threading.Thread(target=async_fun, args=my_tuple) 时的线程状态。
  2. 就绪状态:调用了start()方法之后,线程就由新建状态转成就绪状态,就绪状态就是表示线程可以是随时准备获取CPU的状态。
    以下几种情况下线程会进入就绪状态:1.sleep()方法规定的时间已过。2.调用了set()方法发出通知,3.其他线程释放了该同步锁,并由该线程获得。
  3. 运行状态:当就绪状态的线程获得了CPU的使用权之后,并开始执行target参数执行的目标函数或者run()方法,就表明线程处于运行状态。
  4. 阻塞状态:获得CPU的调度但是没有执行完任务的线程.
    以下几种情况下:线程都会进入阻塞状态:1.调用了sleep()方法。2.调用了wait()方法,并且等待条件满足。3.线程试图获取某个对象的同步锁时,如果该锁被其他线程占用,则当前线程进入阻塞状态。
  5. 线程死亡状态:线程的任务执行完成或者程序执行过程中发生异常,线程都会进入死亡状态。

Thread.join()用法详解。

join()方法的功能是在程序指定位置,优先让该方法的调用者使用CPU资源,即调用线程等待该线程完成后,才能继续用下运行。
该方法的语法格式如下:thread.join( [timeout])
其中,thread为Thread类或其子类的实例化对象;timeout参数作为可选参数,
其功能是指定thread线程最多可以霸占CPU资源的时间,如果省略,则默认直到thread执行结束才释放CPU资源。

import threading


# 定义线程要调用的方法
def async_fun(*add):
    for arc in add:
        print(threading.current_thread().getName() + " " + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

# 创建线程
thread = threading.Thread(target=async_fun, args=my_tuple, name="线程1")
thread2 = threading.Thread(target=async_fun, args=my_tuple, name='线程2')
# 启动线程
thread.start()
# 等待线程1执行完
thread.join()
# 线程1执行完之后启动线程2
thread2.start()
thread2.join()

运行结果是:

线程1 码农飞哥
线程1 好好学习
线程1 早日突破职业瓶颈
线程2 码农飞哥
线程2 好好学习
线程2 早日突破职业瓶颈

如上有线程1和线程2两个线程。在线程1调用thread.join()之后线程1和线程2的执行由并行改成了串行。也就是说必须是线程1执行完之后才启动线程2。现在把该语句去掉变成下面这样:

# 启动线程
thread.start()
# 线程1执行完之后启动线程2
thread2.start()
thread2.join()

运行结果是:

线程1 码农飞哥
线程2 码农飞哥
线程2 好好学习
线程2 早日突破职业瓶颈
线程1 好好学习
线程1 早日突破职业瓶颈

可以看出线程1和线程2的运行是并行的。

sleep函数的用法

位于time模块中的sleep(secs)函数,可以实现令当前执行的线程暂停secs秒后在继续执行,所谓暂停,即令当前线程进入阻塞状态,当达到sleep()函数规定的时间后,
再由阻塞状态转为就绪状态,等待CPU调度。

# 定义线程要调用的方法
def async_fun(*add):
    for arc in add:
        start_time = time.time()
        time.sleep(2)
        print(str((time.time() - start_time)) + " 秒 " + threading.current_thread().getName() + " 结束调用" + arc)


my_tuple = ("码农飞哥", "好好学习", "早日突破职业瓶颈")

# 创建线程
thread = threading.Thread(target=async_fun, args=my_tuple)
# 启动线程
thread.start()

运行结果是:

2.0052337646484375 秒 Thread-1 结束调用码农飞哥
2.004210948944092 秒 Thread-1 结束调用好好学习
2.002394199371338 秒 Thread-1 结束调用早日突破职业瓶颈

可以看出线程每次执行都花费了2秒的时间。

daemon守护线程详解

当一个线程设置为守护线程之后,本次任务执行完成之后线程并不会死亡,其生命周期就是主程序的生命周期。也就是说程序不结束,守护线程就一直在(执行任务时发生异常的情况除外)

总结

本文是Python并发编程的第一篇。简单的介绍了线程的基本概念以及生命周期。

Python知识图谱

为了更好帮助更多的小伙伴对Python从入门到精通,我从CSDN官方那边搞来了一套 《Python全栈知识图谱》,尺寸 870mm x 560mm,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,有兴趣的小伙伴可以了解一下------扫描下图中的二维码即可购买。


我本人也已经用上了,感觉非常好用。图谱桌上放,知识心中留。

我是码农飞哥,再次感谢您读完本文

以上是关于Python从入门到精通(二十)Python并发编程的基本概念-线程的使用以及生命周期的主要内容,如果未能解决你的问题,请参考以下文章

Python从入门到精通(二十)Python并发编程的基本概念-线程的使用以及生命周期

❤️【Python从入门到精通】(二十七)更进一步的了解Pillow吧!

Python从入门到精通(二十四)Python定时执行任务的姿势

Python从入门到精通五万六千字对Python基础知识做一个了结吧!(二十八)值得收藏

Python从入门到精通五万六千字对Python基础知识做一个了结吧!(二十八)值得收藏

ROS从入门到精通系列(二十六) 标准化ROS代码风格 - . Python 风格指南