python-线程-进程-协程
- 一、多线程
- 1.创建一个简单的线程
- 2.自定义线程类
- 3.线程传参
- 4.线程阻塞,守护线程
- 5.GIL锁
- 6.线程池
- 7.线程队列
- 二、进程
- 1.创建一个简单的多进程
- 2.进程锁、开始进程、阻塞进程、守护进程
- 3.进程池
- 4.进程队列
- 5.进程启动模式
- 6.进程总结
- 三、协程
- 1.创建一个简单的协程
- 四、总结:
进程和线程:
线程,是计算机中可以被cpu调度的最小单元(真正在工作)。 进程,是计算机资源分配的最小单元(进程为线程提供资源)。一个进程中可以有多个线程,同一个进程中的线程可以共享此进程中的资源。
一、多线程
1.创建一个简单的线程
以下代码中,创建了两个线程,name参数可以设置线程名(要在线程执行前设置),target参数传入函数名(必传),t1.start()是启动线程的方法
import threading
def func1():
time.sleep(1)
print(f'hello func1 !')
def func2():
time.sleep(1)
print('hello func2 !')
t1 = threading.Thread(target=func1, name="线程1")
t2 = threading.Thread(target=func2, name="线程2")
print(t1.name)
print(t1.name)
t1.start()
t1.start()
2.自定义线程类
自定义线程类,直接将线程需要做的事写到run方法中。
import threading
class MyThread(threading.Thread):
def __init__(self, name):
self.name = name
def run(self):
print('执行此线程', n)
t = MyThread(args=(100,))
t.start()
3.线程传参
线程传参有两种方法,一种是元组(元组中如果只有一个参数是后面要加逗号不然会报错),另一种是字典方法。
import threading
def func1(m, n):
print(f'm = {m}')
print(f'n = {n}')
def func2(m, n):
print(f'm = {m}')
print(f'n = {n}')
t1 = threading.Thread(target=func1, args=(1, 2))
t2 = threading.Thread(target=func2, kwargs={'m': 1, 'n': 2})
t1.start()
t2.start()
4.线程阻塞,守护线程
t1.join()为线程阻塞,作用是等t1线程执行完毕后主线程才会继续往下走
t1.setDaemon(True)-必须写在.start()前,设置守护线程,参数为True设置为守护线程,主线程执行完毕后子线程会自动结束
#参数为False设置为非守护线程,主线程等待子线程执行完毕后,主线程线程才会结束(不设置默认为False)
import threading
def func1():
time.sleep(1)
print(f'hello func1 !')
def func2():
time.sleep(1)
print(f'hello func2 !')
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.setDaemon(True) # 设置为守护线程,主线程执行完毕后子线程会自动结束
t1.start()
t2.start()
t1.join() # t1线程执行完毕才会继续往下走
t2.join() # t1线程执行完毕才会继续往下走
5.GIL锁
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁。
同步锁: 给一段代码加了同步锁之后,在这段代码执行时只能有一个线程执行。–不支持嵌套,在一个函数内只能锁一次–如果嵌套会造成死锁
递归锁:支持多次申请和释放–支持嵌套锁
死锁:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且 同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。
import threading
# lock = threading.Lock() # 同步锁
lock = threading.RLock() # 递归锁
def func1():
look.acquire() # 加锁 第一个抵达的线程并上锁其他线程再此等待
time.sleep(1)
print(f'hello func1 !')
look.release() # 解锁 解锁后其他线程可以进入并执行
def func2():
lock.acquire() # 加锁
time.sleep(1)
print(f'hello func2 !')
lock.release() # 解锁
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
另一种加锁方法with–with下的程序执行完毕后会自动释放所锁
def func1():
with lock:
time.sleep(1)
print(f'hello func1 !')
6.线程池
在线程池提交一个任务后,如果线程池种有空闲线程则分配一个线程去执行,执行完毕后再将任务交还给线程池,如果没有空闲线程则等待
pool.shutdown(True) # 等待线程池所有任务执行完毕后,主进程再继续执行–类似.join()
from concurrent.futures import ThreadPoolExecutor
import time
def func(n):
time.sleep(1)
print(f'hello func1 !--{n}')
pool = ThreadPoolExecutor(100) # 创建一个线程池
# 使用循环给线程池提交任务
for i in range(1, 1000):
pool.submit(func, i)
pool.shutdown(True) # 等待线程池所有任务执行完毕
7.线程队列
(1)创建一个简单的线程队列–先进先出
import threading
import queue
q = queue.Queue() # 创建一个队列(实例化)
def func1():
q.put(1) # 向队列存入数据
print('向队列存入数据--1')
def func2():
out = q.get() # 取出队列中的数据-没有则等待
print(f'已取出队列数据--{out}')
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
(2)创建进程池队列
from concurrent.futures import ThreadPoolExecutor
import time
import queue
q = queue.Queue()
def func1(n):
time.sleep(10)
q.put(n)
print(f'下载完成并存入数据--{n}')
def func2(m):
time.sleep(0.5)
print(f'上传队列数据--{m}')
pool_1 = ThreadPoolExecutor(100) # 创建第一个线程池
pool_2 = ThreadPoolExecutor(100) # 创建第二个线程池
for i in range(1000):
pool_1.submit(func1, i)
for i in range(1000):
out = q.get()
pool_2.submit(func2, out)
pool_1.shutdown(True) # 等待线程池所有任务执行完毕
pool_2.shutdown(True) # 等待线程池所有任务执行完毕
if __name__ == '__main__':
pass
总结:
Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full()如果队列满了,返回True,反之False,Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]])获取队列,timeout等待时间 Queue.get_nowait()
相当于Queue.get(False),非阻塞方法 Queue.put(item) 写入队列,timeout等待时间
Queue.task_done()在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号。每个get()调用得到一个任务,接下来task_done()调用告诉队列该任务已经处理完毕。
Queue.join() 实际上意味着等到队列为空,再执行别的操作
二、进程
在程序开发中 多线程 和 多进程 是可以结合使用,例如:创建2个进程(建议与CPU个数相同),每个进程中创建3个线程。
1.创建一个简单的多进程
进程传参和线程方法相同
需要注意的是Windows下进程启动模式默认使用的是fork模式所以执行进程必须需写在if name ==
‘main’:后面,关于进程模式后面有详细介绍
import multiprocessing
import time
def func1(n):
time.sleep(1)
print(f'hello func1 !--{n}')
def func2(n):
time.sleep(1)
print(f'hello func2 !--{n}')
p1 = multiprocessing.Process(target=func1, args=(1,))
p2 = multiprocessing.Process(target=func2, args=(2,))
if __name__ == '__main__':
p1.start()
p2.start()
2.进程锁、开始进程、阻塞进程、守护进程
(1)进程锁
import multiprocessing
import time
lock = multiprocessing.Lock()
def func1(n):
lock.acquire() # 上锁
time.sleep(1)
print(f'hello func1 !--{n}')
lock.release() # 解锁
# # 或使用with方法-
# def func1(n):
# with lock:
# time.sleep(1)
# print(f'hello func1 !--{n}')
p1 = multiprocessing.Process(target=func1, args=(1,))
if __name__ == '__main__':
p1.start()
p2.start()
(2)开始进程:
p1.start() # 当前进程准备就绪,等待被CPU调度(工作单元其实是进程中的线程)。
(3)阻塞进程:
p1.join() # 等待当前进程的任务执行完毕后再向下继续执行。
(4)守护进程:
p.daemon = 布尔值,守护进程(必须放在start之前)
p.daemon =True,设置为守护进程,主进程执行完毕后,子进程也自动关闭。
p.daemon =False,设置为非守护进程,主进程等待子进程,子进程执行完毕后,主进程才结束。
3.进程池
进程池方法同线程池方法类似,只不过进程池添加进程,要写在if name == ‘main’:后
import time
from concurrent.futures import ProcessPoolExecutor
def func(n):
time.sleep(1)
print(f'hello func !--{n}')
if __name__ == '__main__':
pool = ProcessPoolExecutor(10)
for i in range(200):
pool.submit(func, i)
pool.shutdown(True) # 等待进程池所有任务执行完毕后再继续向下执行
4.进程队列
pass
5.进程启动模式
- fork,【“拷贝”几乎所有资源】【支持文件对象/线程锁等传参】【unix】【任意位置开始】【快】
父进程使用 os.fork() 来启动一个 Python 解释器进程。在这种方式下,子进程会继承父进程的所有资源,因此子进程基本等效于父进程。这种方式只在 UNIX 平台上有效,UNIX 平台默认使用这种方式来启动进程。
官方文档:(https://docs.python.org/3/library/os.html#os.fork)- spawn,【run参数传必备资源】【不支持文件对象/线程锁等传参】【unix、win】【main代码块开始】【慢】
父进程会启动一个全新的 Python 解释器进程。在这种方式下,子进程只能继承那些处理 run() 方法所必需的资源。典型的,那些不必要的文件描述器和 handle 都不会被继承。使用这种方式来启动进程,其效率比使用 fork 或 forkserver 方式要低得多。
Windows 只支持 spawn 方式来启动进程,因此在 Windows 平台上默认使用这种方式来启动进程。
官方文档:https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.run)- forkserver,【run参数传必备资源】【不支持文件对象/线程锁等传参】【部分unix】【main代码块开始】
如果使用这种方式来启动进程,程序将会启动一个服务器进程。在以后的时间内,当程序再次请求启动新进程时,父进程都会连接到该服务器进程,请求由服务器进程来 fork 新进程。通过这种方式启动的进程不需要从父进程继承资源。这种方式只在 UNIX 平台上有效。
修改进程模式-写在创建进程前
multiprocessing.set_start_method("fork") # fork、spawn、forkserver
6.进程总结
python中的多线程无法利用CPU资源,在python中大部分情况使用多进程。python中提供了非常好的多进程包multiprocessing
multiprocessing模块用来开启子进程,并在子进程中执行功能(函数),该模块与多线程模块threading的编程接口类似
multiprocessing的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件
----进程方法
p.start(): 启动进程,并调用该子进程中的p.run()
p.run(): 进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法-自定义进程类中使用
p.terminate():
强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁p.is_alive(): 如果p仍然运行,返回True
p.join([timeout]): 主线程等待p终止(强调:
是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
----进程属性
p.daemon:
默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置p.name: 进程的名称
p.pid: 进程的pid
p.exitcode: 进程在运行时为None、如果为–N,表示被信号N结束
p.authkey:
进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功
三、协程
1.创建一个简单的协程
from gevent import monkey
monkey.patch_all()
import gevent
import time
def func(n):
time.sleep(1)
print(f'hello func1 !--{n}')
task_lists = []
for i in range(1, 10+1):
task = gevent.spawn(func, i)
task_lists.append(task)
gevent.joinall(task_lists)
四、总结:
计算密集型:如果程序中数据计算量比较大需要利用CPU多核心优势可以使用多进程(例如累加计算)。
IO密集型:如果程序中IO操作表较多建议使用多线程(例如文件读写,网络传输)