Python 多线程,多进程

任何一门语言都需要有多任务处理能力,Python自然也一样,有很多人诟病Python慢,Python慢也只是多线程的时候慢,如果你还没有使用多线程就说Python慢,那就是你的问题了,话说回来,为什么Python的多线程会慢呢,是因为CPython解释器的GIL锁的问题,Python的多线程直接调用的操作系统的多线程,也就是说Python的多线程是真的多线程,不是虚拟出来的,CPython在解释Python代码的过程中理论上单线程的,CPython会在多个操作系统线程上解释Python代码,这是Python慢的根本原因。如果你使用其他Python解释器就不会有这种问题,CPython也尝试去掉GIL锁,但是效率反而没有现在高。
说的有点远,对于Python来说,实现多任务的方式有三种,多进程,多线程和协程,只有多进程能够真正的并行,多线程和协程都是并发。

1 进程

python的内置multiprocessing包提供了调用多进程的接口。

1.1 基本应用

import time
import multiprocessing


def task(name):
    while True:
        print(f"Hello {name}")
        time.sleep(1)


def main():
    p1 = multiprocessing.Process(target=task, args=("xiaoming",), name="xiaoming")
    p2 = multiprocessing.Process(target=task, args=("xiaohong",), name="xiaohong")
    p1.start()
    if p1.is_alive():
        print(f"{p1.name}-{p1.pid}")
    p2.start()
    if p2.is_alive():
        print(f"{p2.name}-{p2.pid}")


if __name__ == '__main__':
    print(f"主进程开始:{multiprocessing.process.current_process().pid}-{multiprocessing.process.current_process().pid}")
    main()

在这里插入图片描述
这是最基本的应用,同一个任务需要两个进程共同执行。

1.2 子进程做为主进程的守护进程

有一些场景可能需要子进程守护主进程,比如,一款带北京音乐的游戏,游戏启动后背景音乐要跟着启动,游戏停止了,背景音乐自然也要停止,不能游戏停止了背景音乐还在继续。

class Game:
    def __init__(self, name):
        self.name = name

    def screen(self):
        while True:
            print(f"玩{self.name}中。。。。")
            time.sleep(1)

    def bg_music(self):
        while True:
            print("背景音乐播放中。。。。")
            time.sleep(2)


def main():
    game = Game("CS1.5", True)
    p_game = multiprocessing.Process(target=game.screen, daemon=True)
    p_mucis = multiprocessing.Process(target=game.bg_music, daemon=True)
    p_game.start()
    p_mucis.start()
    time.sleep(10)
    print("老婆回家了,赶紧收电脑")


if __name__ == '__main__':
    main()

上面的例子有三条进程,主进程模拟玩了10秒钟游戏后停止,游戏子进程和背景音乐子进程是主进程的守护进程,主进程停止后,自动停止。

1.3 操作进程的常用方法

属性

p.name 
p.pid 

方法

p.is_alive()  # 返回True False 
p.start()     # 启动进程
p.join()    # 主进程等待子进程执行结束
p.terminate()    # 终止进程
p.kill()      # 杀死进程

1.4 子进程拥有独立的内存空间

进程是操作系统分配资源的最小单位,每个进程拥有独立的地址空间。

import multiprocessing

name = "哈利波特"


def show():
    print(name)
    print(f"子进程{multiprocessing.process.current_process().pid}内,name的内存地址为{id(name)}")


def main():
    p1 = multiprocessing.Process(target=show)
    p2 = multiprocessing.Process(target=show)

    p1.start()
    p2.start()

    print(f"主进程内:name的内存地址:{id(name)}")


if __name__ == '__main__':
    main()

在这里插入图片描述
为了区分明显,这里先剧透一下多线程,同一个进程中的多个线程共享同一个内存地址空间。

import threading

name = "哈利波特"


def show():
    print(name)
    print(f"子线程{threading.current_thread().ident}内,name的内存地址为{id(name)}")


def main():
    t1 = threading.Thread(target=show)
    t2 = threading.Thread(target=show)

    t1.start()
    t2.start()

    print(f"主线程内:name的内存地址:{id(name)}")


