Python基础10——线程、进程、协程

18 线程

18.1 进程和线程

进程:打开一个程序至少就会有一个进程。操作系统进行资源分配的基本单位

线程:线程是CPU调度的基本单位,每个进程至少都有一个线程。

单线程:只有一个线程

def funa():
    print(123)

def funb():
    print(456)
funa()
funb()

# 先执行funa
# 再执行funb

多线程

线程模块: threading

import threading

线程类Thread参数:

​ target:执行的任务名

​ args: 以元组的形式给执行任务传参

def funa():
    print(123)
    time.sleep(2)
    print("结束了")
    
def funb():
    print(456)
    time.sleep(3)
    print("结束了")
 
if __name__ == "__main__":
    # 1创建子线程
    t1 = threading.Thread(target=funa) #funa是个函数名
	t2 = threading.Thread(target=funb)
    
    # 2.开启子线程
    t1.start()
    t2.start()

带参数的执行

def funa(a):
    print("你好吗",a)
    time.sleep(2)
    print("我很好")

def funb(b):
    print(b)
    time.sleep(2)
    print("有点甜")
    
    
if __name__ == "__main__":
    # 第一种传入参数的方式
    f1 = Thread(target=funa,args=("传入参数",))
    f2 = Thread(target=funa,args=("传入参数2",))
    
    f1.start()
    f2.start()
    
    
    # 第二种传入参数的方式
    f1 = Thread(target=funa,kwargs={"a":"参数啊"})
    f2 = Thread(target=funa,kwargs={"b":"参数2"})
    
    f1.start()
    f2.start()



18.2 线程

步骤

  1. 创建子线程 Thread()
  2. 开启子线程

18.2.1 守护线程 、阻塞线程

守护线程:主线程执行完,子线程立刻结束

阻塞线程:等待join_list中的子线程执行完,再执行主线程

def funa():
    print("开始a")
    time.sleep(2)
    print("结束a")

def funb():
    print("开始b")
    time.sleep(2)
    print("结束b")

if __name__ == "__main__":
    t1 = threading.Thread(target=funa)
    t2 = threading.Thread(target=funb)

    #开启守护线程,主线程执行完,子线程也会跟着结束
    t1.setDaemon(True)
    t2.setDaemon(True)

    t1.start()
    t2.start()
    
    # 阻塞主线程,暂停的作用,只有join的执行完,才会执行主线程
    t1.join()
    t2.join()
    
    t1.setName("线程1")
    t2.setName("线程2")
    
    # 获取线程名字
    t1.getName()
    t2.getName()

    print("这是主线程,程序的最后一行")

18.2.2 线程的执行顺序的无序的

两个任务是一起执行的,线程之间的执行是无序的

def test():
    time.sleep(1)
    print("当前的线程是",threading,current_thread())
   

if __name__ =="__main__":
    for i in range(5):
        # 创建子线程
        s1 = threading.Thread(target=test)
        s1.start()
    

18.2.3 创建线程类

线程执行代码的封装

  1. 继承Thread类
  2. 重写run方法
from threading import Thread

# 定义一个线程类
class Mythread(Thread):
    
    # 重写run方法,规定run这个名字,表示线程活动的方法
    def run(self):
        print("面向对象")
        time.sleep(3)
        print("线程")

if __name__ == "__main__":
    my = MyThread()
    my.start()

18.2.4 资源共享

# 资源共享
from threading import Thread #导入线程模块
import time

li = []
# 写入数据
def wdata():
    for i in range(5):
        li.append(i)
        time.sleep(0.2)
    print("写入的数据是:",li)

# 读取数据
def rdata():
    print("读取的数据是:",li)
    
if __name__ == "__main__":
    wd = Thread(target=wdata)
    rd = Thread(target=radata)
	wd.start()
    
    wd.join() #只有等待写入完毕,才可以执行后面的代码
    rd.start()
    
    
    print("这是最后一行")

18.2.5 资源共享导致资源竞争

a是共享的资源,导致add和add2两个线程去竞争资源a,导致结果不一样。

from threading import Thread

a = 0
n = 1000000
# 循环b次给全局变量a加1
def add():
    for i in range(n):
        global a  #global 声明全局变量
        a += 1
    print("第一次",a)

def add2():
    for i in range(n):
        global a  #global 声明全局变量
        a += 1
    print("第二次",a)
    
    
