python 多任务编程
-
单核CPU
其在实现多任务时候,实质上是CPU交替执行多个任务A、B、C等,由于CPU执行速度较快,表象为多个任务同时进行。
宏观上是同时执行,微观上仍是顺序执行
-
并发
并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
-
在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。
真正的并行执行多任务需要依靠多核CPU
多进程编程
- 进程
正在运行中的程序称之为进程
它是程序和运行环境的结合
- 进程的五状态模型
- 创建进程
- 进程就绪,等待CPU分配时间片段
- 进程执行,就绪状态被分配了时间片段开始执行
- 分配的时间片段走完之后,该进程回到就绪状态
- 当执行的任务需要其他I/O输入时需要进行等待,进入阻塞状态
- 当I/O输入完成后,阻塞状态进入就绪状态,等待CPU分配时间片段
- 最后进程进入终止状态,进程结束结束
多进程编程提高运行效率主要是利用任务在阻塞状态时空闲的CPU时间片段,将这个时间片段分配给其他进程。
- python中创建子进程
# 利用fork进程(分支函数,相当于创建一个进程的副本 )
# fork函数调用一次,有两个返回值,一个是子进程的pid,另外一个是0
# 为什么返回子进程pid?可以利用子进程os.getppid()获取父进程
import os
print('当前进程的pid为:%s' % os.getpid())
print('当前进程的父进程pid为:%s' % os.getppid())
print('使用fork函数创建进程')
process = os.fork()
if process == 0:
print('创建的子进程返回值为0,子进程pid为:%s,父进程的pid为:%s' % (os.getpid(), os.getppid()))
else:
# 父进程返回的是子进程的pid
print('返回的子进程pid为:%s' % process)
___________________________________
当前进程的pid为:18330
当前进程的父进程pid为:4791
使用fork函数创建进程
返回的子进程pid为:18335
创建的子进程返回值为0,子进程pid为:18335,父进程的pid为:18330
fork函数在windows下不支持
-
子进程和父进程之间的数据互相不影响
子进程的数据相当于对父进程数据的重新拷贝
-
Process类(通过实例化对象实现多进程)
# Process继承BaseProcess类,需要传入的参数如下
_________________________________________________
def __init__(self, group=None, target=None, name=None, args=(), kwargs={},
*, daemon=None):
# target:处理的对象
# name :进程的别名
# args 传入对象所需要传递的参数
# kwargs :传入所需的关键字参数
…………
# 未使用多进程
def ListenMusic():
time.sleep(1)
print('listening musics……')
def DoHomework():
time.sleep(1)
print('doing homework……')
def no_use_multiProcess():
for i in range(4):
ListenMusic()
DoHomework()
if __name__ == '__main__':
start_time = datetime.now()
no_use_multiProcess()
end_time = datetime.now()
print('所用时间为:%d秒' % (end_time - start_time).seconds)
____________________________________________________________
所用时间为:8秒
# 使用多进程
def use_multiProcess():
processPool = []
for i in range(4):
P1 = Process(target=ListenMusic)
P2 = Process(target=DoHomework)
P1.start()
P2.start()
processPool.append(P1)
processPool.append(P2)
# 需要使用join函数:等所有创建的子进程先执行完,再执行主进程
# 使用列表生成式
[process.join() for process in processPool]
_______________________________________________________________
所用时间为:1秒
- note
1.实例化Process类就相当于创建了子进程
2.新创建的进程不会自动启动,需要调用start函数,进程处于就绪状态
start和run函数的区别:
start是让进程处于就绪状态,但是并没有运行。一旦得到CPU的时间片段,进程就可以运行,执行run方法。
3.加入进程池中各个进程竞争CPU的时间片段
4.当进程处于休眠状态时把时间片段分配给其他的进程使用
5.join函数:阻塞住主进程再等待子进程结束,然后再往下执行
- join源码
def join(self, timeout=None):
'''
Wait until child process terminates
'''
self._check_closed()
assert self._parent_pid == os.getpid(), 'can only join a child process'
assert self._popen is not None, 'can only join a started process'
res = self._popen.wait(timeout)
if res is not None:
_children.discard(self)
# 里面调用了wait()方法,阻塞主进程
-
进程池
当具有成百上千以及更多的任务时候,创建这么多的进程是不合适的,因为创建子进程会重新复制父进程的信息,并且会占用很多的内存空间,所以当任务数量大时,我们采用进程池来处理
-
资源进程
预先创建好的空闲进程,管理进程会把工作分发到空闲进程来处理
-
管理进程
管理进程负责创建资源进程,把工作交给空闲资源进程处理,回收已经处理完工作的资源进程
-
def use_pool():
# 使用线程池处理
from multiprocessing import Pool, cpu_count
# 创建线程池,设置线程池中资源线程的个数
pool = Pool(processes=cpu_count())
pool.map(task, list(range(1, 100000)))
# 这里close表示关闭进程池的使用,不再接受新的进程
# 当进程池close的时候并未关闭进程池,只是会把状态改为不可再插入元素的状态,完全关闭进程池使用
pool.close()
# 阻塞主进程,等待进程池中子进程的完成
pool.join()
# close方法需要在join方法之前调用
- 利用创建Process子类实现多进程
import time
from multiprocessing import Process
class MyProcess(Process):
# 可以使用构造方法传递参数
def __init__(self, num):
super(MyProcess, self).__init__()
self.num = num
# 要重写run方法
def run(self):
print('这是子任务%d' % self.num)
if __name__ == '__main__':
# 创建多个子进程
for i in range(1, 10):
process = MyProcess(i)
process.start()
- 进程通信的目的
- 数据传输
- 数据共享
- 资源共享
- 进程控制
- 进程间通讯的方式
- 管道
- 信号
- 消息队列
这三种方式用于同一主机通信
- 套接字
不同主机之间实现进程通信用套接字体
- 消息队列
from multiprocessing.context import Process
# 使用消息队列实现生产者消费者模型
from multiprocessing import Queue
class Producer(Process):
def __init__(self, queue):
super(Producer, self).__init__()
self.queue = queue
def run(self):
for i in range(1, 11):
print('生产了产品%d' % i)
# put方法当队列满的时候会一直等待
self.queue.put(i)
class Consumer(Process):
def __init__(self, queue):
super(Consumer, self).__init__()
self.queue = queue
def run(self):
while True:
# get出队操作当队列为空时也会一直等待,即阻塞当前进程
product = self.queue.get()
print('消费了产品%d' % product)
if __name__ == '__main__':
queue = Queue()
p = Producer(queue)
c = Consumer(queue)
p.start()
c.start()
多线程编程
-
什么是线程?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以有很多线程,每条线程并行执行不同的任务。
-
进程和线程的区别
- 进程是资源分配的最小单位,一个新的进程会分配一块新的内存空间,而线程是程序执行的最小单位。
- 进程之间的内存空间是互相独立的,而线程之间共享内存空间。
- 进程间通信需要依靠管道、消息队列、套接字等通信方式,而线程之间因为共享内存空间,所以通信更加方便,但需要处理同步和互斥的问题。
- 一个进程中必然有一个主线程
import threading
if __name__ == '__main__':
print('当前存在的线程个数:%d' % threading.activeCount())
print('当前线程的信息:%s' % threading.currentThread())
________________________________________________________
当前存在的线程个数:1
当前线程的信息:<_MainThread(MainThread, started 139683240683328)>
import threading
import time
from datetime import datetime
def task():
print('正在执行任务……')
time.sleep(1)
if __name__ == '__main__':任务
start_time = datetime.now()
threadPool = []
for i in range(10):
# 使用多线程
thread = threading.Thread(target=task)
# 开启线程
thread.start()
threadPool.append(thread)
[thread.join() for thread in threadPool]
end_time = datetime.now()
print('耗费时间为:%d' % (end_time - start_time).seconds)
一个py文件执行就相当于一个程序,创建多个线程来执行这个任务。
- IP地址归属地查询
# 使用实例化对象实现多线程
import json
from datetime import datetime
from json import JSONDecodeError
import requests
import threading
from fake_useragent import UserAgent
def get_addr(ip):
url = 'http://ip-api.com/json/%s' % ip
# 该网址返回的是json字符串
ua = UserAgent()
headers = {'User-Agent': ua.random}
response = requests.get(url, headers=headers)
try:
info = response.json()
print(info)
print('%s的所属城市为:%s-%s' % (ip, info['country'], info['city']))
except JSONDecodeError as e:
print('未找到%s的信息' % ip)
if __name__ == '__main__':
start_time = datetime.now()
base_ip = '1.1.1.'
threadPool = []
for i in range(1, 115):
ip = base_ip + str(i)
# 使用多线程
thread = threading.Thread(target=get_addr, args=(ip,))
# 开启线程
thread.start()
threadPool.append(thread)
[thread.join() for thread in threadPool]
end_time = datetime.now()
print('耗费时间为:%s' % (end_time - start_time).seconds)
- 用继承类方式实现多线程
# 网段IP地址存活探测
import os
from threading import Thread
from colorama import Fore
class taskThread(Thread):
def __init__(self, ip):
super(taskThread, self).__init__()
self.ip = ip
def run(self):
cmd = 'ping -c1 -w1 %s &> /dev/null' % (self.ip)
result = os.system(cmd)
if not result:
print(Fore.GREEN + 'ip:%s存活' % self.ip)
else:
print(Fore.RED + 'ip:%s不存在' % self.ip)
if __name__ == '__main__':
base_ip = '192.168.122.'
for i in range(0, 255):
ip = base_ip + str(i)
thread = taskThread(ip)
thread.start()
- GIL(全局解释器锁)
GIL本质就是一把互斥锁,是夹在解释器身上的,
同一个进程内的所有线程都需要先抢到GIL锁,才能执行解释器代码
优点:
保证Cpython解释器内存管理的线程安全
缺点:
同一进程内所有的线程同一时刻只能有一个执行,也就说Cpython解释器的多线程无法实现并行
-
多线程处理同一全局变量会发生混乱
虽然CPython中使用了全局解释器锁,进程中同一时刻只能执行一个线程,但是各个线程之间的切换会引发错误,线程执行到一半切换到另外线程就可能导致全局变量的混乱
from threading import Thread
money = 0
def add():
for i in range(100000):
# 声明money为全局变量
global money
# 这步操作可以分为两部分:1为加法运算 2为赋值运算 需要统一执行
money += 1
def reduce():
for i in range(100000):
global money
money -= 1
if __name__ == '__main__':
# 使用多线程
task1 = Thread(target=add)
task2 = Thread(target=reduce)
task1.start()
task2.start()
task1.join()
task2.join()
print(money)
_______________________________
-33576
- 使用线程锁
from threading import Thread, Lock
money = 0
def add():
for i in range(1000000):
# 声明money为全局变量
global money
# 加锁
lock.acquire()
money += 1
# 解锁
lock.release()
…………
if __name__ == '__main__':
# 使用多线程
# 实例化线程锁
lock = Lock()
…………
print(money)
- 死锁问题
两个线程分别占有一部分资源,但是都需要对方的资源才能继续,就会造成死锁问题。
尽量避免:1.尽量使用同一把锁
2.对锁的优先级进行排序
简单理解:两个线程各自拥有不同的锁,一个线程上锁之后执行任务,遇到sleep()函数,途中进入阻塞状态,然后另一个线程开始执行,上锁之后也遭遇阻塞,之后两个线程都无法正常执行,锁不同。
协程
一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
import time
import gevent
from gevent import monkey
monkey.patch_all()
def task(num):
print('正在执行任务%d' % num)
time.sleep(1)
if __name__ == '__main__':
# 一个进程下多个协程
# 列表生成式
gevents = [gevent.spawn(task, num) for num in range(1, 11)]
gevent.joinall(gevents)
print('任务执行结束')