python高级知识之进程&线程&协程

一、进程

1、定义

进程:由操作系统控制,创建并管理(创建、启动、挂起和销毁),每个进程占用独立的资源

并发:伪并行,就是几个进程共用一个cpu,几个进程之间是时间轮换的,而这个轮换时间很短,肉眼是看不到的,所以我们肉眼看到的是几个进程同时在进行,其他中间有停止的,只是停止时间短,这就叫并发

并行:指一个进程就用一个cpu,每个进程是没有停止的,就是一起运行的

同步:一个任务执行完后,另一个任务才能开始执行,就是有一个任务需要等待,这样两个任务之间的关系是同根生的,非常紧密的

异步:两个任务之间没有关联,你执行你的,我执行我的,互相不用等待

2、进程实现

from multiprocessing import Process


class MyProccess(Process):
    def __init__(self):
        super().__init__()
    def run(self):
        print('hello')
if __name__ == '__main__':
    p1=MyProccess()
    p1.start()   # 会调用MyProccess的run方法
--------结果如下-----------
hello

3、应用

import os
import time
from multiprocessing import Process


def music():
    time.sleep(2)
    print('music正在占用的pid为', os.getpid())
    print("听音乐。。。")


def lol():
    time.sleep(2)
    print('lol正在占用的pid为', os.getpid())
    print('打游戏。。。')


if __name__ == '__main__':
    print('主进程的pid为',os.getpid())  # 获取当前线程的pid
    p1 = Process(target=music)
    p2 = Process(target=lol)
    p1.daemon = True # 守护进程,默认值是False,当为True的时候主进程结束则守护进程也强制结束
    p2.daemon = True
    p1.start()
    p2.start()
    p1.join()  # 线程阻塞,上面的代码任务没有执行完,不会运行下面的代码
    p2.join()
    print('主进程结束')
--------结果如下-----------
主进程的pid为 33912
music正在占用的pid为 33914
听音乐。。。
lol正在占用的pid为 33915
打游戏。。。
主进程结束

注意:
1)守护进程daemon默认值是False,设置为True后主进程结束则守护进程也强制结束,该设置必须写在start()前面;
2)加入代码p1.join() 后,join后面的主进程程序必须等子进程执行完后在执行

4、同步锁

import time
from multiprocessing import Process, Lock


def fun(name):
    with open('water.txt', 'r', encoding='utf-8') as f:
        n = f.read()
        print(f"{name}看到剩余{n}瓶")


def buy_water(name, lock):
    time.sleep(0.5)
    lock.acquire()
    with open('water.txt', 'r', encoding='utf-8') as fp:
        n = fp.read()
    if n == '1':
        print(f'{name}买到水了!')
        n = int(n) - 1
    elif n == '0':
        print(f'很遗憾,{name}没买到水!')
    with open('water.txt', 'w', encoding='utf-8') as f:
        f.write(str(n))
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    for i in range(10):
        fun(i)
        p = Process(target=buy_water, args=(i, lock))
        p.start()
--------结果如下-----------
0看到剩余11看到剩余12看到剩余13看到剩余14看到剩余15看到剩余16看到剩余17看到剩余18看到剩余19看到剩余15买到水了!
很遗憾,6没买到水!
很遗憾,0没买到水!
很遗憾,1没买到水!
很遗憾,9没买到水!
很遗憾,2没买到水!
很遗憾,3没买到水!
很遗憾,7没买到水!
很遗憾,8没买到水!
很遗憾,4没买到水!

以上示例可以看出:同步锁把抢水的程序锁起来了,所以,虽然有10个用户都在抢票,但实际上只有一人能进入抢水程序,其他的人要等待第一个进抢水程序的人执行完后才能又进一个人,反正每次就只允许一个人在使用抢水程序,其他的人继续排队

5、信号量

