Python并发编程

目录

进程与线程

一、多线程使用

例题

1.编写求花费时间的装饰器以及请求页面函数

2.Python默认自上而下串行执行所花费的时间

3.使用多线程并发执行

4.总结

自定义线程类

二、线程锁

1.互斥锁lock

2.可重入锁rlock

3.条件锁condition

4.事件锁event

5.信号量锁semaphore

三、全局解释器锁

四、多进程

进程的组成

僵尸进程与孤儿进程

多进程模块

自定义进程类

进程之间共享数据

1.manager

2.queue

进程池

五、协程


并发:某一时间段内处理多件事情,交替执行

并行:同一时刻可以处理多件事情,同时执行

进程与线程

进程:正在运行的程序,是系统进行资源分配的最小单位

线程:运行在进程之上,是操作系统进行调度的最小单位

进程与线程之间的区别:

一个进程可以有一个以上的线程,进程之间都是独立的,一个进程内的线程共享这个进程空间。

同个进程内的线程是可以直接交流的,两个进程要想通信,必须要通过内核代理实现。

创建新的线程很简单,但是创建新的进程需要对父进程进行克隆,所有的进程都是由另外一个进程创建。

一个线程可以控制和操作同一个进程内的其他线程,进程只能操作子进程。

一个主线程改变可能会影响其他线程,一个父进程的改变不会影响子进程。

真正在CPU上面运行的是线程。

进程的状态模型(三态模型):

上下文切换:一个进程切换到另一个进程运行,就称为进程的上下文切换

一、多线程使用

threading库是Python的线程模型,使用threading库我们可以实现线程的并发,实现多线程任务

例题

通过计算请求一个页面所花费的时间,对比Python默认自上而下的串行执行和多线程并发执行花费时间的差异,感受一下并发执行

1.编写求花费时间的装饰器以及请求页面函数

import requests
import time

#求执行函数花费多少时间的装饰器
def runtime(func):
    def inner(*args, **kwargs):
        #获取请求页面前的时间
        start = time.time()
        result = func(*args, **kwargs)
        #获取请求完页面后的时间
        end = time.time()
        print(f"spend {end - start}s to execute the function of requesting page")
        return result
    return inner

#请求页面函数
def get_content(url):
    requests.get(url)
    #使用time模拟阻塞时间
    time.sleep(0.5)

2.Python默认自上而下串行执行所花费的时间

@runtime
def main():
    print("自上而下串行执行")
    #多次请求页面
    for i in range(5):
        get_content("http://www.baidu.com")

main()

3.使用多线程并发执行

import threading

@runtime
def main():
    print("多线程并发执行")
    
    t_list = []
    
    #模拟多次请求页面
    for i in range(5):
        t = threading.Thread(target=get_content, args=("http://www.baidu.com",))
        #在t.start启动线程之前设置,默认就为False
        t.setDaemon(False)
        #启动线程,会自动调用t.run方法,t.run方法里面又会去调用传递进来的target
        t.start()
        #将线程实例存入列表,方便阻塞环境上下文
        t_list.append(t)
    
    #等线程全部创建启动后再join
    for t in t_list:
        #阻塞当前环境上下文,直到t的线程执行完成
        t.join()

main()

1.threading.Thread(target=get_content, args=("http://www.baidu.com",))中,target指定任务,传入callable对象(类、函数);args指定要传入的参数,传入的为元组--当只有一个参数时,最后要打逗号

2.t.setDaemon(),当设置False时,表示为前台线程--主线程会等到子线程结束才退出(默认);当设置True时,表示为后台线程--主线程一执行结束就退出

3.t.join,join所完成的工作就是线程同步--主线程任务结束之后,进入阻塞状态,一直等到其他子线程执行结束之后,主线程才会终止

4.总结

对比两种方式运行的结果来看,当有多个任务时,使用多线程执行效率会高许多

自定义线程类

可以对run方法进行重写,在启动线程时实现定制功能

#继承threading.Thread父类
class MyThread(threading.Thread):
    def __init__(self, num):
        #执行父类的__init__
        super().__init__()
        self.num = num
    
    #重写run方法
    def run(self):
        print(f"the number {self.num} thread is running")

