python学习笔记:多并发(线程、进程、协程)

Python内置了threading模块以提供对多线程的支持,简单示例如下。

线程

多线程

定义一个需要2s才能执行完毕的函数,以线程的方式来同时运行两个这样的函数:

import threading,time

def func(n):
    print("{} running...this is func_{}".format(threading.current_thread(),n))

    time.sleep(2)
    print("{} over.this is func_{}".format(threading.current_thread(),n))
t1=threading.Thread(target=func,args=(1,))
t2=threading.Thread(target=func,args=(2,))

print("{}.program start.".format(threading.current_thread()))

t1.start()
t2.start()

print("{}.program over.".format(threading.current_thread()))

运行结果:

<_MainThread(MainThread, started 3640)>.program start.
<Thread(Thread-1, started 13052)> running...this is func_1
<Thread(Thread-2, started 10080)> running...this is func_2
<_MainThread(MainThread, started 3640)>.program over.
<Thread(Thread-1, started 13052)> over.this is func_1
<Thread(Thread-2, started 10080)> over.this is func_2

从运行结果中不难看出,多线程的程序会有一个主线程,即程序本身。而主线程与子线程之间的关系是并行的;并且主线程的运行不会被子线程所阻塞,因为主线程运行到”program over”时两个子线程都没有结束;但是注意到,在程序的最后,当主线程的任务完成时,整个程序必须等待所有子线程结束后它才会结束。

整个程序运行流程图如下所示:

join()

对于运行关键任务的子线程,主线程可能需要用到该子线程的运行结果,此时主线程与该子线程的关系不能再是完全的并行关系,主线程必须等待该子线程运行结束,然后主线程继续运行直至整个程序结束。考虑如下代码:

import threading,time

def func_1():
    print("func_1 running...")
    time.sleep(2)
    print("func_1 over.")

def func_2():
    print("func_2 running...")
    time.sleep(4)               #关键任务
    print("func_2 over.generate the result.")
t1=threading.Thread(target=func_1,args=())
t2=threading.Thread(target=func_2,args=())

print("program start.")

t1.start()
t2.start()
t2.join()           #主线程等待t2

print("program over.")

运行结果为:
program start.
func_1 running…
func_2 running…
func_1 over.
func_2 over.generate the result.
program over.

从运行结果不难看出,线程t1是完全独立且并行的,而主线程与t2虽然是不同的线程,但是两者的关系却是串行的,因为(假设)主线程的继续运行需要依赖于t2的运行结果,所以主线程需要等待t2的结束才能继续往下运行。线程的join()方法相当于将该线程加入到了主线程的流程中,如下图所示:

守护线程

假设有一个交互性程序,用户通过键盘与触摸操作来与程序互动。相比于在程序中不断循环检测是否有键盘操作或是触摸操作而言,为键盘操作与触摸操作分别设置一个线程用于监测事件要好得多,这即是所谓的守护线程。主线程没有等待守护线程结束的责任,反之,守护线程却有着维持主线程正常工作的义务。考虑如下代码:

import threading,time

def func_1():
    print("监测键盘事件...")
    while True:
        pass

def func_2():
    print("监测触摸事件...")
    while True:
        pass
t1=threading.Thread(target=func_1,args=())
t2=threading.Thread(target=func_2,args=())

print("program start.")

t1.setDaemon(True)          #设置守护进程
t1.start()
t2.setDaemon(True)
t2.start()

time.sleep(2)
print("program over.")

运行结果为:
program start.
监测键盘事件…
监测触摸事件…
program over.

从结果可以看出,当主线程任务完成时,守护进程也随之终止,整个程序退出,流程图如下所示:

GIL

CPython(Python解释器)在发展早期,为了多线程下的数据安全,加入了一个Global Interpreter Lock,这就导致CPython下的多线程只是一个伪实现,并不是真正的多线程。这就导致CPython并不适合完成计算密集型任务,而只适合去完成IO密集型任务。

可考虑的方案有两种,一是使用multiprocess替代threading,二是使用如JPython这样的解释器。两种方案各有优劣,前者是使用多进程,会增加进程间通信的时间;而后者在某些性能上不如CPython。

线程锁

对于关键数据或资源,在多线程下可能会出现一致性问题,为防止这种情况,可对关键资源进行操作的关键代码区进行加锁,在加锁的代码区内,只允许一个线程访问。