if __name__ == '__main__':
    main()

在这里插入图片描述

1.5 进程间的通信

前面的例子基本上都是一个进程就做一件事情,但是在实际的开发过程中,大多数都是进程之间配合完成工作的,常用的生产者消费者模型,多进程之间的通信可以使用队列实现。

import time
import multiprocessing
import random


def product(q, delay):
    while True:
        if q.full():
            print("生产力饱和,减慢生产速度")
            delay = 10
        else:
            x = random.randint(1, 100)
            print(f"生产者生产{x}")
            q.put(x)
            delay = 1
        time.sleep(delay)


def consume(q, delay):
    while True:
        if q.empty():
            print("消费速度快,减速消费")
            delay = 10
        else:
            result = q.get()
            print(f"消费者消费: {result}")
            delay=1
        time.sleep(delay)


def main():
    q = multiprocessing.Queue(maxsize=10)
    p_pro = multiprocessing.Process(target=product, args=(q, 1))
    p_con = multiprocessing.Process(target=consume, args=(q, 1))
    p_pro.start()
    p_con.start()


if __name__ == '__main__':
    main()

注意这里的队列不要使用queue,Queue,这个队列不支持多线程,会报错

1.6 进程池

创建进程销毁进程都会占用系统资源,在不清楚需要多少进程的情况下,比如web请求,不知道什么时候来的请求会多,这种情况下可以使用池化技术,创建一个任务队列,所有的任务都在任务队列中排队,进程池每次处理指定数量的任务,进程循环利用,省去了创建和销毁的性能开销。
标准库中的这两个模块提供了进程池和线程池的支持:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

2 线程

进程是操作系统调度资源的最小单元,线程是CPU调度的最小单元,同一个进程中可以有多个线程,多个线程共享同一片地址空间

2.1 基本使用

threading模块的Thread类提供Python多线程高级别API

import threading


def sing():
    while True:
        print("singing")


def dance():
    while True:
        print("dancing")


def main():
    threading.Thread(target=sing).start()
    threading.Thread(target=dance).start()


if __name__ == '__main__':
    main()

线程与进程相同的定义就不重复,可以模仿进程操作

2.2 本地线程

本地线程就是每个线程拥有一个隔离的内存空间。多线程不会互相干扰‘

2.3 线程锁

我只是模拟了一下多线程争夺全局变量的问题,实际中不会用多线程做这种减法

import time
import threading

count = 10


def decr():
    global count
    temp = count 
    # 模拟延时,0.01s对于人类来说非常短,但是对于CPU来讲可以做很多事情,
    # 当遇到IO阻塞后,操作系统会切换到别的线程,这时候,count还没有减去1,
    # 所以10个线程拿到的count数据都是10,别减了10次还是9 ,这是明显不对的
    time.sleep(0.01) 
    count = temp - 1
    print(f"==当前线程id{threading.current_thread().ident}==count = {count}\n")


ts = []

for i in range(10):
    t = threading.Thread(target=decr)
    t.start()
    ts.append(t)

for t in ts:
    t.join()  # 主线程阻塞等待子线程执行结束后再结束
"""
==当前线程id2332==count = 9
==当前线程id12000==count = 9
==当前线程id8972==count = 9
==当前线程id13424==count = 9
==当前线程id12016==count = 9
==当前线程id3620==count = 9
==当前线程id12740==count = 9
==当前线程id10176==count = 9
==当前线程id12116==count = 9
==当前线程id1052==count = 9
"""

使用锁,一个线程对一段代码上锁后,只有这个线程解锁后,其他线程才能继续操作

import time
import threading

count = 10

lock = threading.Lock()


def decr():
    try:
        lock.acquire() # 获取锁
        global count
        temp = count
        time.sleep(0.01)
        count = temp - 1
        print(f"==当前线程id{threading.current_thread().ident}==count = {count}\n")
    finally:
        lock.release() # 释放锁 支持with上下文管理器

ts = []

for i in range(10):
    t = threading.Thread(target=decr)
    t.start()
    ts.append(t)