if __name__ == "__main__":
    # 创建两个子线程
    first = Thread(target=add)
    second = Thread(target=add2)
    
    # 启动线程
    first.start()
    second.start()

# 运行结果
# 第一次 1008170
# 第二次 1509617
    

18.2.6 线程同步的方式

  1. 线程等待(join)

  2. 互斥锁

同步的概念

​ 有两个线程,线程A写入,线程B读取线程A写入的值;线程A先写入,线程B才能读取;线程A和B之间就是一种同步关系。

18.2.7 互斥锁

保证多个线程访问共享数据不会出现数据错误问题:保证同一时刻只能有一个线程去操作

threading模块里面定义了Lock这个函数,通过调用这个函数可以获取到一把互斥锁

互斥锁的作用

  1. 保证同一时刻只有一个线程去操作共享数据,不会出现错误问题。
  2. 使用互斥锁会影响代码的执行效率

如果互斥锁使用不当,就会出现死锁的状态。

acquire() 加锁

release() 解锁

加锁和解锁必须成对出现

from threading import Thread,Lock

a = 0
n = 1000000
# 循环b次给全局变量a加1

# 创建互斥锁
lock = Lock()

def add():
    lock.acquire() #加锁
    for i in range(n):
        global a  #global 声明全局变量
        a += 1
    print("第一次",a)
    lock.release() #解锁

def add2():
    lock.acquire() #加锁
    for i in range(n):
        global a  #global 声明全局变量
        a += 1
    print("第二次",a)
    lock.release() #解锁
    
    
if __name__ == "__main__":
    # 创建两个子线程
    first = Thread(target=add)
    second = Thread(target=add2)
    
    # 启动线程
    first.start()
    second.start()


18.3 进程

18.2.1 进程介绍

运行一个程序就会有一个进程,一个进程默认有一个线程

进程:一个程序运行起来后,代码+用到的资源称之为进程,是操作系统分配资源的基本单位

进程的状态

  1. 就绪态:万事俱备,只欠cpu
  2. 执行态: cpu正在执行其功能
  3. 等待态:等待某些条件满足
import time
print("我们在学习进程")
name = input("请输入你的名字") #用户输入,进行阻塞,等待态。
print(name)    #运行状态
time.sleep(2)   #睡眠2秒,阻塞状态
print("对酒当歌,人生几何") #运行状态

18.3.2 进程创建

multiprocessing模块就是跨平台版本的多进程模块,提供了一个Process类来代表一个进程对象,这个对象可以理解为是一个独立的进程,可以执行另外的事情。


from multiprocessing import Process

# Process 类  参数
#	target : 调用对象,子进程要执行的任务
#    args:以元组的形式传值
#    kwargs:以字典的形式传值
 
    
# 常用的方法:
#	start() 开启子进程
#	is_alive()  判断子进程是否还活着,存活为True
#	join   主进程等待子进程执行完

# 常用的属性
#	name  当前进程的别名
#	pid   当前进程的进程号


import os
from multiprocessing import Process

def one():
    print("这是子进程一")
    print(f"子进程id{os.getpid()},父进程id{os.getppid()}")

def two():
    print("这是子进程二")
    print(f"子进程id{os.getpid()},父进程id{os.getppid()}")
    
if __name__ == "__main__":
    # 创建子进程
    p1 = Process(target=one,name="进程名称1")
    p2 = Process(target=two)
    
    #开启
    p1.start()
    p2.start()
    
    print("p1的子进程名是:",p1.name)
    print("p2的子进程名是: ",p2.name)
    
    #查看子进程的进程号
    print(p1.pid)
    print(p2.pid)
    
	print(f"主进程{os.getpid()},父进程:{os.getppid()}")
    # 在cmd中,输入tasklist,找到pycharm.ext就可以看到进程号
    

is_alive() 和 join()


def speak(name):
    print(f"现在{name}在说话")

def listen(name2):
    print(f"{name2}在听课")

if __name__ == "__main__":
    
    p1 = Process(target=speak, args=('九歌',))
    p2 = Process(target=listen, args=('李四',))
    
    p1.start()
    p1.join() #等待p1执行完,再执行后面的操作
    p2.start()
    
    
    print("p1的状态是:",p1.is_alive())
    print("p2的状态是:",p2.is_alive())
    
    
    

18.3.3 进程的通信

进程间不共享全局变量

import time
li = []
# 写入数据
def wdata():
    for i in range(5):
        li.append(i)
        time.sleep(0.2)
    print("写入的数据是",li)