考虑如下情况,假设一个多线程程序要做复杂运算,其中部分运算牵涉到一个公共数据。用如下方法进行实现模拟:
线程中对公共数据的复杂运算,用+1操作并休眠1s来模拟;而线程中对公共数据的读取用print函数来模拟。于是有如下代码:

import threading,time

num=0

def complex_compute():
    global num
    num+=1
    time.sleep(1)

def func_add():
    complex_compute()        #对数据的运算
    print(num)        #读取数据查看是否正确
t1=threading.Thread(target=func_add,args=())
t2=threading.Thread(target=func_add,args=())

print("program start.")

t1.start()
t2.start()

print("program over.")

按照期望,线程t1读取(输出)的数据应该是1,t2是2,但是程序的实际输出为两者都是2。这就是线程同时对公共数据的操作所引起的一致性问题。

加入线程锁后的代码:

def func_add():
    lock.acquire()              #互斥区开始
    complex_compute()           #安全操作
    lock.release()              #互斥区结束
    print(num)

这样一来,程序的输出就正常了。

信号量

事件信号

用事件信号来模拟公路上红绿灯的事件:当信号灯为红灯时,所有车子都停止,当信号灯为绿灯时,车子才能前进。

import threading
import time

#事件实例
event=threading.Event()

#红绿灯
def RG_light():
    #计数器设零
    count=0

    while True:
        #2-3表示红灯
        if count>1 and count<4:
            print("[light]green!")
            event.set()
            count+=1

        #计数器值最大为4,此时代表黄灯
        elif count==4:
            print("[light]yellow!")
            event.clear()

            #计数器清零
            count=0

        #0,1表示红灯
        else:
            print("[light]red!")
            count+=1

        time.sleep(1)

def car():
    while True:

        #若事件标志位被设置则说明是绿灯
        if event.is_set():
            print("[car]running...")

        #否则是红灯
        else:
            print("[car]stop!")

            #等待事件标志
            event.wait()
            print("[car]ready to go!")
        time.sleep(0.5)

t1=threading.Thread(target=RG_light,args=())
t2=threading.Thread(target=car,args=())
t1.start()
t2.start()

截取部分程序输出:


线程级队列与栈

import queue

队列

队列简单示例:

q=queue.Queue(maxsize=10)

q.put(1)
q.put(2)

print(q.get())
print(q.get())

输出为1,2。

对于这种普通用法,如果队列为空时,再去进行get()操作时,程序会被阻塞,可以使用不等待的get_nowait()方法:

q=queue.Queue(maxsize=10)

q.put(1)
q.put(2)

while True:
    try:
        print(q.get_nowait())       #无等待地取元素
    except queue.Empty as e:
        print("queue empty!")
        exit(0)

输出为:

带优先级的队列

此种队列会根据插入元素的优先级对元素进行排队,优先级的数值越小,优先级越高。

#优先级队列
q=queue.PriorityQueue(maxsize=10)

q.put((3,"3"))
q.put((-5,"-5"))
q.put((0,"0"))

while True:
    try:
        print(q.get_nowait())       #无等待地取元素
    except queue.Empty as e:
        print("queue empty!")
        exit(0)

输出为:

python中还有一种后进先出队列LifoQueue,实际上就实现了一个栈,基本示例如下:

#栈
q=queue.LifoQueue(maxsize=10)

q.put(1)
q.put(2)

while True:
    try:
        print(q.get_nowait())       #无等待地取元素
    except queue.Empty as e:
        print("queue empty!")
        exit(0)

输出为:

生产者消费者模型

import queue
import time
import threading

q=queue.Queue(maxsize=10)

def Producer():
    id=0
    while True:
        q.put("good No.{}".format(id))
        id+=1
        time.sleep(1)

def Comsumer(name):
    while True:
        good=q.get()
        print("{} get {}".format(name,good))
        time.sleep(1)

producer=threading.Thread(target=Producer,args=())
comsumer_1=threading.Thread(target=Comsumer,args=("C1",))
comsumer_2=threading.Thread(target=Comsumer,args=("C2",))

producer.start()
comsumer_1.start()
comsumer_2.start()

部分输出:

进程