for t in ts:
    t.join()  # 主线程阻塞等待子线程执行结束后再结束

"""
==当前线程id10828==count = 9
==当前线程id1044==count = 8
==当前线程id4116==count = 7
==当前线程id1624==count = 6
==当前线程id660==count = 5
==当前线程id2748==count = 4
==当前线程id6552==count = 3
==当前线程id6540==count = 2
==当前线程id2836==count = 1
==当前线程id7304==count = 0
"""

2.4 死锁

import time
import threading

# 创建两把锁
lockA = threading.Lock()
lockB = threading.Lock()

a = 0
b = 0


def taskA():
    global a, b
    lockA.acquire()
    temp = a
    print("变量a上锁")
    lockB.acquire()
    temp_ = b
    print("变量b上锁")
    lockB.release()
    print("变量b释放锁")
    lockA.release()
    print("变量a释放锁")


# taskA 抢a锁的时候,刚好taskB抢b锁
def taskB():
    global a, b
    lockB.acquire()
    time.sleep(1)
    temp = b
    print("变量b上锁")
    lockA.acquire()
    temp_ = a
    print("变量a上锁")
    lockB.release()
    print("变量a释放锁")
    lockA.release()
    print("变量b释放锁")


def main():
    taskA()
    taskB()


ts = []

for i in range(10):
    t = threading.Thread(target=main)
    t.start()
    ts.append(t)

for t in ts:
    t.join()  # 主线程阻塞等待子线程执行结束后再结束

在这里插入图片描述

2.5 递归锁(解决死锁)

当两个线程同时争一把锁的情况下,就会出现死锁,程序会一直阻塞住,不往下运行了,所以程序中一定要避免使用死锁,递归锁可以解决这个问题,RLock本身有一个计数器,如果碰到acquire,那么计数器+1,如果计数器大于0,那么其他线程无法获取锁,如果碰到release,计数器-1

import time
import threading

# 创建两把锁
lockR = threading.RLock()

a = 0
b = 0


def taskA():
    global a, b
    lockR.acquire()
    temp = a
    print("变量a上锁")
    lockR.acquire()
    temp_ = b
    print("变量b上锁")
    lockR.release()
    print("变量b释放锁")
    lockR.release()
    print("变量a释放锁")


# taskA 抢a锁的时候,刚好taskB抢b锁
def taskB():
    global a, b
    lockR.acquire()
    time.sleep(1)
    temp = b
    print("变量b上锁")
    lockR.acquire()
    temp_ = a
    print("变量a上锁")
    lockR.release()
    print("变量a释放锁")
    lockR.release()
    print("变量b释放锁")


def main():
    taskA()
    taskB()


ts = []

for i in range(10):
    t = threading.Thread(target=main)
    t.start()
    ts.append(t)

for t in ts:
    t.join()  # 主线程阻塞等待子线程执行结束后再结束

2.6 信号量

信号量本身也是一种锁,在同一时刻,只能有设定数量的线程执行操作

import threading
import time

sem = threading.Semaphore(5)


def task():
    with sem:
        print("Hello")
        time.sleep(2)


ts = []

for i in range(100):
    t = threading.Thread(target=task)
    t.start()
    ts.append(t)

for t in ts:
    t.join()

上面的简单示例没2s打印5次hello。

2.7 事件对象

生活中的小常识,必须要切好菜之后才能炒菜,菜没切好就要等着,对于线程来说就是阻塞住。
Event对象提供了简单的线程通信方式,

event.isSet()  #返回event的状态值;初始值为False
event.wait()   #如果 event.isSet()==False将阻塞线程;
event.set()    #设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
event.clear()  #恢复event的状态值为False。
import threading
import time

event = threading.Event()


def qiecai():
    print("开始切菜")
    time.sleep(5)
    event.isSet() or event.set()


def zuofan():
    print("等待切菜。。。")
    event.wait()
    print("菜切好了,开始炒菜")
    time.sleep(3)
    print("出锅")
    event.clear()


t1 = threading.Thread(target=qiecai)
t2 = threading.Thread(target=zuofan)

