Python线程&进程&协程

发布于:2025-03-01 ⋅ 阅读:(10) ⋅ 点赞:(0)

1.多任务

1.1多任务的执行方式

并发:指两个或多个事件在同一时间段内发生。指的是任务数多于cpu核数时,通过操作系统的各种任务调用算法,来实现多个任务“一起”(看上去是一起)执行的效果。

并行:指两个或多个事件在同一时刻发生(同时发生)。指的是任务数小于cpu核数时真的一起执行。

1.2进程与线程

进程打开一个程序就至少会有一个进程

一个正在运行的程序或者是一个软件就是一个进程,他是操作系统进行资源分配的基本单位。

一个进程默认有一个线程,进程里可以创建多个线程,线程是依附在进程里面的,没有进程就没有线程。

程序启动时默认会有一个主线程(因为线程是程序执行的最小单位)

程序员自己创建的线程可以称为子线程,多线程可以完成多任务

2.线程

2.1单线程

单线程示例:

import time
def speak():
    print('hello world')
    time.sleep(2)    #睡眠2s
    print('你好,世界')

def play():
    print('劳逸结合')
    time.sleep(2)
    print('多做运动')

if __name__ == '__main__':
    speak()
    play()

__name__  == '__main__'表示如果程序文件直接运行,则执行以下代码 

1.防止别人导入文件的时候执行main里面的方法

2.防止wiondows系统递归创建子进程

2.2多线程创建

使用threading模块里的Thread类创建出示例对象,然后通过start()方法真正的去产生一个新的线程

程序启动会默认有一个主线程

通过start()方法来启动子线程

注意:导入模块时可以导入import threading模块

           创建线程时需要用threading.Thread()

也可以导入from threading import Thread类,创建线程时,直接使用Thread()

2.2.1线程类Thread的参数

group:线程组

target:执行目标的任务名  (也就是函数名)

args:以元组的方式给执行任务进行传参

*args:传任意多个参数

kwargs:以字典方式给执行任务传参

name:线程名

2.2.2步骤

1.导入模块 threading

2.创建子线程 Thread()类

3.守护线程 setDaemon  --设置守护线程,主线程执行完了,子线程也会跟着结束

4.启动子线程 start()

5.阻塞主线程 join()a.join--a是子线程,阻塞主线程,主线程会等待子线程a执行完在执行下面的代码

2.2.3多线程的优势

单线程实现:

import time
t1 = time.time()
def sing(name):
    print('%s在唱歌' % name)
    time.sleep(1)    #睡眠2s


if __name__ == '__main__':
    for i in range(4):
        sing('panda')

t2 = time.time()
print('执行时间是',t2-t1)   #执行时间是 4.042494535446167

多线程实现:多个线程同时进行,减少运行时间

import time
import threading    #创建多线程
t1 = time.time()
def sing(name):
    print('%s在唱歌' % name)
    time.sleep(1)    #睡眠2s


if __name__ == '__main__':
    for i in range(4):
        t = threading.Thread(target=sing,args=('panda',))  #一个元素的元组要加,
        t.start()   #启动线程

t2 = time.time()
print('执行时间是',t2-t1)   #0.0010912418365478516

2.2.4多线程的应用

import time
from threading import Thread

def speak():
    print('hello world')
    time.sleep(2)    #睡眠2s
    print('你好,世界')

def play():
    print('劳逸结合')
    time.sleep(2)
    print('多做运动')

if __name__ == '__main__':
    #创建子线程
    f1 = Thread(target=speak)
    f2 = Thread(target=play)

    #设置守护线程
    f1.setDaemon(True)
    f2.setDaemon(True)

    #开启子线程
    f1.start()
    f2.start()

    print("这是主线程")

这里我们可以发现很多想要的数据没有打印出来,因为这些线程同时进行,设置了守护线程,主线程结束,子线程也结束,停留了两秒才输出的就打印不出来,我们就能用join()方法来阻塞主线程

import time
from threading import Thread

def speak():
    print('hello world')
    time.sleep(2)    #睡眠2s
    print('你好,世界')

def play():
    print('劳逸结合')
    time.sleep(2)
    print('多做运动')

if __name__ == '__main__':
    #创建子线程
    f1 = Thread(target=speak)
    f2 = Thread(target=play)

    #设置守护线程
    f1.setDaemon(True)
    f2.setDaemon(True)

    #开启子线程
    f1.start()
    f2.start()

    #阻塞主线程
    f1.join()    #主线程等f1执行完在执行
    f2.join()    #f2也进行设置所以f1和f2是同时执行的

    #修改线程名字
    f1.setName('线程一')
    f2.setName('线程二')

    #获取名字
    print(f1.getName())
    print(f2.getName())

    print("这是主线程")

