文章目录
一. 多任务编程
-
定义: 操作系统可以同时运行多个任务。 比如 : 可以同时上网,同时听歌,同时用电脑赶作业。 这就是多任务 。
-
单核CPU 实现多任务
操作系统轮流让各个任务交替执行,每个任务执行0.01 秒,这样反复执行。 表面上看是每个任务交替执行,但CPU 的执行速度非常快,因此感觉所有任务都是同时执行的。
-
多核CPU 实现多任务
并行:任务交替进行
并发:任务同时进行
因为任务数量远远多于CPU 的核心数,所以操作系统会自动把多任务轮流调度到每个核心上执行。
二. 多进程编程
(一) 基本概念
-
进程与程序
程序: 编写完毕的代码,在没有运行的时候,称作是程序
进程: 正在运行的代码
ps : 进程除了代码之外,还需要运行环境等。 -
进程的五种状态
进程的五个状态: 创建, 准备,运行,等待,结束
(二)进程的创建
创建子进程:
python 中的os 模块封装了常见的系统调用,其中有fork ,可以在python程序中创建出子进程。
常用的函数:
os. fork()
os.getpid() ----- 获得当前进程的pid ( proces id)
os.getppid() -------- 获得当前进程的父进程 (parent proces id)
eg:
import os
import time
# 定义一个全局变量money
money = 100
print("当前进程的pid:", os.getpid())
print("当前进程的父进程pid:", os.getppid())
# time.sleep(115)
p = os.fork()
# 子进程返回的是0
if p == 0:
money = 200
print("子进程返回的信息, money=%d" %(money))
# 父进程返回的是子进程的pid
else:
print("创建子进程%s, 父进程是%d" %(p, os.getppid()))
print(money)
实验结果为:
注意:
1) os.fork() 操作系统会创建一个新的进程复制父进程的所有信息到子进程中
2) 普通的函数调用,调用一次,返回一次。 fork()函数调用,调用一次,返回两次
3) 父进程和子进程都会从fork() 函数中得到一个返回值,子进程返回是0 , 而父进程返回的是子进程的id 号
多进程修改全局变量: 多进程中,每个进程中的所有数据(包括全局变量) 都各拥有一份,互不影响
(三)多进程编程
1. 多进程模块 : multiprocessing
因为windows 没有fork 调用,且Python 是跨平台的,因此大多用multiprocessing模块
multiprocessing 模块提供了一个Process 来代表一个进程对象。
2. 表示方法:
Process([group [, target [, name [, args [, kwargs]]]]])
target:表示这个进程实例所调⽤对象;
args:表示调⽤对象的位置参数元组;
kwargs:表示调⽤对象的关键字参数字典;
name:为当前进程实例的别名;
group:⼤多数情况下⽤不到;
3. Process 类的常用方法:
(1)is_alive(): 判断进程实例是否还在执⾏;
(2)join([timeout]): 是否等待进程实例执⾏结束,或等待多少秒;
(3)start(): 启动进程实例(创建⼦进程);
(4)run(): 如果没有给定target参数,对这个对象调⽤start()⽅法时,
就将执 ⾏对象中的run()⽅法;
(5)terminate(): 不管任务是否完成,⽴即终⽌;
4. Process 类常用属性
name : 当前进程实例别名 默认是proces-N , N 是从1开始计数
pid: 当前进程实例的PID 值
5. 多进程编程方法1:实例化对象
eg:
from multiprocessing import Process
import time
def task1():
print("正在听音乐")
time.sleep(1)
def task2():
print("正在编程")
time.sleep(2)
def no_multi():
for i in range(3):
task1()
for i in range(5):
task2()
def use_multi():
process = []
for i in range(3):
p = Process(target= task1)
p.start()
process.append(p)
for i in range(5):
p = Process(target= task2)
p.start()
process.append(p)
[process.join() for process in process]
if __name__ == '__main__':
start_time = time.time()
# no_multi()
use_multi()
end_time = time.time()
print(end_time - start_time)
运行结果为:
没有用多进程:
使用多进程:
结论: 在相同的任务下,使用多进程要比没有使用多进程花费的时间少得多。
6. 多进程编程方法2 : 创建子类
eg:
from multiprocessing import Process
import time
class MyProcess(Process):
def __init__(self, music_name):
super(MyProcess, self).__init__()
self.music_name = music_name
def run(self):
"""重写run方法, 内容是你要执行的任务"""
print("听音乐%s" % (self.music_name))
time.sleep(1)
# 开启进程: p.start() ====== p.run()
if __name__ == '__main__':
for i in range(10):
p = MyProcess("音乐%d" % (i))
p.start()
运行结果为:
(四) 进程池
-
进程池的作用
当操作对象的数目不大的时候可以直接利用Multiprocessing 的Process 动态生成多个进程。但是要是目标太大,手动去限制操作太繁琐因此使用进程池。 -
进程池的使用方法
Pool 可以提供指定个数的进程供用户调用,当有新的请求到Pool 中时,如果池还没有满,那么就会创造一个新的进程用来执行该请求,但如果池中的进程数量已经达到了最大规模,那么就会请求等待,直到 进程池中有进程结束, 才能创建新的进程来执行。
eg:
def is_prime(num):
"""判断素数"""
if num == 1:
return False
for i in range(2, num):
if num % i == 0:
return False
else:
return True
def task(num):
if is_prime(num):
print("%d是素数" % (num))
from multiprocessing import Process
def use_mutli():
ps = []
# 不要开启太多进程, 创建子进程会耗费时间和空间(内存);
for num in range(1, 10000):
# 实例化子进程对象
p = Process(target=task, args=(num,))
# 开启子进程
p.start()
# 存储所有的子进程对象
ps.append(p)
# 阻塞子进程, 等待所有的子进程执行结束, 再执行主进程;
[p.join() for p in ps]
def no_mutli():
for num in range(1, 100000):
task(num)
def use_pool():
"""使用进程池"""
from multiprocessing import Pool
from multiprocessing import cpu_count # 4个
p = Pool(cpu_count())
p.map(task, list(range(1, 100000)))
p.close() # 关闭进程池
p.join() # 阻塞, 等待所有的子进程执行结束, 再执行主进程;
if __name__ == '__main__':
import time
start_time = time.time()
use_pool()
end_time = time.time()
print(end_time - start_time)
运行结果为:
用多进程的编程方式:
用进程池的方式:
结论: 有上面两个实验截图我们可以明显的看到当我们需要编程的对象很多的时候,采用进程池的方法可以大大节省时间。因为多进程的方式每一个进程在创建的时候也是需要时间的 ,所以采用进程池的方式更好。
(五)进程间的通信
-
进程间通信的方式:管道,信号,消息队列,信号量,套接字
-
消息队列
可以使用multiprocessing 模块的Queue 实现多进程之间的数据传递,Queue本身是一个消息队列程序
1) Queue. qsize() 返回当前队列包含的消息数量
2)Queue. empty() 如果队列为空,返回True , 反之,返回False
3)Queue.full() 如果队列满了,返回True , 反之,返回False4) Queue.get([block[, timeout]]):获取队列中的⼀条消息,然后将其从列队中移除,block默认值为True;
5) Queue.get_nowait();相当Queue.get(False);
6) Queue.put(item,[block[, timeout]]): 将item消息写⼊队列,block默认值 为True;
7) Queue.put_nowait(item):相当Queue.put(item, False)
eg:
import multiprocessing
import time
class Producer(multiprocessing.Process):
def __init__(self, queue):
super(Producer, self).__init__()
self.queue = queue
def run(self):
for i in range(10):
self.queue.put(i)
time.sleep(0.1)
print("传递消息。内容为:%s" % (i))
class Consumer(multiprocessing.Process):
def __init__(self, queue):
super(Consumer, self).__init__()
self.queue = queue
def run(self):
while True:
time.sleep(0.1)
recvData = self.queue.get()
print("接收到另一进程的传递数据%s" % (recvData))
if __name__ == '__main__':
q = multiprocessing.Queue()
p1 = Producer(q)
p2 = Consumer(q)
p1.start()
p2.start()
p1.join()
p2.join()
运行结果为:
三. 多线程编程
1. 定义
线程是操作系统中能够进行运算调度最小单位。它被包含在进程中,是进程中实际运作单位。
每个进程中最少含有一个线程,即进程本身。进程可以启动多个线程,操作系统可以并行处理这些线程。
**一个进程中一定有一个线程,称作是主线程**
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190722141931287.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0ZhbmN5eWo=,size_16,color_FFFFFF,t_70)
2. 进程与线程
1)进程是资源分配的最小单位。线程是程序执行的最小单位。
2) 进程有自己独立的地址空间。线程是共享进程中的数据,使用相同的地址
3) 进程之间的通信需要以通信的方式(IPC)进行。 线程之间的通信更方便,同一进程 下的线程共享全局变量,静态变量等数据。 难点: 处理好同步和互斥
3. 线程的分类
1) 内核线程
2) 用户空间或者用户线程
内核线程是操作系统的一部分,而内核中没有实现用户空间线程
4. 线程的编译
python中的tthread模块是比较底层的模块,python中的 threading 模块是对thread 模块做了一些包装,可以更加方便去使用
python2 导入的是: thread
python3 导入的是: _thread
import threading
if __name__ == '__main__':
# 一个进程里面一定有一个线程, 叫主线程.
print("当前线程个数:", threading.active_count())
print("当前线程信息:", threading.current_thread())
5. 多线程的实现
采用实例化对象的方法来实现多线程
import time
import threading
def task():
"""当前要执行的任务"""
print("听音乐........")
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
threads = []
for count in range(5):
t = threading.Thread(target=task)
#让线程开始执行任务
t.start()
threads.append(t)
#等待所有的子线程执行结束, 再执行主线程;
[thread.join() for thread in threads]
end_time = time.time()
print(end_time-start_time)
运行结果为:
多线程编程:
1) 多线程程序的执行顺序是不确定的
2) 当执行到sleep语句的时候,线程将被阻塞(Blocked),到sleep结束后,线程进入到就绪状态,等待调用。而线程的调用将自行选择一个线程执行。
3) 代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序,run函数中每次循环的执行顺序都不能确定。
6. 线程的状态
应用案例: IP地址归属地的批量查询任务,并且存储到 数据库中。
思路:1) 在网站中搜索IP , 提取出其中的国家和城市两个信息。
2) 采用sqlalchemy 创建数据库,并且将获取到的信息按顺序编号保存在数据库当中
3) 使用线多程的方法执行任务。
实验代码如下:
import requests
import json
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from threading import Thread
def task(ip):
"""获取指定IP的所在城市和国家并存储到数据库中"""
# 获取网址的返回内容
url = 'http://ip-api.com/json/%s' % (ip)
try:
response = requests.get(url)
except Exception as e:
print("网页获取错误:", e)
else:
# 默认返回的是字符串
"""
{"as":"AS174 Cogent Communications","city":"Beijing","country":"China","countryCode":"CN","isp":"China Unicom Shandong Province network","lat":39.9042,"lon":116.407,"org":"NanJing XinFeng Information Technologies, Inc.","query":"114.114.114.114","region":"BJ","regionName":"Beijing","status":"success","timezone":"Asia/Shanghai","zip":""}
"""
contentPage = response.text
# 将页面的json字符串转换成便于处理的字典;
data_dict = json.loads(contentPage)
# 获取对应的城市和国家
city = data_dict.get('city', 'null') # None
country = data_dict.get('country', 'null')
print(ip, city, country)
# 存储到数据库表中ips
ipObj = IP(ip=ip, city=city, country=country)
session.add(ipObj)
session.commit()
if __name__ == '__main__':
engine = create_engine("mysql+pymysql://root:westos@172.25.254.123/pymysql",
encoding='utf8',
# echo=True
)
# 创建缓存对象
Session = sessionmaker(bind=engine)
session = Session()
# 声明基类
Base = declarative_base()
class IP(Base):
__tablename__ = 'ips'
id = Column(Integer, primary_key=True, autoincrement=True)
ip = Column(String(20), nullable=False)
city = Column(String(30))
country = Column(String(30))
def __repr__(self):
return self.ip
# 创建数据表
Base.metadata.create_all(engine)
# 1.1.1.1 -- 1.1.1.10
threads = []
for item in range(10):
ip = '1.1.1.' + str(item + 1) # 1.1.1.1 -1.1.1.10
# task(ip)
# 多线程执行任务
thread = Thread(target=task, args=(ip,))
# 启动线程并执行任务
thread.start()
# 存储创建的所有线程对象;
threads.append(thread)
[thread.join() for thread in threads]
print("任务执行结束.........")
print(session.query(IP).all())
6. 共享全局变量
1) 优缺点
优点: 在一个进程中的 所有线程共享全局变量,能够在不使用其他方式的前提下完成多线程之间的数据共享。
缺点: 线程是对全局变量随意篡改可能造成多线程之间对全局变量的混乱。(线程处在非安全的状态下)
2) 针对缺点的改进 -------- GIL
GIL (global interpreter lock) : python解释器中任意时刻都有一个线程在执行 。
对python虚拟机访问由全局变量解释器锁(GIL) 来控制。 这个锁能够保证同一时刻只有一个线程在运行。
eg: 死锁问题
在线程间共享多个资源的时候,如果两个线程分别占用了一部分资源并且同时等待对方的资源就会造成死锁的问题。
import time
import threading
class Account(object):
def __init__(self, id, money, lock):
self.id = id
self.money = money
self.lock = lock
def reduce(self, money):
self.money -= money
def add(self, money):
self.money += money
def transfer(_from, to, money):
if _from.lock.acquire():
_from.reduce(money)
time.sleep(1)
if to.lock.acquire():
to.add(money)
to.lock.release()
_from.lock.release()
if __name__ == '__main__':
a = Account('a', 1000, threading.Lock()) # 900
b = Account('b', 1000, threading.Lock()) # 1100
t1 = threading.Thread(target=transfer, args=(a, b, 200))
t2 = threading.Thread(target=transfer, args=(b, a, 100))
t1.start()
t2.start()
print(a.money)
print(b.money)
eg2:
#定义全局变量
money = 0
def add():
for i in range(1000000):
global money
lock.acquire()
money += 1
lock.release()
def reduce():
for i in range(1000000):
global money
lock.acquire()
money -= 1
lock.release()
if __name__ == '__main__':
from threading import Thread, Lock
# 创建线程锁
lock = Lock()
t1 = Thread(target=add)
t2 = Thread(target=reduce)
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
运行结果为:
线程同步: 当有一个线程在对内存进行操作的时候其他线程都不可以对这个内存地址进行操作,直到线程完成操作,其他线程才能对该内存地址进行操作
理解: 同步: 协同步调。按照预定的先后次序进行运行
同: 协同, 协助, 互相配合
Q: 1.为什么需要线程锁:
A; 多个线程程在同一时间对同一个数据进行修改的时候,会出现混乱
Q2 : 实现线程锁的方法:
A: 1):实例化一个锁的对象
lock = threading.Lock()
2)操作变量之前加一个锁
lock.acquire()
3) 操作变量之后进行解锁
lock.release()
四. 协程
1. 协程的优势
1) 执行效率高,因为子程序切换(函数)不是线程切换,而是由程序自身控制
2) 没有切换线程的步骤。 所以与多线程相比,线程的数量越多,协程性能的优势越明显
3)不需要多线程的锁机制,因为 只有一个线程,也不存在同时写变量冲突,在控制资源的时候也不用加锁,执行效率高。
2. 协程的实现方法
1) yield
eg:
import time
def costumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s' % n)
time.sleep(1)
r = '200 OK'
def producer( c ):
c.next()
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s' % n)
r = c.send(n)
print('[PRODUCER] Producing %s' % r)
c.close()
if __name__ == '__main__':
c = costumer()
producer( c)
-
gevent 实现协程
思路: 当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
eg:import gevent import requests import json from gevent import monkey from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from threading import Thread from gevent import monkey #打补丁 monkey.patch_all() def task(ip): url = 'http://ip-api.com/json/%s' % (ip) try: response = requests.get(url) except Exception as e: print("网页获取错误:", e) else: contentPage = response.text # 将页面的json字符串转换成便于处理的字典; data_dict = json.loads(contentPage) # 获取对应的城市和国家 city = data_dict.get('city', 'null') # None country = data_dict.get('country', 'null') print(ip, city, country) # 存储到数据库表中ips ipObj = IP(ip=ip, city=city, country=country) session.add(ipObj) session.commit() if __name__ == '__main__': engine = create_engine("mysql+pymysql://root:westos@172.25.254.123/pymysql", encoding='utf8', # echo=True ) # 创建缓存对象 Session = sessionmaker(bind=engine) session = Session() # 声明基类 Base = declarative_base() class IP(Base): __tablename__ = 'ips' id = Column(Integer, primary_key=True, autoincrement=True) ip = Column(String(20), nullable=False) city = Column(String(30)) country = Column(String(30)) def __repr__(self): return self.ip #创建数据表 Base.metadata.create_all(engine) #使用协程 gevents = [gevent.spawn(task, '1.1.1.' + str(ip + 1)) for ip in range(10)] gevent.joinall(gevents) print("执行结束....")
总结:
总结:
- 线程与进程的区别;
1) 进程是资源分配最小单位。 线程是程序执行的最小单
2) 进程有自己的独立地址空间。线程是共享进程中的数据的,使用相同 的地址空间
3) 进程之间的通信需要以通信的方式(IPC)进行。线程间在同一进程下线程间共享全局变量,静态便两个等数据。
-
进程之间内存是否共享? 如何实现通讯?
进程间能共享
通信方式有: 1. 管道, 2. 信号,3. 消息队列,4. 信号量,5. 套接词 -
进程间的通讯方式
通信方式有: 1. 管道, 2. 信号,3. 消息队列,4. 信号量,5. 套接词 -
多线程的实现方法:
1). 实例化对象的方法
2) 采用创建子类的方法 -
GIL 锁。
GIL锁是全局解释器锁: 作用是能够保证同一时刻只有一个线程在运行 -
python中是否线程安全? 如何解决线程安全?
线程是不安全的。 因为线程是共享一个内存空间,所以所有的线程都可以对全局变量进行修改,那么会 造成该线程之间对全局变量的混乱。
解决方法: 加一个线程锁(GIL) -
什么叫做死锁?
死锁 就是在线程共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,这样的情况就会造成死锁 -
什么是协程? 常见的协程模块有哪些
协程就是在执行的过程中子程序的内部可以中断,然后转而执行别的子程序,在适当的时候再回来接着执行 -
协程中的join()是用来做什么的? 它是怎么发挥作用的?
join()方法: 用来等待进程实例执行结束,或者是等待多少秒
作用: 在运行的过程中,可以使每个实例运行结束后再返回给主函数,然后再结束程序。