前一篇文章讲到,由于CPython解释器中存在GIL,所以CPython的多线程并不是真正意义上的多线程,这就导致了CPython解释器不适用于计算密集型任务,而只适用于IO密集型任务。有一种解决办法就是使用多进程替代多线程。

import multiprocessing

多进程

Python的多进程与多线程的使用方法几乎一致,一个简单示例如下:

import multiprocessing
import time
import os

def func(id):
    print("{} running...this is func_{}".format(os.getpid(),id))
    time.sleep(1)

if __name__=="__main__":
    for i in range(3):
        p=multiprocessing.Process(target=func,args=(i,))
        p.start()

输出为:

此处注意,同多线程情况一样,这里的21268、20604、13332是三个进程的进程ID,并且这三个ID具有相同的父进程3804,这个父进程就是pycharm。

进程间通信

队列形式

前文所讲述的队列结构是线程级别的队列,同一进程下的线程是共享一块内存区的,可以互相访问数据;但是不同进程间的内存区不是共享的,需要用手段来实现进程间通信。

线程间的数据访问

import threading
import queue

def func():
    data=q.get_nowait()
    print(data)

if __name__=='__main__':
    q=queue.Queue(maxsize=5)

    #主线程在队列中添加数据
    q.put("data from main process/thread")

    #子线程在队列中取数据
    t=threading.Thread(target=func,args=())

    t.start()
    t.join()

输出为:data from main process/thread

进程间的数据访问
上述代码对进程是不起作用的,会报错。那么是不是可以使用参数传递的方式,将一个进程中的队列传递给另一个进程去访问呢?考虑如下代码:

import multiprocessing
import queue

def func(que):
    data=que.get_nowait()
    print(data)

if __name__=='__main__':
    q=queue.Queue(maxsize=5)

    #主进程在队列中添加数据
    q.put("data from main process/thread")

    #子进程接受父进程的队列作为参数,并取数据
    t=multiprocessing.Process(target=func,args=(q,))

    t.start()
    t.join()

报错信息:TypeError: can’t pickle _thread.lock objects

如果一个进程要访问另一个进程队列里的数据,需要使用进程级队列,并且需要传递参数:

from multiprocessing import Process,Queue

def func(que):
    data=que.get_nowait()
    print(data)

if __name__=='__main__':
    q=Queue(maxsize=5)

    #主进程在队列中添加数据
    q.put("data from main process/thread")

    #子进程拥有父进程队列的副本,并取数据,子进程取完数据之后,父进程中的队列被清空
    t=Process(target=func,args=(q,))

    t.start()
    t.join()

输出为:data from main process/thread

这里实际上并不是子进程访问了父进程队列中的数据,而是在创建子进程时,父进程把队列复制了一份交给子进程。在子进程取到数据之后,由于数据要同步,所以父进程的队列也会被清空。

管道形式

进程间另一种通信形式就是管道。

from multiprocessing import Process,Pipe

def func(conn):
    conn.send("[C]hello")
    print(conn.recv())

    conn.close()

if __name__=='__main__':
    #创建一个管道对象,返回两个端连接
    p_conn,c_conn=Pipe()

    #将一端的连接传给子进程
    p=Process(target=func,args=(c_conn,))
    p.start()

    #开始通信
    print(p_conn.recv())
    p_conn.send("[P]hello")

    p.join()
    p_conn.close()

输出为:[C]hello [P]hello

管道通信类似网络编程中的socket,两端之间的收发也需要注意顺序与次数的问题,否则会造成程序的阻塞。

进程间数据共享

Python的multiprocessing模块中提供了一个Manager类,专门用于实现进程间数据共享。

from multiprocessing import Process,Manager
import os

#往字典与列表中写入PID
def func(d,l):
    d[os.getpid()]=os.getpid()
    l.append(os.getpid())

if __name__=='__main__':
    #Manager对象中包含了一些可供不同进程访问的数据类型
    manager=Manager()

    #创建进程级字典与列表
    _dict=manager.dict()
    _list=manager.list()

    p_list=[]
    for i in range(3):
        p=Process(target=func,args=(_dict,_list,))
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()

    print(_dict)
    print(_list)

输出为:

进程池

进程池限制了程序同时能够运行的进程数量,考虑如下代码:

from multiprocessing import Pool
import os
import time

def func(i):
    print("第{}个进程:{}".format(i,os.getpid()))
    time.sleep(1)