Semaphore的内部维护了一个计数器,每一次acquire操作都会让计数器减1,每一次release操作都会让计数器加1,当计数器为0时,任何线程的acquire操作都不会成功,Semaphore确保对资源的访问有一个上限, 这样,就可以控制并发量

import time
from multiprocessing import Process, Semaphore


def play_game(name, s):
    s.acquire()
    time.sleep(0.5)
    print(f"{name}在打游戏")
    time.sleep(1)
    print(f"{name}打完了")
    time.sleep(1)
    s.release()


if __name__ == '__main__':
    s = Semaphore(4)
    for name in range(10):
        p = Process(target=play_game, args=(name, s))
        p.start()
--------结果如下-----------
3在打游戏
0在打游戏
2在打游戏
1在打游戏
2打完了
1打完了
0打完了
3打完了
4在打游戏
6在打游戏
5在打游戏
7在打游戏
5打完了
6打完了
4打完了
7打完了
8在打游戏
9在打游戏
8打完了
9打完了

以上示例可以看出:信号量就是对一段程序设定允许最多几个人使用,相当于一个游戏厅,只有四个位置,最多允许四个人在游戏,后面人要等待前面的人打完才能打。

6、事件

事件event中有一个全局内置标志Flag,值为 True 或者False。使用wait()函数的线程会处于阻塞状态,此时Flag指为False,直到有其他线程调用set()函数让全局标志Flag置为True,其阻塞的线程立刻恢复运行,还可以用isSet()函数检查当前的Flag状态

import time
from multiprocessing import Process, Event


def test(e):
    print("子进程开始")
    time.sleep(2)
    print("子进程结束")

    # 设置事件值为Ture,我们还可以通过e.clear()把值改为False
    e.set()


if __name__ == '__main__':

    # 创建事件对象e,此时默认值为False
    e = Event()

    # 创建子进程,把e传给子进程
    p = Process(target=test, args=(e,))
    p.start()
    print('等待子进程结束后拿到值')

    # 当值为False会阻塞,当值为Ture的时候,不会阻塞,因为我们在子进程中最好把值改为Ture,所以wait()以后的程序要等到子进程执行完才能执行
    e.wait()
    print('拿到值,执行主进程')
    time.sleep(1)
    print('主进程执行完毕')
--------结果如下-----------
子进程开始
等待子进程结束后拿到值
子进程结束
拿到值,执行主进程
主进程执行完毕

总结:
注意互斥锁Lock与事件Event区别,需求不同,使用方式也不同:

  • 互斥锁Lock主要针对多个线程同时操作同一个数据,使用互斥锁可以保证数据正常修改或者访问;

  • 事件Event主要用于唤醒正在阻塞等待状态的线程/进程;

7、队列,消费者生产者模型

1、队列
队列就相当于一个容器,里面可以放数据,特点是先放进去先拿出来,即先进先出

#coding:utf-8
from multiprocessing import Process,Queue
def work01(q:Queue):
    print('work01正在工作。。。')
    while not q.empty():
        print(f'work01获得了{q.get()}')
        q.task_done()  # 这个任务做完了
def work02(q:Queue):
    print('work02正在工作...')
    while not q.empty():
        print(f'work02获得了{q.get()}')
        q.task_done()  # 这个任务做完了

if __name__ == '__main__':
    q=Queue()
    q.put('a')
    q.put('b')
    q.put('c')
    p1=Process(target=work01,args=(q,))
    p2=Process(target=work02,args=(q,))
    p1.start()
    p2.start()
    print('主进程运行完毕')
