前言:线程、进程和协程都是实现多任务(同一时间可以做多个事情就叫做多任务)的方法
并发:一段时间内做不同的事情,比如一个人吃饭喝水,吃的时候不能喝,喝的时候不能吃,只有在一段时间,吃了再喝或者喝了在吃
并行:同一时间做不同的事情,比如边听音乐边吃饭
1、线程
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
1.1线程的创建方式
第一种:继承Thread类,重写run方法创建线程
import threading
import time
class A(threading.Thread):
def __init__(self,name):
super().__init__(name=name)
def run(self):
for i in range(5):
print(i)
if __name__ == "__main__":
t = A('test_name')
t.start()
第二种:通过写thread.Tread创建
import threading # 内置模块
import time
def demo():
for i in range(3):
print("hello world")
time.sleep(5)
if __name__ == '__main__':
t = threading.Thread(target=demo)
t.daemon = True
t.start()
print("1")
1.2守护线程
- 引入:代码的执行顺序是自上而下,遇到函数或者类就载入内存直到实例化类或者调用函数的时候才去执行内部代码,下面的代码的执行结果如下,是不是很诧异,为什么只打印了一次hello world
- 答案:开启了守护线程daemon后,主线程不再等待子线程结束,而是一旦主线程结束,该程序就结束了
- 扩展:线程的执行顺序和代码执行顺序一样,从上而下,不同的是,在线程中主线程继续往下执行,但会分配一个子线程去执行线程里面的东西,而普通的程序就只有一个主线程,所以主线程不会往下走,而是去执行函数里面的东西
- 解决:想要实现要让子线程执行完毕,在让主线程执行,可以在线程创建开始之后加上t.join()
1.3查看线程数量
使用threading.enumerate()来查看当前线程的数量。
可以通过这个方法去验证是哪一句话创建了线程,就在可以的话上下输出threading.Thread就可以验证,最后发现
当调用Thread的时候,不会创建线程。
当调用Thread创建出来的实例对象的start方法的时候,才会创建线程以及开始运行这个线程。
1.4共享全局变量
线程和普通程序一样共享全局变量,但是进程不会共享全局变量
import threading
g_num = 200
def test1():
global g_num
for i in range(1):
g_num += 1
print("--test1, g_num = %d--" % g_num)
def test2():
print("--test2, g_num = %d--" % g_num)
if __name__ == "__main__":
print("--执行线程之前, g_num = %d--" % g_num)
t1 = threading.Thread(target=test1)
t1.start()
t2 = threading.Thread(target=test2)
t2.start()
1.5资源竞争与解决方法
为什么会发生资源竞争数据足够大的时候,计算机执行的太快,还没来的及写入结果,循环就已经结束,所以会发生资源竞争,为了解决资源竞争,就要加锁保证他们互不干扰,但是加锁之后就是普通的单线程,所以最后衍生为同步锁,同步进行,使用案例为,爬虫常用,详细点击加锁详情
2、进程进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。并且进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程的创建也是两种方法,第一种就是multiprocessing.Process,另一种就是类的继承重写run方法
注意:进程的创建要放在main函数下面,不然会报错,进程池即使有错误也不会报错,有异常出现也不会抛异常,要自己好好写
2.1进程是不共享全局变量的
由下面的代码可以验证,进程之间是不会共享全局变量的,那么进程之间要怎样才能做到通信呢
2.2进程之间的通信队列
在函数 download 中,将list元素保存到队列中
在函数 manage_data 中,将list元素从队列中取出,并且添加到新的列表中
import multiprocessing
# 下载数据
def download(q):
lis = [11, 22, 33]
for item in lis:
q.put(item)
print("下载完成,并且保存到队列中...")
# 处理数据
def manage_data(q):
ana_data = list()
while True:
data = q.get()
ana_data.append(data)
if q.empty():
break
print(ana_data)
def main():
q = multiprocessing.Queue()
t1 = multiprocessing.Process(target=download, args=(q,))
t2 = multiprocessing.Process(target=manage_data, args=(q,))
t1.start()
t2.start()
if __name__ == '__main__':
main()
2.3start和run的区别
- start() 方法来启动进程,真正实现了多进程运行,这时无需等待 run 方法体代码执行完毕而直接继续执行下面的代码:调用 Process 类的 start() 方法来启动一个进程,这时此进程处于就绪(可运行)状态,并没有运行,一旦得到 cpu 时间片,就开始执行 run() 方法,这里方法 run() 称为进程体,当进程结束后,不可以重新启动。
- run() 方法只是类的一个普通方法,如果直接调用 run 方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待 run 方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。
2.4进程池
通过进程池可以创建规定数量的进程,所有的任务都会通过这些进程来完成,但是只有等一个任务结束后,等待的任务才会被进程池中的进程执行,所有任务公用进程池中的进程,进程号不变,这样就可以节省穿件
代码如下,自己看运行效果就明白了
import os, time, multiprocessing
def worker(msg):
t_start = time.time()
print(f"{msg}开始执行,进程号为{os.getpid()}")
time.sleep(2)
t_stop = time.time()
print(msg, f"执行完成,耗时{t_stop - t_start}")
# worker()
po = multiprocessing.Pool(3)
for i in range(10):
# po.apply_async(worker, (i,)) # 异步执行不用等待
po.apply(worker, (i,)) # 同步执行,只有等上一个任务完成后才可以进行下一个任务
po.close() # 将进程池处于无法链接的状态
po.join() # 等待所有的子进程结束后主进程才结束
2.5进程池之间的通信
进程池之间的通信只能根据进程池创建出来的队列进行通信,如果使用的是其他的队列则不会报错也不会执行任务
import multiprocessing
def demo1(q):
q.put("a")
def demo2(q):
data = q.get()
print(data)
if __name__ == '__main__':
# q = multiprocessing.Queue() 程序直接结束
q = multiprocessing.Manager().Queue()
po = multiprocessing.Pool(2)
po.apply_async(demo1, args=(q,))
po.apply_async(demo2, args=(q,))
po.close()
po.join()
3.1定义
协程,又称为微线程,它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,只要在合适的时机,就可以把一个协程切换到另一个协程。
CPU上下文(CPU寄存器和程序计数器):
- CPU寄存器是CPU的内置的容量小,但速度极快的内存。
- 程序计数器则是用来存储CPU正在执行的指令位置、或者即将执行的下一条指令位置。
3.2与线程的差异
线程:每个线程都有自己缓存Cache等等数据,操作系统还会做这些数据的恢复操作。所以线程的切换非常消耗性能。
协程:单纯的操作CPU的上下文,所以一秒切换上百万次系统都能抗住。所以完成多任务的效率比线程和进程都高
3.3实现生成器
3.3.1greenlet实现协程
pip install greenlet
from greenlet import greenlet
import time
def demo1():
while True:
print("demo1")
gr2.switch()
time.sleep(0.5)
def demo2():
while True:
print("demo2")
gr1.switch()
time.sleep(0.5)
if __name__ == '__main__':
gr1 = greenlet(demo1)
gr2 = greenlet(demo2)
# 要手动的通知协程启动
gr1.switch()
3.3.2gevent实现协程
greenlet已经实现了协程,但是这个还的人工切换,就很麻烦,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
原理: 当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
pip install gevent
import gevent
import time
def f1(n):
for i in range(n):
print(gevent.getcurrent(),i)
gevent.sleep(0.5)
def f2(n):
for i in range(n):
print(gevent.getcurrent(),i)
gevent.sleep(0.5)
g1 = gevent.spawn(f1, 5) # 创建协程
g2 = gevent.spawn(f2, 5)
g1.join()
g2.join()
进程是资源分配的单位,切换需要的资源很最大,效率很低
线程是操作系统调度的单位,切换需要的资源一般,效率一般
协程切换任务资源很小,效率高,是一个微线程,线程、协程常用与爬虫,但是由于全局解释器锁的存在,又不是真正意义上的多线程