Python 并发1: 进程,线程
相信大家在操作系统就了解过 进程,线程,死锁,系统调度等等,对基本的概念还是有的。但是,在实践的过程上我在python上的实现却略有不足。所以重新入门Python并发。
线程 Thread
所谓线程,线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。进程由操作系统创建,在操作系统执行进程的时候,一般会给进程分配空间等资源。而线程既可以由进程创建,也可以由操作系统创建,通过使用父进程所分配到的资源进行命令的执行调度,这里不详细展开,会在Linux入门中更新。
- Python 中创建多线程的方法: threading. 通过 threading.Thread 创建事例对象,当start启动后即可执行线程。同时,线程的执行可能收到分时,处理器的调度策略等乱序执行。
>>> from threading import *
>>> import time
>>> def print_Thread(name = 'None'):
... print('I am ' + name)
... time.sleep(5)
... print('Yes, I am ' + name)
>>> for i in range(3):
... t = Thread(target = print_Thread, args = (str(i),)) # 这里的target是方法的调用
... t.start() # 生成进程,t是主线程,生成线程执行 print_Thread的进程为子线程。start之后生成子线程后主线程的逻辑结构就结束了,但是主线程会等待子线程结束后再结束.主线程如果出现问题,子线程也会死亡。
... print('zhu_end')
I am 0
zhu_end
I am 1
zhu_end
I am 2
zhu_end
>>>
>>> Yes, I am 0
Yes, I am 1
Yes, I am 2
-
Python同时提供一些方便我们进行线程管理的API:
threading.enumerate() 查看当前运行的线程数
>>> def print_t():
... import time
... time.sleep(5)
... print('This is a threading')
>>> if __name__ == '__main__':
... from threading import *
... for i in range(5):
... t = Thread(target = print_t)
... t.start() # 子线程在这里被创建
... print(enumerate())
...
[<_MainThread(MainThread, started 10864)>, <Thread(Thread-6, started 21948)>, <Thread(Thread-7, started 14448)>, <Thread(Thread-8, started 5916)>, <Thread(Thread-9, started 15372)>, <Thread(Thread-10, started 15664)>]
>>> This is a threading
This is a threading
This is a threading
This is a threading
This is a threading
- 线程代码的封装: 通过继承Thread类完成类线程的创建。这里来做一些接口的拓展:start() 每个thread 对象都只能被调用1次start() run() 如果创建Thread的子类,重写该方法。负责执行target参数传来的可执行对象。
from threading import *
import time
class Th_son(Thread):
def __init__(self,myname:str):
Thread.__init__(self) # 因为__init__会覆写父进程,也就是Thread的初始化。所以要在吃实话之中重新进行弗雷德初始化来达到调用的方法
self.myname = myname
def change(self,myname:str):
used_name = self.myname
self.myname = myname
print('Name from ' + used_name + ' change to ' + self.myname)
time.sleep(1)
def print_name(self):
print('My name is ' + self.myname)
time.sleep(1)
def run(self):
self.print_name()
# myname = input() # 在python中子进程里如果调用input会出错。
self.change('New Thread2')
if __name__ == '__main__':
New_Th = Th_son(myname = 'New Thread 1')
print(enumerate())
New_Th.start() # 因为在Python中复写了RUN, 不需要再进行复写(多态),调用Thread的方法Start()
print(enumerate())
[<_MainThread(MainThread, started 5172)>]
[<_MainThread(MainThread, started 5172)>, <Th_son(Thread-12, started 3876)>]
-
关于多线程之间的传参: 全局变量的共享
首先明确全局变量的变化: 如果创建的可变类型变量,全局变量不需要声明global,但是可变类型由于在函数中你能找到他的地址,所以可以不用加global。 多线程间可以共享全局变量,包括使用args传参的方法。(因为太简单就不放代码了)
-
线程的互斥锁。众所周知,资源的进程,线程的切换都是不可抗力使得结果与我们有较大偏差,因此我们引申锁得概念。这个操作系统都讲过,不再细说。 避免死锁的办法可以通过添加超时时间等方法解决
# python锁接口(互斥锁):
# 注意,因为mutex一般是对全局的阻塞, 所以mutex需要设置为全局变量OK?
mutex = threading.Lock() # 锁创建
mutex.acquire() # 进行锁定
mutex.release() # 锁释放
Python慢的深度原因: GIL
Python 慢的原因: A. 动态类型语言, 边解释边执行
B. GIL 无法利用多核CPU并发执行
什么是GIL: 全局解释器锁(Global Interpreter Lock , GIL) 是计算机程序设计语言解释器(CPython解释器),用于同步线程的一种机制。它使得任何时刻仅有一个线程再运行。即使在多核处理器上,使用GIL解释器也只允许同一时间执行一个线程 。依赖于IO的释放。所以IO密集型对多线程可以最大发挥优势,所以在爬虫中经常使用分布式爬虫。
解决GIL带来的限制,Python multiprocessing多进程机制实现真正的并行计算,利用多核CPU优势。
进程 Process
操作系统调度的基本单位 。是包括,资源,PCB,代码上下文,可执行的代码等一系列的集合。Tips: 进程与线程相比有自己分配的资源,同时也意味着进程切换开销要大很多。同时进行子进程的创建时,需要拷贝主进程的资源拷贝,时空间消耗很大。进程之间有相互通讯的模块(队列),但整体来说数据的协同没有同一进程内的线程方便。在python中,多进程可以理解为浪费了空间等资源,但达到了并发,减少了时间
- 进程的创建:multiprocessing
(注,在windows系统上不允许子进程使用print 打印信息,所以在这里可以用命令行,或者改用Linux系统跑代码才会有print输出)
from multiprocessing import Process
import os
from time import sleep
# 子进程要执行的代码
def run_proc(name, age, **kwargs):
for i in range(10):
#在Win下的python环境中,子进程是无法进行Print打印的,所以我们采取的方法最好就是用Linux的虚拟机啦,从这个代码之后所有的代码是虚拟机版本
print('子进程运行中,name= %s,age=%d ,pid=%d...' % (name, age,os.getpid()))
print(kwargs)
sleep(0.5)
if __name__=='__main__':
print('父进程 %d.' % os.getpid())
p = Process(target=run_proc, args=('test',18), kwargs={"m":20})
print('子进程将要执行')
p.start()
sleep(1)
p.terminate()
p.join()
print('子进程已结束')
C:\Users\40629>python C:\Users\40629\Desktop\Untitled-1.py
父进程 19680.
子进程将要执行
子进程运行中,name= test,age=18 ,pid=18484...
{'m': 20}
子进程运行中,name= test,age=18 ,pid=18484...
{'m': 20}
子进程已结束
- 子进程继承父进程的资源,同时继承父进程的环境变量
>>> from multiprocessing import *
>>> import os
>>> def show():
... try:
... print(os.environ['NewProcessComming'])
... except:
... print('Not success')
...
>>> if __name__ == '__main__':
... os.environ['NewProcessComming'] = '/bin/bash'
... p1 = Process(target = show)
... p1.start()
...
>>> /bin/bash
# 重新进入PYTHON, 新开一个进程
>>> import os
>>> os.environ['NewProcessComming']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.8/os.py", line 675, in __getitem__
raise KeyError(key) from None
KeyError: 'NewProcessComming'
# 环境变量资源没有保存哦,说明上面子进程确实是继承了父进程的资源,也会导致伊西俄安全问题
- 进程间的通信,通过操作系统来实现(SOCKET),一般是采用队列的数据结构存储数据与进行数据的交换: Queue
# 基础API:
q= mulyiprocessing.Queue(space) # 队列对象的实例化.Tips:q在多线程通讯之间需要以传参的方式传递才能发挥作用
q.put() # 存数据,在python中数据类型任意。如果Q队列满了,就会阻塞等待
q.get() # 取数据,如果取数据的时候队列为空,就会阻塞等待
q.full() # 判断是否满
q.empty() # 是否空
实例Show: (扩展,在不同主机间网络通讯实现消息队列,Redis, Kafka)
>>> from multiprocessing import *
>>> import time
>>> def read(queue):
... while(True):
... if queue.empty():
... time.sleep(1)
... m = queue.get()
... print('Done Reading ' + str(m))
>>> def write(queue):
... write_num = 0
... while(True):
... if queue.full():
... time.sleep(1)
... queue.put(write_num)
... print('Done Writing ' + str(write_num))
... write_num += 1
>>> if __name__ == '__main__':
... queue = Queue(3)
... p1 = Process(target = read, args = (queue,))
... p2 = Process(target = write, args = (queue,))
... p1.start()
... p2.start()
Done Writing 0
Done Writing 1
Done Writing 2
Done Reading 0
Done Reading 1
.......
- 进程池:当创建的进程目标成千上万,创建进程工作量巨大,同时创建,回收的代价太高,因此考虑multiprocessing.Pool,也就是进程池
初识化进程池时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没满就会创建一个新的进程执行改请求。但是如果进程池被取空了,请求就会被阻塞知道有进程空出来。
from multiprocessing import Pool
import os
from time import sleep
def test():
print("I'm Process " + str(os.getpid()))
if __name__ == '__main__':
process_pool = Pool(3)
for i in range(5):
process_pool.apply(test)
# 先关闭进程池在等待所有进程结束
process_pool.close()
# 阻塞主进程
process_pool.join()
print('___end____')
I'm Process 15824
I'm Process 15824
I'm Process 8876
I'm Process 12408
I'm Process 15824
___end____
可以发现进程号相同,换句话说进程池减少了进程创建和销毁的过程.