# 读取数据
def rdata():
    print("读取的数据是:",li)

if __main__ == "__main__":
    p1 = Process(target=wdata)
    p2 = Process(target=rdata)
    
    p1.start()
    p1.join()
    p2.start()
    

进程的通信保证资源的传输

可以使用multiprocessing模块的Queue实现多进程之间的数据传输,Queue本身是一个消息队列程序

q.put() 放入数据

q.get() 取出数据


# 入队
q.put() #放入数据

# 出队
q.get() #取出数据

# 导入模块
from queue import Queue
# 初始化一个队列对象
q = Queue(3) #3表示最多可以接受3条消息
q.put('我今天去输液了,输的什么液,想你的夜')
q.put("你都不知道心疼人的")
q.put("被一个人牵动着情绪很烦,但也可以很甜蜜")

# get() #取出
print(q.get())
print(q.get())
print(q.get())


# q.empty() 判断队列是否为空,为空返回True,否则为False
# q.qsize() 队列中的数量
# q.full()  判断队列是否满了,满了返回True

print("目前的消息数量:",q.qsize())


# 通过队列来传递消息
from multiprocessing import Process,Queue
import time

li = ["蒸羊羔","蒸熊掌","蒸花鸭"]

# 写入数据
def wdata(q): #q表示队列对象
    for i in range(3):
        print(f"将早餐{i}放进去")
        q.put(i)
        time.sleep(0.2)

# 读取数据
def rdata(q): #q表示队列对象
    # 只要还有消息,就一直取出来
    while True:
        if q.empty(): #判断队列是否为空
           break  #跳出循环
        else:
            print("顾客从队列中获取到:",q.get())
        


if __name__ == "__main__":
    # 创建队列对象
    q =Queue()  #省略里面的参数,没有大小限制
    p1 = Process(target=wdata,args=(q,))
    p2 = Process(target=rdata,args=(q,))
    
    p1.start()
    p2.start()


18.3.4 进程池

把子进程放到进程池之中

进程池的概念

​ 定义一个池子,在里面放上固定数量的进程,有需求,就拿一个池子中的进程来处理任务

​ 处理完毕,进程并不关闭,而是将这个进程再放回池子中继续等待任务

方法

p.apply_async() 异步非阻塞,不用等待当前进程执行,随时根据系统调度来进行进程切换。

p.close() 关闭进程池

p.join() 主进程阻塞,等待所有工作进程退出,只能再close()后调用

from multiprocessing import Pool
import time

def work(a):
    print("我们在上课")
    time.sleep(2)
    return a * 3

if __name__ == "__main__":
    #定义一个进程池,最大进程数3
    p = Pool(3)
    
    li = []
    for i in range(6):
        #p.apply_async(调用的目标,传递的参数)
        res = p.apply_async(work,args=(i,)) #异步运行
    	# 异步:进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态
        
        li.append(res) #将结果进行保存
        print(res.get()) #打印结果
    
    # 关闭进程池
    p.close()
    # 等待p进程池中所有子进程执行完,必须放在close方法后面
    p.join()
    
    # 使用get来获取apply_async的结果
    for i in li:
        print(i.get())

      

18.4 协程

18.4.1 协程介绍

协程,又称为微线程,纤程。英文名Coroutine

协程是python中另外一个实现多任务的方式,只不过比线程占用更小执行单元(理解为需要的资源)。为啥说它是一个执行单元,因为它自带CPU上下文。这样只有在合适的时机,我们可以把一个协程切换到另一个协程。

只要这个过程中保存或恢复CPU上下文那么程序还是可以运行的。

协程:单线程下的并发,又称为微线程

对于协程来说,程序员就是上帝,你想让他执行到哪里,它就执行到哪里。

使用场景

  1. 如果一个线程里面io操作比较多,协程就比较适用
  2. 适合用于高并发处理

简单实现协程

import time

def work1():
    while True:
        yield '心疼哥哥'

def work2():
    while True:
        yield '互联网水太深,你把握不住'

if __name__ == "__main__":
    
    w1 = work1()
    w2 = work2()
    
    while True:
        print(next(w1))
        print(next(w2))
# 程序员可以简单的通过代码,控制w1和w2的执行顺序

18.4.2 greenlet

greenlet: 是一个用C实现的协程模块,通过设置switch()来实现任意函数之间的切换

属于手动切换,当遇到IO操作,程序会阻塞,而不能进行自动切换