t1.start()
t2.start()

t1.join()
t2.join()

在这里插入图片描述

2.8 条件对象

acquire(*args)
请求底层锁。此方法调用底层锁的相应方法,返回值是底层锁相应方法的返回值。

release()
释放底层锁。此方法调用底层锁的相应方法。没有返回值。

wait(timeout=None)
等待直到被通知或发生超时。如果线程在调用此方法时没有获得锁,将会引发 RuntimeError 异常。

这个方法释放底层锁,然后阻塞,直到在另外一个线程中调用同一个条件变量的 notify() 或 notify_all() 唤醒它,或者直到可选的超时发生。一旦被唤醒或者超时,它重新获得锁并返回。

当提供了 timeout 参数且不是 None 时,它应该是一个浮点数,代表操作的超时时间,以秒为单位(可以为小数)。

当底层锁是个 RLock ,不会使用它的 release() 方法释放锁,因为当它被递归多次获取时,实际上可能无法解锁。相反,使用了 RLock 类的内部接口,即使多次递归获取它也能解锁它。 然后,在重新获取锁时,使用另一个内部接口来恢复递归级别。

返回 True ,除非提供的 timeout 过期,这种情况下返回 False。

在 3.2 版更改: 很明显,方法总是返回 None。

wait_for(predicate, timeout=None)
等待,直到条件计算为真。 predicate 应该是一个可调用对象而且它的返回值可被解释为一个布尔值。可以提供 timeout 参数给出最大等待时间。

这个实用方法会重复地调用 wait() 直到满足判断式或者发生超时。返回值是判断式最后一个返回值,而且如果方法发生超时会返回 False 。

忽略超时功能,调用此方法大致相当于编写:

while not predicate():
    cv.wait()
因此,规则同样适用于 wait() :锁必须在被调用时保持获取,并在返回时重新获取。 随着锁定执行判断式。



notify(n=1)
默认唤醒一个等待这个条件的线程。如果调用线程在没有获得锁的情况下调用这个方法,会引发 RuntimeError 异常。

这个方法唤醒最多 n 个正在等待这个条件变量的线程;如果没有线程在等待,这是一个空操作。

当前实现中,如果至少有 n 个线程正在等待,准确唤醒 n 个线程。但是依赖这个行为并不安全。未来,优化的实现有时会唤醒超过 n 个线程。

注意:被唤醒的线程并没有真正恢复到它调用的 wait() ,直到它可以重新获得锁。 因为 notify() 不释放锁,其调用者才应该这样做。

notify_all()
唤醒所有正在等待这个条件的线程。这个方法行为与 notify() 相似,但并不只唤醒单一线程,而是唤醒所有等待线程。如果调用线程在调用这个方法时没有获得锁,会引发 RuntimeError 异常。

notifyAll 方法是此方法的已弃用别名。
import threading
import time

count = 500
con = threading.Condition()

class Producer(threading.Thread):
    # 生产者函数
    def run(self):
        global count
        while True:
            if con.acquire():
                # 当count 小于等于1000 的时候进行生产
                if count > 1000:
                    con.wait()
                else:
                    count = count + 100
                    msg = self.name + ' produce 100, count=' + str(count)
                    print(msg)
                    # 完成生成后唤醒waiting状态的线程,
                    # 从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁
                    con.notify()
                con.release()
                time.sleep(1)


class Consumer(threading.Thread):
    # 消费者函数
    def run(self):
        global count
        while True:
            # 当count 大于等于100的时候进行消费
            if con.acquire():
                if count < 100:
                    con.wait()

                else:
                    count = count - 5
                    msg = self.name + ' consume 5, count=' + str(count)
                    print(msg)
                    con.notify()
                    # 完成生成后唤醒waiting状态的线程,
                    # 从waiting池中挑选一个线程,通知其调用acquire方法尝试取到锁
                con.release()
                time.sleep(1)





def test():
    for i in range(2):
        p = Producer()
        p.start()
    for i in range(5):
        c = Consumer()
        c.start()


if __name__ == '__main__':
    test()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

kobe_OKOK_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值