#通过自定义的线程类创建线程,并执行线程
t1 = MyThread(1)
t2 = MyThread(2)
t1.start()
t2.start()

二、线程锁

线程在同一个进程内是共享资源的,很容易发生资源的争抢,会产生脏数据

threading模块中提供了5种常见的锁,来保证线程安全,实现对各个线程之间数据的访问、修改变的可控

1.互斥锁lock

一次只放行一个线程,一个被加锁的线程在运行时不会将执行权交出去,只有当该线程被解锁时才会将执行权通过系统调用交给其他线程

当同一线程尝试多次获取同一个锁时,会产生死锁,如何避免产生死锁?

1.尽量避免同一个线程对多个lock进行锁定

2.多个线程对多个lock进行锁定,尽量保证它们以相同的顺序加锁

3.设置超时时间

2.可重入锁rlock

可重入锁是一种特殊类型的互斥锁,底层维护了一个互斥锁和一个计数器,可重入锁可以被拿到锁的线程多次获取,但必须以相同的次数释放,才能真正释放多锁的拥有权

可重入锁与互斥锁的主要区别就是,可重入锁允许同一个线程多次获取同一个锁,而不会产生死锁

互斥锁--一个线程尝试多次获取同一个锁,会产生死锁:

import threading
lock = threading.Lock()

lock.acquire()
print("lock acquire 1")
#同一线程未释放,又去尝试获取同一个原始锁
lock.acquire()
print("lock acquire 2")
lock.release()
print("lock release 1")
lock.release()
print("lock release 2")

可重入锁--同一线程多次获取同一个锁不会产生死锁:

import threading
lock2 = threading.RLock()
lock2.acquire()
print("lock1 acquire 1")
#不会产生死锁
lock2.acquire()
print("lock1 acquire 2")
lock2.release()
print("lock1 release 1")
lock2.release()
print("lock1 release 2")

3.条件锁condition

内部是通过lock和rlock锁实现的,并且在此基础上增加了暂停线程运行的功能,允许一个或多个线程等待某个条件满足才被唤醒

4.事件锁event

事件锁是基于条件锁来做的,它与条件锁的区别在一次只能放行全部,不能放行任意数量的子线程运行

事件锁对象中有一个信号标志,默认为False,如果一个线程等待一个Event对象,那么这个Event对象的标志将决定这个线程是否会被阻塞,如果一个线程将Event对象的标志设置为真,那么所有等待这个Event对象的线程都将会被放行

5.信号量锁semaphore

允许一定数量的线程同时访问锁,可以用semaphore来控制线程的并发数量

三、全局解释器锁

GIL全局解释器锁是解释器层面的锁,是CPython的历史遗留问题,在CPython解释器中,GIL是一把互斥锁,用于阻止同一个进程下多个线程的同时执行。

基本行为:

1.当前执行的线程必须要有全局解释器锁

2.当遇到io阻塞或者CPU时间片到,都会释放全局解释器锁

四、多进程

进程的组成

进程控制块(PCB):进程标识符pid、进程优先级、进程当前状态、进程相应的程序和数据地址、进程资源清单(打开的文件列表等)等

数据段:存放程序运行过程中处理的各种数据

正文段:存放要执行的程序代码

僵尸进程与孤儿进程

正常情况:子进程由父进程创建,子进程再创建新的进程。当子进程结束后,它的父进程会调用wait()或者waitpid()取得子进程的终止状态,回收子进程的资源 

僵尸进程:子进程退出了,但是父进程没有响应--没有调用wait或者waitpid方法去获取子进程的状态,那么这个子进程的进程描述符就会依然存在系统中,这种进程就称为僵尸进程

孤儿进程:父进程退出了,子进程还在运行,那么这个子进程就被称为孤儿进程,孤儿进程会被pid为1的进程收养

多进程模块

multiprocessing是Python中实现多进程的模块

在multiprocessing中,通过创建一个Process对象然后调用其start()方法来生成进程,例:

from multiprocessing import Process, current_process
import time

lst = []