2.2.5多线程的意义及应用场景

多线程并不是多个线程并发在同一时间点运行,而是cpu有计划的交替执行多个线程

磁盘网络为主的程序 IO密集型,多线程的使用有很大的优势

计算为主的程序 cpu密集型程序,不建议使用多线程操作

2.3线程执行代码的封装

1.继承Thread

2.重构run方法

启动线程start会调用run方法

import time
from threading import Thread
class MyThread(Thread):  #定义一个新子类来继承Thread类
    def run(self) -> None:  #重构run方法,规定run这个名字表示线程活动的方法
        print('panda')
        time.sleep(2)
        print('play')

if __name__ == '__main__':
    #创建实例线程
    t = MyThread()
    t.start()   #panda   play

start和run方法的区别:

start方法是声明分到一个子线程的函数已经就绪,等待被cpu执行

run方法是执行这个线程,自动调用的方法

from threading import Thread
import threading

class MyThread(Thread):  #定义一个新子类来继承Thread类
    def run(self) -> None:  #重构run方法,规定run这个名字表示线程活动的方法
        print(f"当前线程的名字是{threading.current_thread().name}")

if __name__ == '__main__':
    #创建实例线程
    t1 = MyThread()
    t2 = MyThread()

    #启动线程
    t1.start()  #当前线程的名字是Thread-1
    t2.start()  #当前线程的名字是Thread-2

    #run方法启动
    t1.run()    #当前线程的名字是MainThread
    t2.run()    #当前线程的名字是MainThread

2.4线程执行是无序的

他是由cpu调用来决定的,cpu调用哪个线程,哪个线程就先执行,没有调用的线程就不执行

import time
from threading import Thread
import threading


def task():
    time.sleep(1)
    print(f"当前线程的名字是{threading.current_thread().name}\n",end='')

if __name__ == '__main__':
    for i in range(5):
        t = Thread(target = task)
        t.start()

线程美化,手动添加换行

2.5线程之间资源共享(全局变量)

import time
from threading import Thread

li = []
#写入数据
def wdate():
    for i in range(5):
        li.append(i)
        time.sleep(1)
    print("写入的数据是:",li)
#读取数据
def rdate():
    print("读取的数据是:",li)

if __name__ == '__main__':
    #创建子线程
    t1 = Thread(target=wdate)
    t2 = Thread(target=rdate)

    #开启子线程
    t1.start()
    t2.start()

输出结果:读取的数据是[0]

                  写入的数据是[0,1,2,3,4]

这里有两种解决办法能读到完整的数据:

1.阻塞线程,加入join等t1任务执行结束,在继续主线程的运行(建议使用)

import time
from threading import Thread

li = []
#写入数据
def wdate():
    for i in range(5):
        li.append(i)
        time.sleep(1)
    print("写入的数据是:",li)
#读取数据
def rdate():
    print("读取的数据是:",li)

if __name__ == '__main__':
    #创建子线程
    t1 = Thread(target=wdate)
    t2 = Thread(target=rdate)

    #开启子线程
    t1.start()
    #阻塞线程
    t1.join()
    t2.start()

2.在t1开始后t2开始前,是时间暂停一会,到t1运行完,在执行

import time
from threading import Thread

li = []
#写入数据
def wdate():
    for i in range(5):
        li.append(i)
        time.sleep(1)
    print("写入的数据是:",li)
#读取数据
def rdate():
    print("读取的数据是:",li)

if __name__ == '__main__':
    #创建子线程
    t1 = Thread(target=wdate)
    t2 = Thread(target=rdate)

    #开启子线程
    t1.start()
    #休眠
    time.sleep(5)
    t2.start()

2.6资源竞争

a=0
b=1000000

#循环一次就给全局变量a+1
def add():
    for i in range(b):
        global a
        a += 1
    print("第一次",a)
def add2():
    for i in range(b):
        global a
        a += 1
    print("第二次",a)
add()
add2()

 

from threading import Thread
a=0
b=1000000

#循环一次就给全局变量a+1
def add():
    for i in range(b):
        global a
        a += 1
    print("第一次",a)
def add2():
    for i in range(b):
        global a
        a += 1
    print("第二次",a)
# add()
# add2()
if __name__ == '__main__':
    t1 = Thread(target=add)
    t2 = Thread(target=add2)
    t1.start()
    t2.start()

放到一个多线程中就会造成资源竞争(当数量一定大时)

2.7线程同步

两种方式:join和互斥锁