if __name__=='__main__':
    pool=Pool(processes=2)          #开设一个进程池,最多允许2个进程

    #从进程池中运行五个进程
    for i in range(5):
        pool.apply_async(func=func,args=(i,))

    pool.close()
    pool.join()         #等待进程池中的进程完毕

虽然程序在进程池中起了5次进程,但是因为进程池允许同时运行的进程数为2,所以进程是两个两个一组运行的,这可以从运行结果看出来。

带回调函数的进程池
在进程池中启动进程时,可以指定进程结束时调用的函数,称为回调函数。

from multiprocessing import Pool
import os
import time

def func():
    print("PID:{},PPID:{}".format(os.getpid(),os.getppid()))
    time.sleep(1)

def call_back(arg):
    print("进程结束,回调函数PID:{}".format(os.getpid()))

if __name__=='__main__':
    pool=Pool(processes=2)          #开设一个进程池,最多允许2个进程

    for i in range(2):
        pool.apply_async(func=func,args=(),callback=call_back)

    pool.close()
    pool.join()         #等待进程池中的进程完毕

输出为:

需要注意的是回调函数是由父进程调用的,可以从回调函数输出的PID看出来。

回调函数一个常用的用法为,在一个并行多任务系统下,当某任务触发IO操作时,系统就会切换任务,而当之前任务的IO操作结束时,调用回调函数,用于通知系统IO操作已结束,该任务可以继续运行。

协程

协程是比线程还要轻量、用户层面的微线程。协程可以在单线程中实现并发,通过用户管理的保存上下文操作来实现协程间的切换。Python的第三方库greenlet实现了对协程的支持,安装之后,一个简单的示例如下:

from greenlet import greenlet

def test1():
    print(12)
    gr2.switch()        #保存断点切换到gr2
    print(34)

def test2():
    print(56)
    gr1.switch()        #保存断点切换到gr1
    print(78)

if __name__=="__main__":
    #创建协程对象
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)

    gr1.switch()        #保存断点切换到gr1

    print("program over")

输出为:

greenlet中的协程切换是需要用户指定的,Python中还有一个三方库gevent,这个库实现了协程之间的自动切换。仔细考虑以下代码:

import gevent

def test1():
    print("test1 running...")
    gevent.sleep(0)         #触发切换
    print("test1 running...")

def test2():
    print("test2 running...")
    gevent.sleep(1)         #触发切换
    print("test2 running...")

def test3():
    print("test3 running...")
    gevent.sleep(0)         #触发切换
    print("test3 running...")

if __name__=="__main__":
    gevent.joinall(
        [gevent.spawn(test1),gevent.spawn(test2),gevent.spawn(test3)]
    )

    print("program over")

输出为:

当协程触发切换操作时,gevent会自动寻找无需等待的协程运行,上述代码的运行过程大致如下:

  1. 程序运行协程test1()
  2. test1()切换到test2()
  3. test2()切换到test3()
  4. test3()切换到test1()
  5. test1()结束,切换到test2()
  6. test2()仍为等待状态,切换到test3()
  7. test3()结束,切换到test2()
  8. test2()等待1s后,运行结束

协程的用法是在单个线程中实现多并发,并且当某个协程触发IO操作时或者用户显示指定时进行协程的切换。上述代码中的gevent.sleep(0)可认为是协程IO的模拟,下面代码就粗略对比了循环方式与协程方式获取网页的速度:

import gevent
from urllib.request import urlopen
import time

from gevent import monkey       #补丁,使模块能响应各种阻塞
monkey.patch_all()

url_list=[
    "https://www.python.org/",
    "https://www.yahoo.com/",
    "https://www.github.com/"
]

def get_page(url):
    res=urlopen(url)
    print("GET:{}".format(url))

if __name__=="__main__":
    tic = time.time()
    for url in url_list:
        get_page(url)
    toc = time.time()
    print("循环耗时:{}".format(toc - tic))

    tic=time.time()
    gevent.joinall(
        [gevent.spawn(get_page,url_list[0]),
         gevent.spawn(get_page,url_list[1]),
         gevent.spawn(get_page,url_list[2])]
    )
    toc=time.time()
    print("协程耗时:{}".format(toc-tic))

输出为:

猜你喜欢

转载自blog.csdn.net/qq_31823267/article/details/78872935
今日推荐