Python基础 - 多线程(上)
前面对 进程 一点认识, 通俗理解, 进程是操作系统(OS)进行资源调度分配的基本单元. 每个程序的至少就一个进程在OS中被"监控"着的哦. 然后围绕着多进程, 用消息队列共享全局变量, 守护主进程, 进程池...这一通探讨, 当然还是偏向应用的一方, 我自己偶尔工作有多任务的处理的地方, 也是优先弄多进程 (主要是公司电脑贼强, 我就要弄多进程, 就要浪费资源哈哈..).
进程 呢, 基本没用过, (爬虫除外, 之前有用 scrapy 是多线程的), 自己来手写应该是没有的. 为啥宁愿优先考虑进程呢, 原因是我们大多用的 Python 的解释是 CPython. 它默认下, 其实是有设阻塞的, 在多线程一块, 其实 CPython 没有真正地能实现 多线程.. 先不说这...
也是从通俗的角度来理解的话, 一个进程(程序), 至少有一个进程. 进程和线程, 可以看作一个 1 : n 的关系.
OS 的调度, 有多个进程, 每个进程, 有多个线程 . 这样理解这种 "数量" 关系应该还可以.
单线程
从代码的视角, 即一段代码按顺序执行, 就是个一个单进程.
import time
# 我每天下班之后首先是: 煮饭, 做菜
def cook_rice():
for i in range(3):
print(f"cook rice ...{i}")
time.sleep(1)
def cook_dishes():
for i in range(4):
print(f"cook dishes...{i}")
time.sleep(0.5)
if __name__ == '__main__':
cook_rice()
cook_dishes()
cook rice ...0
cook rice ...1
cook rice ...2
cook dishes...0
cook dishes...1
cook dishes...2
cook dishes...3
这结果跟咱想的没毛病. 先做饭, 然后再做菜... but, 真实中是这样的嘛? 真实是 我先插上饭, 然后打开电脑, 放点音乐啥的, 就着手去洗菜, 切菜这些事情了.
可见真是情况是, 对于做饭这个程序, 我其实是可以同时做的. 这也是前面提到的多任务呀, 从生活来看, 这是非常自然的事情.而我一直观点是, 写代码的本质, 是对现实世界的抽象和模拟. 因此, 洗菜做饭这种每天都要面对的基本事情, 同样在编程从, 自然是归类为 Python 基础了.
多线程
也是用Python内置的一 threading 模块 的 Thread 类, 来演示一波哦, 做饭和做菜是可以一起弄的.
import time
import threading
# 我每天下班之后首先是: 煮饭, 做菜
def cook_rice():
for i in range(3):
print(f"cook rice ...{i}")
time.sleep(1)
def cook_dishes():
for i in range(4):
print(f"cook dishes...{i}")
time.sleep(0.5)
if __name__ == '__main__':
# 假设就各创一个线程来弄
t1 = threading.Thread(target=cook_rice)
t2 = threading.Thread(target=cook_dishes)
# 启动多线程
t1.start()
t2.start()
cook rice ...0
cook dishes...0
cook dishes...1
cook rice ...1
cook dishes...2
cook dishes...3
cook rice ...2
可以看到进程是不断在 "切换", 用进程的术语说, 多个进程的在进行 "任务调度" , 这里线程, 就, 嗯, 多个线程一起运行吧. (其实并未真正多个线程同时运行, 只是调度太快而已 cpu).
主线程 - 等待所有子线程
跟多进程是一样的, 主线程, 会默认等待所有的子进程, 都结束后才, 程序才会真正结束.
然后在创建线程, 传参也是一样的, 两种方式 * args 和 ** kwargs. 这些都没啥, 大致有印象就行, 理解这个过程和特性更为重要, 怎么写, 会百度就好.
import time
import threading
# 我每天下班之后首先是: 煮饭, 做菜
def cook_rice(n):
for i in range(n):
print(f"cook rice ...{i}")
time.sleep(1)
def cook_dishes(n):
for i in range(n):
print(f"cook dishes...{i}")
time.sleep(0.5)
if __name__ == '__main__':
# 假设就各创一个线程来弄
t1 = threading.Thread(target=cook_rice, args=(3,))
t2 = threading.Thread(target=cook_dishes, kwargs={"n":4})
# 启动多线程
t1.start()
t2.start()
print("main threading done ....")
cook rice ...0
cook dishes...0
main threading done ....
cook dishes...1
cook rice ...1
cook dishes...2
cook dishes...3
cook rice ...2
守护主线程
也是跟进程那块一样的, 从代码视角看, 即设置一个属性, daemon = True 即可.
import time
import threading
def cook_rice(n):
for i in range(n):
print(f"cook rice ...{i}")
time.sleep(1)
if __name__ == '__main__':
# 将 daemon 参数 传为 True 即设置为了守护主线程.
t1 = threading.Thread(target=cook_rice, args=(4,), daemon=True)
# 启动多线程
t1.start()
time.sleep(2)
print("main threading done ....")
cook rice ...0
cook rice ...1
cook rice ...2
main threading done ....
我在测试的时候, 这部分发现线程和进程有一个不一样的点, 在多个线程中, 如果只将其中一个线程给 daemon 后, 当主进程结束, 其余的线程还是会继续执行; 而在多个进程中, 如果将其中一个进程给 daemon 后, 整个任务也就跟着停掉了. 很奇怪, 尚未弄明白这一点,暂时, 我后续再测测看.
阻塞-等待子进程结束
其实这有些莫名其妙, 直接运行不设置 daemon 不就行了吗, 主要是啥, 我单纯想熟悉下 join() 这个 API 而已啦.
import time
import threading
# 我每天下班之后首先是: 煮饭, 做菜
def cook_rice(n):
for i in range(n):
print(f"cook rice ...{i}")
time.sleep(1)
if __name__ == '__main__':
# 假设就各创一个线程来弄
t1 = threading.Thread(target=cook_rice, args=(4,), daemon=True)
# 启动多线程
t1.start()
time.sleep(2)
# 阻塞等待, 子进程结束
t1.join()
print("main threading done ....")
cook rice ...0
cook rice ...1
cook rice ...2
cook rice ...3
main threading done ....
自定义线程
没啥应用, 也是纯练习下编程技巧而已. 工作也偶尔会遇到这样的情况, 就某个库的某个功能, 可能不能满足我的业务需求, 但我又不能全部写一遍, 或者去 改掉它的源代码, 改源代码这事情我干过, 那时候还是小白没经验, 最后导致, 只有我一个人能用, 很尴尬去改源代码....
正确的做法是: 创建一个自定义类, 去继承要修改的类, 然后 重写其某个方法 . 这是正确的. 这里呢要自定义, 其实就继承上面调用的这 threading.Tread 类, 然后 start() 的部分, 点进去看一下源码就知道了, 其实去 start() 会调用 它上面的 run( ) 方法, 因此, 重写 run( ) 方法即可.
import time
import threading
class MyTread(threading.Thread):
def __init__(self, *args):
super().__init__()
self.args = args
@staticmethod
def cook_rice(n):
for i in range(n):
print(f"cook rice ...{i}")
def cook_dishes(self):
for i in range(self.args[0]):
print(f"cook dishes with {i}")
def run(self):
self.cook_rice(self.args[0])
self.cook_dishes()
if __name__ == '__main__':
t1 = MyTread(3)
t1.start()
t1.join() # 等待子进程
print("done")
上篇就这吧, 也是对主线程的一个基本认识即可, 当然前提是对前面的 进程也有所了解, 这样对比起来学习是非常快的. 这些代码模板, 守护主线程, 阻塞, 继承重写.. 都跟进程是一样的哦,
而下篇呢, 就会聊点跟进程不一样的地方,如线程中 资源能共享, 已经资源竞争, 互斥锁这些概念,, 还是蛮有趣的哦.