2.7.1.join:等待第一个子线程执行完成之后,代码再继续往下执行,开始执行第二个子线程

from threading import Thread
a = 0
b = 1000000

#循环一次就给全局变量a+1
def add():
    for i in range(b):
        global a
        a += 1
    print("第一次",a)
def add2():
    for i in range(b):
        global a
        a += 1
    print("第二次",a)
# add()
# add2()
if __name__ == '__main__':
    t1 = Thread(target=add)
    t2 = Thread(target=add2)
    t1.start()
    t1.join()
    t2.start()

2.7.2.互斥锁

概念:对共享数据进行锁定,保证多个线程访问共享数据不会出现错误问题:保证同一时刻只能有一个线程去操作

方法:

acquire():上锁

release():释放锁

注意:这两个方法是成对出现的,否则容易形成死锁(死锁:一直等待对方释放锁的情景,死锁会造成应用程序停止响应,不在处理其他任务)

导入模块,也是导入threading模块,导入的是Lock类

from threading import Thread,Lock
a = 0
b = 1000000

#创建互斥锁
lock = Lock()
#循环一次就给全局变量a+1
def add():
    lock.acquire()     #上锁
    for i in range(b):
        global a
        a += 1
    print("第一次",a)
    lock.release()     #释放锁
def add2():
    lock.acquire()  # 上锁
    for i in range(b):
        global a
        a += 1
    print("第二次",a)
    lock.release()  # 释放锁
# add()
# add2()
if __name__ == '__main__':
    t1 = Thread(target=add)
    t2 = Thread(target=add2)
    t1.start()
    #t1.join()
    t2.start()

注意:互斥锁时多个线程一起去抢,抢到锁的线程先执行

3.进程

3.1.含义:是操作系统进行资源分配和调度的基本单位,是操作系统结构的基础

一个正在运行的程序或者一个软件就是一个进程

程序跑起来就是进程

注意:进程里面可以创建多个线程,多进程可以完成多任务

3.2进程的状态

1.就绪状态:运行条件都已经满足,正在等待cpu执行

2.执行状态:cpu正在执行其功能

3.等待(阻塞)状态:等待某些条件满足,如果一个程序sleep了,此时就处于等待状态

3.3进程的语法结构

multiprocessing模块提供了Process类代表进程对象

3.3.1Process参数

1.target:执行目标任务名,即子进程要执行的任务

2.args:以元组的形式传参

3.kwargs:以字典的形式传参

3.3.2常用的方法

1.start():开启子进程

2.is_alive():判断子进程是否存活,存活返回True,死亡返回False

3.join():等待子进程执行结束

3.3.3常用的属性

name:当前进程的别名。默认Process-N

pid:当前进程的进程编号

在进程中获取进程名只需要 (对象名).name,在线程中需要用getName

1.修改进程名的第一种方法,传参的时候修改

from multiprocessing import Process

def sing():
    print("唱歌")
def dance():
    print("跳舞")
if __name__ == '__main__':
    #创建子进程
    p1 = Process(target=sing)  #修改进程名的第一种方法
    p2 = Process(target=dance,name='子进程二')
    #开启
    p1.start()
    p2.start()

    #访问进程名
    print(p1.name)   #Process-1
    print(p2.name)   #子进程二

2.修改进程名的第二种方法,设立对象后,直接进行修改

from multiprocessing import Process

def sing():
    print("唱歌")
def dance():
    print("跳舞")
if __name__ == '__main__':
    #创建子进程
    p1 = Process(target=sing)
    p2 = Process(target=dance)
    #开启
    p1.start()
    p2.start()

    #修改进程名
    p1.name = '子进程1'
    p2.name = '子进程2'

    #访问进程名
    print(p1.name)   #子进程1
    print(p2.name)   #子进程2

获取进程编号:导入os模块可以获得主进程的进程编号

os.getpid:获取当前进程的进程编号

os.getppid:获取当前进程的父进程编号

子进程的父进程的pid就是当前py文件主进程的pid

当前py文件主进程的父进程的pid就是pycharme软件中的进程编号

from multiprocessing import Process
import os

def sing():
    os.getpid()   #获取当前进程的进程编号
    print(f"sing子进程编号{os.getpid()},父进程编号{os.getppid()}")  #父进程的pid就是py文件主进程的pid
    print("唱歌")
def dance():
    os.getpid()
    print(f"dance子进程编号{os.getpid()},父进程编号{os.getppid()}")
    print("跳舞")