# 使用list: work01 work02会做重复工作   p1 p2 进程 独享 list内存
# 使用Queue: work01 work02共同完成q中的所有任务     Queue 是被 p1 p2 共享的
from multiprocessing import Queue  
q=Queue(2)                           #创建一个队列对象,并给他设置容器大小,即能放几个数据
q.put(1)                             #put()方法是往容器里放数据
q.put(2)
q.put(3)                             #这是往容器里放第三个数据,由于只能放两个,所以此处会阻塞,不会报错,相当于死循环
q.put_nowait(4)                      #put_nowait()这也是从容器里放数据的方法,但如果容器满了,不会阻塞,会直接报错
q.get()                              #get()方法是从容器里拿数据
q.get()
q.get()                              #这是从容器里拿第三个数据,但是容器的两个数据拿完了,没有数据了,此时也会阻塞,不会报错,死循环
q.get(False)                         #这也是从容器里拿数据的方法,当没数据时不会阻塞,直接报错
q.get_nowait()                       #这也是从容器里拿数据的方法,当没数据时不会阻塞,直接报错
q.full()                             #这是查看容器是否满了,如果满了,返回Ture,否则返回False
q.empty()                             #这是查看容器是否为空,如果为空,返回Ture,否则返回False

2、消费者生产者模型
就是消费者和生产者之间不是直接联系的,而是通过中间的一个队列来进行联系的,生产者把生产的东西放进进队列里,消费者从队列里拿东西,其实消费者和生产者之间没有实质的联系

import time
from multiprocessing import Process, Event, Queue


def product(q: Queue):
    for i in range(10):
        time.sleep(1)
        q.put(i)
        print(f"零件{i}已产出")
    q.put('over')


def customs(q: Queue):
    while True:
        time.sleep(1)
        s = q.get()
        if s == 'over':
            break
        print(f"零件{s}已打包")


if __name__ == '__main__':
    q = Queue(10)
    p1 = Process(target=product, args=(q,))
    p2 = Process(target=customs, args=(q,))
    p1.start()
    p2.start()
--------结果如下-----------
零件0已产出
零件0已打包
零件1已产出
零件1已打包
零件2已产出
零件2已打包
零件3已产出
零件3已打包
零件4已产出
零件4已打包
零件5已产出
零件5已打包
零件6已产出
零件6已打包
零件7已产出
零件7已打包
零件8已产出
零件8已打包
零件9已产出
零件9已打包

以上示例虽然实现了生产者和消费者的功能,但是存在一个问题就是:有个问题就是,生产者要把生产结束的消息放进队列里,让消费者过去结束消息,消费者才知道队列里没有数据了,这样消费者才能停止,如果生产者不放结束消息,当生产者结束时,即不往队列放数据,而消费者不知道生产者已经结束,还一直往队列拿数据,当队列没数据时,消费者一边就会阻塞。解决阻塞,有几个消费者,生产者就应该放几个结束消息,让每个消费者都知道,这样每个消费者才能结束。但是生产者又不知道有几个消费者,所以不知奥应该放几个结束数据,这样就无法解决对消费者现象。此时,我们就就可以引入joinablequeue
3、joinablequeue

import time
from multiprocessing import Process, Queue, JoinableQueue


def product(q: Queue):
    for i in range(10):
        time.sleep(1)
        q.put(i)
        print(f"零件{i}已产出")
    # 当q收到的task_done数量等于放进q的数据数量时,生产者就结束了
    q.join()


def customs(q: Queue):
    while True:
        time.sleep(1)
        s = q.get()
        if s == 'over':
            break
        print(f"零件{s}已打包")
        q.task_done()


if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=product, args=(q,))
    p1.start()
    for i in range(3):
        p2 = Process(target=customs, args=(q,))
        # 把创建的每个消费者子进程设为守护进程
        p2.daemon = True
        p2.start()
    # 主进程要等到生产者子进程结束后才结束
    p1.join()
--------结果如下-----------
零件0已产出
零件0已打包
零件1已产出
零件1已打包
零件2已产出
零件2已打包
零件3已产出
零件3已打包
零件4已产出零件4已打包

零件5已产出零件5已打包

零件6已产出
零件6已打包
零件7已产出
零件7已打包
零件8已产出零件8已打包

零件9已产出
零件9已打包

