这篇文章主要来记录一下自己对于多线程和多进程以及协程的理解,结合简单的代码示例帮助更好地理解其使用方法。
整理本篇文章时,借鉴了前辈的博客,同时基于自己的理解也增加了一些内容和代码示例(个人比较喜欢看代码示例),借鉴的博客地址如下: what is fantasy 的原文博客链接
1. 多任务的概念理解
要更好地理解多线程和多进程和协程的概念,首先理解一下多任务的概念和实现方式。
多任务其实就是:在同一时刻做很多事情。比如windows上打开网页的同时,又打开QQ,又打开微信,又在听音乐,这时就有多个任务同时运行,这就是多任务。
但是cpu又是如何同时执行这么多任务的呢?
其实cpu采用了并发的方式。操作系统使用了时间片轮转的算法让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样,这种执行方式叫做并发。
实际程序开发中会大量使用多任务,如微信在发信息的同时需要接收信息,网易云音乐在下载的同时又需要播放歌曲。多线程和多进程都是实现多任务的一种方式,python语言中实现多任务的方式有三种:线程,进程,协程
2. 线程、进程、协程的概念理解
2.1 关系图
2.1.1 进程与进程之间关系图

2.1.2 进程与线程之间关系
每一个进程启动时都会最先产生一个线程,即主线程,然后主线程会再创建其他的子线程并根据设置管理子线程(开始和结束)。

2.2 线程
2.2.1 什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
一个线程是一个execution context(执行上下文),即一个cpu执行时所需要的一串指令。
2.2.2 线程的工作方式
假设你正在读一本书,没有读完,你想休息一下,但是你想在回来时恢复到当时读的具体进度。有一个方法就是记下页数、行数与字数这三个数值,这些数值就是execution context(执行上下文)。如果你的室友在你休息的时候,使用相同的方法读这本书。你和她只需要这三个数字记下来就可以在交替的时间共同阅读这本书了。
线程的工作方式与此类似。CPU会给你一个在同一时间能够做多个运算的幻觉,实际上它在每个运算上只花了极少的时间,本质上CPU同一时刻只干了一件事。它能这样做就是因为它有每个运算的execution context。就像你能够和你朋友共享同一本书一样,多任务也能共享同一块CPU。
2.3 进程
2.3.1 什么是进程(进程本质上是资源的集合)
一个程序的执行实例就是一个进程。
在linux中,每个进程都是由父进程提供的,每启动一个子进程就从父进程克隆一份数据,但是进程之间的数据本身是不能共享的。一个进程有一个虚拟的地址空间、可执行的代码、操作系统的接口、安全的上下文(记录启动该进程的用户和权限等等)、唯一的进程ID、环境变量、优先级类、最小和最大的工作空间(内存空间),还要有至少一个线程。
与进程相关的资源包括:
- 内存页(同一个进程中的所有线程共享同一个内存空间)
- 文件描述符(e.g. open sockets)
- 安全凭证(e.g.启动该进程的用户ID)
2.3.2 进程的工作方式
待补充。。。。。。
2.4 协程
2.4.1 什么是协程
线程和进程的操作是由程序触发系统接口,最后的执行者是系统,它本质上是操作系统提供的功能。而协程的操作则是程序员指定的,在python中通过yield,人为的实现并发处理。
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时。协程,则只使用一个线程,分解一个线程成为多个“微线程”,在一个线程中规定某个代码块的执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO)。
常用第三方模块gevent和greenlet。(本质上,gevent是对greenlet的高级封装,因此一般用它就行,这是一个相当高效的模块。)
2.4.2 协程的简单实用示例
待完善
3. 线程和进程的联系与区别
- 同一个进程中的线程共享同一内存空间,但是进程之间是独立的。
- 同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的。
- 对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。
- 线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
- 同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现。
- 创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
- 一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
- 线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。
4. 线程的使用示例(threading 类)
4.1 threading 类使用说明
-
传入参数说明
Class threading.Thread(target=None, name=None, args=(), kwargs={})target 线程调用的对象,就是目标函数
name 给线程起一个名字(线程的名字)
args 给目标函数传递实参,元组
kwargs 给目标函数传递关键字参数,字典 -
属性和方法说明
start() 开始子线程,等待CPU调度
setName() 为线程设置名称
getName() 获取线程名称
setDaemon(True) 设置为守护线程,子线程声明周期与主线程同步,必须在 start() 方法之前设置
join() 设置逐个执行每个线程,执行完毕后继续往下执行threading.current_thread() 返回当前线程对象
threading.current_thread().ident 返回当前线程的id
threading.main_thread() 返回主线程(main线程)对象
threading.active_count() 返回当前处于active状态的线程个数。(活着的,还未运行结束的线程个数)
threading.enumerate() 返回所有活着的线程列表,不包括已经终止的线程和未开始的线程
threading.getident() 返回当前线程的ID,非0整数
4.2 线程锁的概念理解
想要更好地理解线程锁,先理解 GIL (Global Interpreter Lock 全局解释器锁) 的工作原理。
- GIL 的理解
在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。
GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。 - 线程锁(互斥锁 mutex)的理解
由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。
所以,出现了线程锁,即同一时刻允许一个线程执行操作。
互斥锁(mutex) 同时只允许一个线程更改数据,这就是线程锁具体的实现方法。
4.3 代码示例
示例1:单纯地使用多线程,不涉及操作共享的数据(所以没有使用线程锁)
import time
import threading
# 定义全局变量VALUE
VALUE = 0
# 模拟下载任务
def download(tuple_n):
print(threading.current_thread()) # 打印当前线程的id
print("start downloading ...... ")
for i in range(tuple_n):
print("downloading: %d" %i)
time.sleep(0.5)
print("download finished!")
# 模拟放音乐任务
def play_music(**kwargs):
print(threading.current_thread()) # 打印当前线程的id
print("start playing ...... ")
for i in range(kwargs["num"]):
print("pla

本文深入探讨了Python中的多任务实现,包括线程、进程和协程的概念及其工作方式。通过实例解析了线程的threading库和进程的multiprocessing库的使用,强调了线程锁与进程间通信的重要性。文章还提到了GIL对Python多线程的影响,并介绍了协程作为轻量级线程在处理IO密集型任务时的优势。
1076

被折叠的 条评论
为什么被折叠?