if __name__ == '__main__':
    #创建子进程
    p1 = Process(target=sing)
    p2 = Process(target=dance)
    #开启
    p1.start()
    p2.start()

    #访问进程名
    print(p1.name)
    print(p2.name)

    #查看子进程的进程编号
    print(f"p1子进程编号{p1.pid}")
    print(f"p2子进程编号{p2.pid}")
    print(f"主进程pid{os.getpid()} 父进程pid{os.getppid()}")

 

查看pycharme软件中的进程编号

cmd命令提示符窗口输入tasklist可以查看电脑里面的进程命令

Ctrl+F查找

pycharm64软件的编号就是主进程的父进程的pid

process方法的进一步使用

from multiprocessing import Process

def sing(name):
    print(f"{name}在唱歌")
def dance(name):
    print(f"{name}在跳舞")
if __name__ == '__main__':
    #创建子进程
    p1 = Process(target=sing,args=('panda',))
    p2 = Process(target=dance,args=('monkey',))

    p1.start()
    
    p2.start()
    print('p1的存活状态',p1.is_alive())   #True
    print('p2的存活状态',p2.is_alive())   #True

存活是因为主进程先执行再执行子进程,所以在主进程中判断是否存活的时候,紫禁城还没有运行完。

使用完毕后需要使子进程死亡,写在主进程中判断存活状态的时候需要加入join阻塞一下

加入join,主进程处于等待状态,子进程处于运行状态

from multiprocessing import Process

def sing(name):
    print(f"{name}在唱歌")
def dance(name):
    print(f"{name}在跳舞")
if __name__ == '__main__':
    #创建子进程
    p1 = Process(target=sing,args=('panda',))
    p2 = Process(target=dance,args=('monkey',))

    p1.start()
    p1.join()   #这一步执行完的时候相当于p1已经执行完了(在主进程中,所以是死亡)
    p2.start()
    p2.join()
    print('p1的存活状态',p1.is_alive())   #False
    print('p2的存活状态',p2.is_alive())   #False

3.4进程间不共享全局变量

import time
from multiprocessing import Process

li = []
#写入数据
def wdate():
    for i in range(5):
        li.append(i)
        time.sleep(1)
    print("写入的数据是:",li)
#读取数据
def rdate():
    print("读取的数据是:",li)

if __name__ == '__main__':
    #创建子进程
    p1 = Process(target=wdate)
    p2 = Process(target=rdate)

    #开启子进程
    p1.start()
    p2.start()

加入join方法读取数据输出的结果也始终为空

3.5进程间的通信

Queue(队列)

q.put():放入数据

q.get():取出数据,获取队列中的一条消息,然后将他从队列中移除

q.empty():判断队列是否为空

q.qsize():返回当前队列包含的消息数量

q.full():判断队列是否满了

使用时需要导入模块from queue import Queue

from queue import Queue

q = Queue(3)  #最多可以三条消息,没有写或者负值就代表没有上线,直到内存尽头
q.put("我爱世界")
q.put("我爱python")
print(q.qsize())   #2
print(q.full())    #False
q.put("我爱中国")
print(q.full())    #True
print(q.qsize())   #3
print(q.get())
print(q.get())
print(q.empty())   #Flase
print(q.get())
print(q.empty())   #True

可以借助队列完成导入和取出数据的结合

import time
from multiprocessing import Process
from queue import Queue

li = ['panda','monkey','bear']
#写入数据
def wdate(q1):
    for i in range(5):
        print(f'{i}已经被放入')
        q1.put(i)
        time.sleep(0.2)
    print("写入的数据是:",li)
#读取数据
def rdate(q2):
    while True:
        #判断队列是否为空,为空就退出循环
        if q2.empty():
            break
        else:
            print("输出数据",q2.get())
    print("读取的数据是:",li)

if __name__ == '__main__':

    #创建队列对象
    q = Queue()
    #创建子进程
    p1 = Process(target=wdate,args=(q,))
    p2 = Process(target=rdate,args=(q,))

    #开启子进程
    p1.start()
    p1.join()
    p2.start()

4.协程 Coroutine

协程,单线程下的开发,又称微线程,协程时Python中另外一种实现多任务的方式,只不过比线程更小,占用更小的执行单元(理解为需要的资源)。它自带cpu上下文。这样只要在合适的时机,我们可以把一个协程切换到另一个协程。只要这个过程中保存和恢复cpu上下文,那么程序还是可以运行的

注意:线程和进程的操作是由程序触发系统接口,最后的执行者是系统,协程的操作则是程序员

4.1简单实现协程(控制操作时间来控制输出顺序,或者控制代码顺序来控制输出顺序)

import time
def task1():
    while True:
        yield 'welcome to python'
        time.sleep(1)