整个个过程就是:生产者生产了10个零件,3个消费者打包零件,每打包一个零件往q里发一个task_done,当q拥有10个task_done时,意味着10个零件打包完了,此时,生产者就结束了,接着主进程也也结束了,然后守护进程跟着结束了,即所有的消费者子进程结束,解决上面所遇到的问题

8、进程池

定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果

def square(x):
    return x * x
if __name__ == '__main__':
    pool = Pool(2)
    result=pool.map(square,[1,2,3,4,5])   # map 会阻塞主程序
    print(result)
    pool.close()
    print("主进程结束")
--------结果如下-----------
[1, 4, 9, 16, 25]
主进程结束

1、进程池同步方法
就是把进程搞成同步的,只有一个执行完了,下一个进程才能执行,主要运用apply()方法

from multiprocessing import Process, Pool


def test(i):
    sum = 0
    for n in range(5):
        sum += i
    return sum


if __name__ == '__main__':
    p = Pool(4)
    for i in range(10):
    	# 调用进程池的apply()方法,虽是10个进程,但是只能一个接着一个执行,此时返回的一个值
        p1 = p.apply(func=test, args=(i,))
        print(p1)
--------结果如下-----------
0
5
10
15
20
25
30
35
40
45

2、进程池异步方法
运用apply_async()方法

from multiprocessing import Process, Pool


def test(i):
    sum = 0
    for n in range(5):
        sum += i
    return sum


if __name__ == '__main__':
    p = Pool(4)
    l1 = []
    for i in range(10):
    	# 调用进程池的apply_async()方法,由于进程池只能允许有4个进程,所以,相当于同时有4个进程在进程池执行,但现在返回的不是值,而是一个对象
        p1 = p.apply_async(func=test, args=(i,))
        # 我们把对象放在一个列表,等所有程序结束再从列表拿值,如现在就用res.get()去拿值,就会形成阻塞,从而演变成进程一个接一个执行,变成同步执行的
        l1.append(p1)
    p.close()
    p.join()
    for j in l1:
        print(j.get())
--------结果如下-----------
0
5
10
15
20
25
30
35
40
45

3、回调函数

import time
from multiprocessing import Process, Pool


def test(num):
    return num


def call_test(i):
    print("我是回调函数")


if __name__ == '__main__':
    p = Pool(4)
    p.apply_async(func=test, args=(5,), callback=call_test)
    time.sleep(2)

注意:一个问题是,若不在主进程中写入一个延迟2秒,当主进程代码走完,而子进程还没执行完,从而子进程会随着主进程结束而结束,从而回调函数也死亡

二、线程

1、定义

线程:在进程中创建,线程不拥有资源,共享进程的资源

2、应用

import threading
import time


def music(user):
    print(f'{user}正在听音乐....')
    print(f'{threading.current_thread().name}正在运行...')
    time.sleep(5)
    print(f'{threading.current_thread().name}运行即将结束。')


def lol(user):
    time.sleep(8)


if __name__ == '__main__':
    t1 = threading.Thread(target=music, args=('无极',), name='线程1')
    t2 = threading.Thread(target=lol, args=('无极',), name='线程2')
    t1.start()
    t2.start()
    t1.join()  # 阻塞主程序,但不会阻塞线程2
    t2.join()
    print('主程序结束!')

3、线程锁

import threading

n = 200000
lock = threading.Lock()
print(hasattr(lock, '__enter__'))  # True
print(hasattr(lock, '__exit__'))  # True


def work():
    global n
    for i in range(1000000):
        # 上锁
        lock.acquire()
        n -= 1
        # 释放锁
        lock.release()
        # with lock:
        #     n -= 1


if __name__ == '__main__':
    # 两个线程共同操作n
    t1 = threading.Thread(target=work)
    t2 = threading.Thread(target=work)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(n)

实例:
“”"
一个列表中有100个url地址(假设请求每个地址需要0.5秒),请设计程序一个程序,获取列表中的url地址,使用4个线程去发送这100个请求,计算出总耗时!
“”"