安装命令 pip install greenlet

卸载命令 pip uninstall ungreenlet

查看已安装模块的命令 pip list


from greenlet import greenlet
def eat():
    print("开始吃夜宵")
    g2.switch() #切换到g2中运行,后面就不执行了
    print("吃饱了")

def study():
    print("开始学习")
	print("学习完毕")


# 实例化一个协程对象
# greenlet(任务名)
g1 = greenlet(eat)
g2 = greenlet(study)

g1.switch() # 切换到g1中运行


# 输出结果
# 开始吃夜宵
# 开始学习
# 学习完毕

18.4.3 gevent

greenlet是手动切换的,比较麻烦

gevent 则是自动切换的

gevent 遇到IO操作,会进行自动切换,属于主动式切换。

在gevent中用到的主要模式是greenlet

pip install gevent

import gevent

# 创建协程对象
# gevent.spawn(函数名) 
# join 阻塞,等待某个协程执行完毕
# joinall 参数是一个协程对象列表,会等待所有的协程都执行完毕再退出


执行A/B两个任务,当A、B遇到耗时操作,gevent会让A继续执行,同时也开始执行B任务

A完成了耗时操作后,B在对应的时间也完成了耗时操作

# 切记,py文件不要跟第三方模块、内置模块重名

import gevent
def write():
    print("我们在写写成代码")
    gevent.sleep(1)   #等待的同时,开启其他所有协程,让所有协程(g1,g2)并发执行
    print("终于写完了")

def listen():
    print("现在先好好听课把")
    gevent.sleep(1) 
    print("课间休息")
    
g1 = gevent.spawn(write)
g2 = gevent.spawn(listen)


g1.join() #等待g1对象执行结束
g2.join() #等待g2对象执行结束


# 运行结果
# 我们在写写成代码
# 现在先好好听课把
# 终于写完了
# 课间休息

joinall 需要等待所有的协程对象执行完,再退出


def work(name):
    for i in range(3):
        gevent.sleep(1) #等待的同时,开启其他所有协程,让所有协程并发执行
        print(f'函数名是:{name},i的值是:{i}')
        
gevent.joinall([
    gevent.spawn(work,'小白'),
    gevent.spawn(work,'小鹅'),
    
])


18.4.4 打补丁

给程序打补丁

monkey补丁,

猴子补丁的功能:

  1. 拥有在模块中替换的功能
from gevent import monkey
import gevent
import time


monkey.patch_all() # 将time.sleep()代码,替换成gevent.sleep()代码 。 必须写在最前面

def work(name):
    for i in range(3):
        # 将用到的耗时操作的代码,替换为gevent里面自己实现耗时操作的代码
        time.sleep(1)
        print(f'函数名是:{name},i的值是:{i}')

gevent.joinall([
    gevent.spawn(work,'小白'),
    gevent.spawn(work,'小鹅'),
    
])


18.4.5 综合例子

import gevent

def funa():
	print("wsc:今天有事情,跟孙yn打个电话")  #1
	gevent.sleep(2)
	print("wsc: 怎么突然挂电话了,再打过去...") #5
    
def funb():
	print("孙yn:wsc来电话啦。。。") #2
	gevent.sleep(3)
	print("孙yn: 他又打电话了")   #6

def func():
    print("亲爱的,你在干啥。。。") #3
    gevent.sleep(1)
    print("你过来呀")     #4

gevent.joinall([
    gevent.spawn(funa),
    gevent.spawn(funb),
    gevent.spawn(func)
])

# 输出结果
wsc:今天有事情,跟孙yn打个电话
孙yn:wsc来电话啦。。。
亲爱的,你在干啥。。。
你过来呀
wsc: 怎么突然挂电话了,再打过去...
孙yn: 他又打电话了

总结

  1. 进程是资源分配的基本单位,线程是CPU调度的基本单位
  2. 对比:
    1. 进程:切换需要的资源最大,效率比较低
    2. 线程:切换需要的资源一般,效率一般
    3. 协程:切换需要的资源很小,效率较高
  3. 多线程:适合IO密集型操作(读写数据比较多,比如爬虫)
  4. 多进程:适合cpu密集型操作(科学计算,计算圆周率,对视频进行高清解码)
  5. 一个运行的程序至少有一个进程,一个进程至少有一个线程。
上一篇:【Python】线程池套协程池的简单实现


下一篇:1313. 解压缩编码列表