def task2():
    while True:
        yield 'nice to meet you'
        time.sleep(2)
if __name__ == '__main__':
    t1 = task1()
    t2 = task2()

    while True:
        print(next(t1))
        print(next(t2))

4.2应用场景

1.如果一个线程里IO(Input/Outout)操作比较多的时候,可以用协程,常见的IO操作:文件操作,网络请求(爬虫)

2.适合高并发处理

4.3greenlet

使用时也需要导入greenlet模块

from greenlet import greenlet

4.3.1greenlet是一个由C语言实现的协程模块。通过设置switch()来实现任意函数之间的转换

为了更好的使用协程来完成任务,Python中的greenlet模块对其封装,从而使得切换任务变得更简单

安装命令:pip install greenlet

卸载:pip install 模块名

查看已安装模块pip list

如果python解释器安装版本过多或者写在不够彻底可能会产生冲突,这时候就需要在前面加上python -m

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

4.3.3通过greenlet实现任务间的切换

from greenlet import greenlet
def sing():
    print("在唱歌")
    g2.switch()
    print("唱完歌了")
def dance():
    print("在跳舞")
    print("跳完舞了")
if __name__ == '__main__':
    #创建协程对象
    g1 = greenlet(sing)
    g2 = greenlet(dance)
    g1.switch()

from greenlet import greenlet
def sing():
    print("在唱歌")
    g2.switch()
    print("唱完歌了")
def dance():
    print("在跳舞")
    g1.switch()
    print("跳完舞了")
if __name__ == '__main__':
    #创建协程对象
    g1 = greenlet(sing)
    g2 = greenlet(dance)
    g1.switch()
    g2.switch()

同一条switch语句跳转过的就不再将执行

4.4gevent

使用时也需要先安装,与上一步一样

greenlet实现了协程,但是需要人工切换,gevent是比greenlet更强大而且能够自动切换的第三方库。属于主动切换

使用时需要导入gevent模块

import gevent

4.4.1使用

gevent.spawn(函数名):创建协程对象

gevent.sleep():             耗时操作

gevent.join():                阻塞,等待某个协程执行结束

gevent.joinall():            等待所有协程对象都执行结束在退出,参数是一个协程对象列表

4.4.2grvent自带耗时操作 

import gevent
import time
def sing():
    print("在唱歌")
    gevent.sleep(2)
    print("唱完歌了")
def dance():
    print("在跳舞")
    gevent.sleep(3)
    print("跳完舞了")
if __name__ == '__main__':
    #创建协程对象
    g1 = gevent.spawn(sing)
    g2 = gevent.spawn(dance)

    #阻塞,等待协程执行结束
    g1.join()
    g2.join()

gevent.sleep()模拟了gevent实现的io阻塞,使在唱歌和在跳舞同时执行,如果用time.sleep就会执行完sing函数中的语句,在执行dance函数中的语句(本来加入阻塞应该等待协程执行结束,在跳转,实现了自动跳转)

4.4.3joinall()

import gevent
import time
def sing(name):
    for i in range(3):
        gevent.sleep(1)
        print(f"{name}在唱歌,被送走的第{i}次")
if __name__ == '__main__':
    gevent.joinall([gevent.spawn(sing,'panda'),gevent.spawn(sing,'monkey')])

gevent.sleep,进行任务切换,实现了并发的效果。

输出结果:

没有gevent,sleep的输出结果:

4.4.4monkey补丁:拥有在运行模块时替换的功能

使用时需要导入模块from gevent import monkey

monkey.patch_all() 是将time.sleep()代码替换成gevent里面自己实现耗时操作的gevent.sleep()代码(需要放在被打补丁者前)

import gevent
import time
from gevent import monkey

monkey.patch_all()
def sing(name):
    for i in range(3):
        time.sleep(1)
        print(f"{name}在唱歌,被送走的第{i}次")
if __name__ == '__main__':
    gevent.joinall([gevent.spawn(sing,'panda'),gevent.spawn(sing,'monkey')])

5.总结

5.1线程是cpu调度的基本单位,进程是资源分配的基本单位

5.2进程,线程,协程对比

进程:需要切换的资源最大,效率最低

线程:切换需要的资源一般,效率一般

协程:切换需要的资源最小,效率最高

5.3多线程适合IO密集型操作(文件操作,爬虫),多进程适合cpu密集型操作(科学及计算,对视频进行高清解码,计算圆周率)

5.4进程线程、协程都是可以完成多任务的,可以根据自己实际开发需要选择使用。


网站公告

今日签到

点亮在社区的每一天
去签到