"""
一个列表中有100个url地址(假设请求每个地址需要0.5秒),请设计程序一个程序,获取列表中的url地址,使用4个线程去发送这100个请求,计算出总耗时!
"""

import time
from multiprocessing.pool import ThreadPool
import queue


def download(q: queue.Queue):
    while not q.empty():
        print(q.get())
        time.sleep(0.5)
        q.task_done()  # 这个任务做完了


def calc_time(func):
    def wrap(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        end = time.time()
        print(f"共花费时间{(end - start):.2f}秒")

    return wrap


@calc_time
def main():
    q = queue.Queue()
    for i in range(100):
        q.put(f'http://www.gushi.com/page={i}')

    pool = ThreadPool(4)  # 4个线程
    pool.apply_async(download, args=(q,))  # pool中的多个线程异步地调用指定函数  这里是  download

    q.join()  # 等待所有的任务做完
    print('finished!')


if __name__ == '__main__':
    main()

注意:
Queue.task_done() 与Queue.join()配合使用,在完成一项工作之后,会向任务已经完成的队列发送一个信号,Queue.join() 实际上意味着等到队列为空,再执行其他操作;
如果线程里每从队列里取一次,但没有执行task_done(),则join无法判断队列到底有没有结束,在最后执行join()是等不到信号结果的,会一直挂起。即每task_done一次 就从队列里删掉一个元素,这样join的时候根据队列长度是否为零来判断队列是否结束,从而执行其他操作。

三、进程与线程的区别

区别:
1)资源(主要指内存资源):进程有,线程没有
2)切换:线程比进程快
3)一个程序运行,至少一个进程,至少一个线程
使用场景:
1)CPU密集的不适合多线程,一直占用CPU的场景不适合多线程(工程机器不停地需要工人操作,不适合一个人操作多台机器)
2)间歇性占用CPU的场景适合多线程

四、协程

1、定义

协程,又称微线程,线程,也称为用户级线程,在不开辟线程的基础上完成多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行 通俗理解只要在def里面只看到一个yield关键字表示就是协程

满足协程的条件:
1)挂起当前状态(暂停)yield
2)激活挂起的状态(恢复)send

2、生成器实现并发

import time


def work01():
    for i in range(5):
        print("work01...")
        time.sleep(1)
        yield


def work02():
    for i in range(5):
        print("work02...")
        time.sleep(1)
        yield


if __name__ == '__main__':
    w1 = work01()
    w2 = work02()
    while True:
        try:
            next(w1)
            next(w2)
        except:
            break

3、gevent实现协程

gevent内部封装了greenlet,其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

import time
from gevent import monkey

monkey.patch_all()
import gevent


def work1():
    for i in range(5):
        print('work1:听音乐...')
        time.sleep(1)


def work2():
    for i in range(5):
        print('work2:打游戏...')
        time.sleep(1)


def main():
    g1 = gevent.spawn(work1)  # 创建协程1
    g2 = gevent.spawn(work2)  # 创建协程2
    g1.join()
    g2.join()
    print('所有任务执行完毕!')


if __name__ == '__main__':
    start = time.time()
    main()
    end = time.time()
    print(f'总共花费{(end - start):.2f}秒。')

4、asyncio实现协程

import time
import asyncio  # 异步IO库, 单线程实现并发 —— 协程


# async  关键字     await关键字

async def work1():  # aysnc  任务work1是异步的
    for i in range(5):
        print(f'work1:听音乐...')
        await asyncio.sleep(1)


async def work2():
    for i in range(5):
        print(f'work2:打游戏...')
        await asyncio.sleep(1)


async def main():
    task1 = asyncio.create_task(work1())  # 任务一
    task2 = asyncio.create_task(work2())  # 任务二
    await task1
    await task2


if __name__ == '__main__':
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(f'共花费时间{(end - start):.2f}秒')

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

克里斯蒂亚诺·罗纳尔达

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值