def task():
    # current_process()表示当前进程
    print(current_process().name, f"start...{i}")
    time.sleep(2)
    # 用于展示各个进程都拥有一份数据,相互隔离
    lst.append(i)
    print(f"lst is {lst}")
    print(current_process().name, f"end...{i}")

#只有直接运行时才能创建多进程
if __name__ == "__main__":
    for i in range(4):
        p = Process(target=task, args=(i + 1,))
        p.start()

自定义进程类

对run方法进行重写

from multiprocessing import Process
class MyProcess(Process):
    def __init__(self, value):
        super().__init__()
        self.value = value

    def run(self):
        print(f"running...{self.value}")

if __name__ == "__main__":
    p1 = MyProcess(1)
    p2 = MyProcess(2)
    p1.start()
    p2.start()

进程之间共享数据

1.manager

Python通过manager方式实现多个无关联进程共享数据

from multiprocessing import Process, Manager, Lock
import time


def task(i, temp, lock):
    with lock:
        print(f"start {i}......")
        time.sleep(1)
        temp.append(i + 1)
        print(temp)


if __name__ == "__main__":
    # 创建数据共享对象,底层为socket通信
    m1 = Manager()
    # 用manager方法创建列表--创建出来的变量可以在不同进程之中修改
    temp = m1.list([0])
    #为了防止资源争抢出现脏数据,设置进程锁
    lock = Lock()
    p_list = []
    for i in range(5):
        p = Process(target=task, args=(i, temp, lock))
        p.start()
        p_list.append(p)

    # 等待子进程运行完成,父进程先退出的话,Manager共享就没用了
    [p.join() for p in p_list]

2.queue

queue是一个消息队列来实现进程之间数据共享

queue最大的优势在于它是线程安全的,其中的put、get操作都是原子操作--要么成功要么失败,没有执行到一半的情况

from multiprocessing import Process, Queue

def task(i, q):
    #q.empty()判断队列是否为空
    if not q.empty():
        #取出队列中的数据
        print(i, "--> get value", q.get())

if __name__ == "__main__":
    q = Queue()
    for i in range(5):
        #向队列中存放数据
        q.put(i)
        p = Process(target=task, args=(i, q))
        p.start()

进程池

如果有多少个任务就开启多少个进程,其实并不划算,进程池就是用固定的进程数去执行同样多的任务,采用预创建的技术,在应用启动之初便预先创建一定数量的进程

Python中使用Pool实现进程池,Pool类可以指定数量的进程供用户调用,当有新的请求提交到Pool中时,如果池还没有满,就会创建一个新的进程来执行请求;如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求。

from multiprocessing import current_process, Pool
import time

def task(i):
    print(current_process().name, f"start......{i}")
    time.sleep(2)
    print(current_process().name, f"end......{i}")

if __name__ == "__main__":
    p = Pool(processes=4, maxtasksperchild=2)
    for i in range(8):
        #进程池接收任务,apply_async是非阻塞的
        p.apply_async(func=task, args=(i, ))
    #关闭进程池,不接受任务了
    p.close()
    #阻塞当前环境
    p.join()
    print("process end......")

其中的Pool(processes=4, maxtasksperchild=2)中,processes表示指定进程池中的进程数,建议进程数与cpu核数一致;maxtasksperchild指定每个子进程最多处理多少个任务,达到相应的任务数后当前进程就会退出,开启新的进程,指定maxtasksperchild就是为了定期释放资源

五、协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈,在执行函数A时,可以随时中断去执行函数B,然后又中断继续执行函数A

asyncio是Python实现协程的模块

import asyncio
import time

#定义协程函数 async是定义一个协程的关键字
async def say_after(delay, what):
    print(f"test start.....{what}")
    #await是用于挂起阻塞的异步调用接口的关键字
    await asyncio.sleep(delay)
    print(what)

async def main():
    #asyncio.create_task函数用来并发运行作为asyncio任务的多个协程
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")
    await task1
    await task2
    print(f"finished at {time.strftime('%X')}")

#asyncio.run函数用来运行最高层级的入口点main()函数
asyncio.run(main())

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值