第十四章 python 并发编程

python 并发编程

  • 一、线程概述
      • 1. 线程和进程
      • 2. 多线程的优势
  • 二、线程的创建和启动
      • 1. 调用Thread类的构造器创建线程
      • 2. 继承Thread类创建线程类
  • 三、线程的生命周期
      • 1.新建和就绪状态
      • 2. 运行和阻塞状态
      • 3. 线程死亡
  • 四、控制线程
      • 1. join
      • 2. 后台线程
      • 3. 线程睡眠:sleeps
  • 五、线程同步
      • 1. 线程安全问题
      • 2. 同步锁(lock)
      • 3. 死锁
  • 六、线程通信
      • 1. 使用Condition实现线程通信
      • 2. 使用队列(Queue)控制线程通信
      • 3. 使用Event控制线程通道
  • 七、线程池
      • 1. 使用线程池
      • 2. 获取执行结果
  • 八、线程相关类
      • 1. 线程局部变量
      • 2. 定时器(执行一次)
      • 3. 任务调度
  • 九、多进程
      • 1. 使用fork创建新进程
      • 2. 使用multiprocessing.Process创建新进程
        • (1)以指定函数作为target创建新进程
        • (2)继承Process类创建子进程
      • 3. Context和启动进程的方式
      • 4. 使用进程池管理进程
      • 5. 进程通信
        • (1)使用Queue实现进程通信
        • (2)使用Pipe实现进程通信

多线程程序则可以包含多个顺序执行流,这些顺序执行流之间互不干扰。

一、线程概述

1. 线程和进程

进程包含如下三个特征。

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己的独立的资源,每一个进程都拥有自己的私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,在程序中是没有这些概念的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

线程(Thread)也被称作轻量级进程(LightweightProcess),线程是进程的执行单元。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。

一个程序运行后至少有一个进程,在一个进程中可以包含多个线程,但至少要包含一个主线程。

操作系统可以同时执行多个任务,每一个任务就是一个进程;进程可以同时执行多个任务,每一个任务就是一个线程。

2. 多线程的优势

线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性——多个线程共享同一个进程的虚拟空间。线程共享的环境包括进程代码段、进程的公有数据等,利用这些共享的数据,线程之间很容易实现通信。

使用多线程编程具有如下几个优点。
进程之间不能共享内存,但线程之间共享内存非常容易。
操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。
因此,使用多线程来实现多任务并发执行比使用多进程的效率高。
Python语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简
化了 Python的多线程编程。

二、线程的创建和启动

Python提供了_thread和threading两个模块来支持多线程。

Python主要通过两种方式来创建线程。

  • 使用threading模块的Thread类的构造器创建线程。
  • 继承threading模块的Thread类创建线程类。

1. 调用Thread类的构造器创建线程

调用Thread类的构造器创建线程很简单,直接调用threading.Thread类的如下构造器创建线程。
_ init _ (self, group=None, target=None, name=None, args=(), kwargs=None, *,
daemon=None)

上面的构造器涉及如下几个参数。

  • group:指定该线程所属的线程组。目前该参数还未实现,因此它只能设为None。
  • target:指定该线程要调度的目标方法。
  • args:指定一个元组,以位置参数的形式为target指定的函数传入参数。元组的第一个元素传给target函数的第一个参数,元组的第二个元素传给target函数的第二个参数……依此类推。
  • kwargs:指定一个字典,以关键字参数的形式为target指定的函数传入参数。
  • daemon:指定所构建的线程是否为后代线程。

通过Thread类的构造器创建并启动多线程的步骤如下。
①调用Thread类的构造器创建线程对象。在创建线程对象时,target参数指定的函数将作为线程执行体。
②调用线程对象的start。方法启动该线程。

代码如下:

import threading

# 定义一个普通的action函数,该函数准备作为线程执行体
def action(max):
    for i in range(max):
        # 调用threading模块current_thread()函数获取当前线程
        # 线程对象的getName()方法获取当前线程的名字
        print(threading.current_thread().getName() +  " " + str(i))
# 下面是主程序(也就是主线程的执行体)
for i in range(100):
    # 调用threading模块current_thread()函数获取当前线程
    print(threading.current_thread().getName() +  " " + str(i))
    if i == 20:
        # 创建并启动第一个线程
        t1 =threading.Thread(target=action,args=(100,))
        t1.start()
        # 创建并启动第二个线程
        t2 =threading.Thread(target=action,args=(100,))
        t2.start()
print('主线程执行完成!')

除此之外,上面程序还用到了如下函数和方法。 .

  • threadmg.current_thread():它是threading模块的函数,该函数总是返回当前正在执行的线程对象。
  • getName():它是Thread类的实例方法,该方法返回调用它的线程名字。

程序可以通过setName(name)方法为线程设置名字,也可以通过getName()方法返
回指定线程的名字,这两个方法可通过name属性来代替。在默认情况下,主线程的名
字为MainThread,用户启动的多个线程的名字依次为Thread-1、Thread-2、Thread-3、
Thread-n 等。

2. 继承Thread类创建线程类

通过继承Thread类来创建并启动线程的步骤如下。
①定义Thread类的子类,并重写该类的run()方法。run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
②创建Thread子类的实例,即创建线程对象。
③调用线程对象的start。方法来启动线程。

代码如下:

import threading

# 通过继承threading.Thread类来创建线程类
class FkThread(threading.Thread):
    def __init__(self):  
        threading.Thread.__init__(self)
        self.i = 0
    # 重写run()方法作为线程执行体
    def run(self):  
        while self.i < 100:
            # 调用threading模块current_thread()函数获取当前线程
            # 线程对象的getName()方法获取当前线程的名字
            print(threading.current_thread().getName() +  " " + str(self.i))
            self.i += 1
# 下面是主程序(也就是主线程的执行体)
for i in range(100):
    # 调用threading模块current_thread()函数获取当前线程
    print(threading.current_thread().getName() +  " " + str(i))
    if i == 20:
        # 创建并启动第一个线程
        ft1 = FkThread()
        ft1.start()
        # 创建并启动第二个线程
        ft2 = FkThread()
        ft2.start()
print('主线程执行完成!')

三、线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入执行状态的,也不是一直处于执行状态的,在线程的生命周期中,,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead) 5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多个线程之间切换,于是线程状态也会多次在运行、就绪之间转换。

1.新建和就绪状态

当程序创建了一个Thread对象或Thread子类的对象之后,该线程就处于新建状态,和其他的Python对象一样,此时的线程对象并没有表现出任何线程的动态特征,程序也不会执行线程执行体。
当线程对象调用start。方法之后,该线程处于就绪状态,Python解释器会为其创建方法调用栈和程序计数器,处于这种状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于Python解释器中线程调度器的调度。

启动线程使用start()方法,而不是run()方法!永远不要调用线程对象的run()方 :
法!调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如 :
果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在该方法返回 :
之前其他线程无法并发执行——也就是说,如果直接调用线程对象的run()方法,则 :
系统把线程对象当成一个普通对象,而nm()方法也是一个普通方法,而不是线程执 混4
行体,

代码如下:

import threading

# 定义准备作为线程执行体的action函数
def action(max):
    for i in range(max):
        # 直接调用run()方法时,Thread的name属性返回的是该对象的名字
        # 而不是当前线程的名字
        # 使用threading.current_thread().name总是获取当前线程的名字
        print(threading.current_thread().name +  " " + str(i))  # ①
for i in range(100):
    # 调用Thread的currentThread()方法获取当前线程
    print(threading.current_thread().name +  " " + str(i))
    if i == 20:
        # 直接调用线程对象的run()方法
        # 系统会把线程对象当成普通对象,把run()方法当成普通方法
        # 所以下面两行代码并不会启动两个线程,而是依次执行两个run()方法
        threading.Thread(target=action,args=(100,)).run()
        threading.Thread(target=action,args=(100,)).run()

在调用线程对象的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。

只能对处于新建状态的线程调用start()方法。也就是说,如果程序对同一个线程
重复调用start。方法,将引发RuntimeError异常.

在调用线程对象的start。方法之后,该线程立即进入就绪状态——相当于“等待执行”,但该线程并未真正进入运行状态。

2. 运行和阻塞状态

当一个线程调用了它的sleep()或yield()方法后才会放弃其所占用的资源——也就是必须由该线程主动放弃其所占用的资源。

当发生如下情况时,线程将会进入阻塞状态。
>线程调用sleep()方法主动放弃其所占用的处理器资源。
>线程调用了一个阻塞式I/O方法,在该方法返回之前,该线程被阻塞。
>线程试图获得一个锁对象,但该锁对象正被其他线程所持有。关于锁对象的知识,后面将有更深入的介绍。
>线程在等待某个通知(Notify)。

针对上面几种情况,当发生如下特定的情况时可以解除阻塞,让该线程重新进入就绪态。

  • 调用sleep。方法的线程经过了指定的时间。
  • 线程调用的阻塞式I/O方法已经返回。
  • 线程成功地获得了试图获取的锁对象。
  • 线程正在等待某个通知时,其他线程发出了一个通知。

3. 线程死亡

线程会以如下三种方式结束,结束后就处于死亡状态。

  • run()方法或代表线程执行体的target函数执行完成,线程正常结束。
  • 线程抛出一•个未捕获的Exception或Error。

当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。

为了测试某个线程是否已经死亡,可以调用线程对象的is_alive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回True;当线程处于新建、死亡两种状态时,该方法将返回False。

代码如下:

import threading

# 定义action函数准备作为线程执行体使用
def action(max):
    for  i in range(100):
        print(threading.current_thread().name +  " " + str(i))
# 创建线程对象
sd = threading.Thread(target=action, args=(100,))
for i in range(300):
    # 调用threading.current_thread()函数获取当前线程
    print(threading.current_thread().name +  " " + str(i))
    if i == 20:
        # 启动线程
        sd.start()
        # 判断启动后线程的is_alive()值,输出True
        print(sd.is_alive())
    # 当线程处于新建、死亡两种状态时,is_alive()方法返回False
    # 当i > 20时,该线程肯定已经启动过了,如果sd.is_alive()为False时
    # 那就是死亡状态了
    if i > 20 and not(sd.is_alive()):
        # 试图再次启动该线程
        sd.start()

四、控制线程

1. join

Thread提供了让一个线程等待另一个线程完成的方法一join。方法。当在某个程序执行流中调用其他线程的join。方法时,调用线程将被阻塞,直到被join。方法加入的join线程执行完成。
join。方法通常由使用线程的程序调用,以将大问题划分成许多小问题,并为每个小问题分配一个线程。当所有的小向题都得到处理后,再调用主线程来进一步操作。

代码如下:

import threading

# 定义action函数准备作为线程执行体使用
def action(max):
    for i in range(max):
        print(threading.current_thread().name + " " + str(i))
  
# 启动子线程
threading.Thread(target=action, args=(100,), name="新线程").start()
for i in range(100):
    if i == 20:
        jt = threading.Thread(target=action, args=(100,), name="被Join的线程")
        jt.start()
        # 主线程调用了jt线程的join()方法,主线程
        # 必须等jt执行结束才会向下执行
        jt.join()
    print(threading.current_thread().name + " " + str(i))

2. 后台线程

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程(Daemon Thread)",又称为“守护线程”或“精灵线程”。Python解释器的垃圾回收线程就是典型的后台线程。 .
后台线程有一个特征:如果所有的前台线程都死亡了,那么后台线程会自动死亡。

代码如下:

import threading

# 定义后台线程的线程执行体与普通线程没有任何区别
def action(max):
    for i in range(max):
        print(threading.current_thread().name + "  " + str(i))
t = threading.Thread(target=action, args=(100,), name='后台线程')
# 将此线程设置成后台线程
# 也可在创建Thread对象时通过daemon参数将其设为后台线程
t.daemon = True
# 启动后台线程
t.start()
for i in range(10):
    print(threading.current_thread().name + "  " + str(i))
# -----程序执行到此处,前台线程(主线程)结束------
# 后台线程也应该随之结束

创建后台线程有两种方式。

  • 主动将线程的daemon属性设置为True。
  • 后台线程启动的线程默认是后台线程。

3. 线程睡眠:sleeps

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用time模块的sleep(secs)函数来实现。该函数可指定一个secs参数,用于指定线程阻塞多少秒。

代码如下:

import time

for i in range(10):
    print("当前时间: %s" % time.ctime())
    # 调用sleep()函数让当前线程暂停1s
    time.sleep(1)

五、线程同步

1. 线程安全问题

案例:银行取钱
①用户输入账户、密码,系统判断用户的账户、密码是否匹配。
②用户输入取款金额。
③系统判断账户余额是否大于取款金额。
④如果余额大于取款金额,则取款成功:如果余额小于取款金额,则取款失败。

代码如下:

class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self.balance = balance
import threading
import time
import Account

# 定义一个函数来模拟取钱操作
def draw(account, draw_amount):
    # 账户余额大于取钱数目
    if account.balance >= draw_amount:
        # 吐出钞票
        print(threading.current_thread().name\
            + "取钱成功!吐出钞票:" + str(draw_amount))
#        time.sleep(0.001)
        # 修改余额
        account.balance -= draw_amount
        print("\t余额为: " + str(account.balance))
    else:
        print(threading.current_thread().name\
            + "取钱失败!余额不足!")
# 创建一个账户
acct = Account.Account("1234567" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()

2. 同步锁(lock)

如下两个方法来加锁和释放锁。

  • acquire(blocking=True, timeout=-l):请求对 Lock 或 RLock 加锁,其中 timeout 参数指定加锁多少秒。
  • release():释放锁。

Lock和RLock的区别如下。

  • threading.Lock:它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
  • threading.RLock:它代表可重入锁(Reentrant Lock)。对于可重入锁,在同一个线程中可以对它进行多次锁定,也可以多次释放。如果使用RLock,那么acquireO和releaseO方法必须成对出现。如果调用了 "次acquire。加锁,则必须调用〃次releaseQ才能释放锁。

RLock的代码格式如下:

class X:
	# 定义需要保证线程安全的方法
	def m():
		# 加锁
		self.lock.acquire()
		try:
			# 需要保证线程安全的代码
			# . . .方法体
		# 使用finally块来保证释放锁
		finally:
			# 修改完成,释放锁
			self.Took.release()

使用RLock对象来控制线程安全,当加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。

通过使用Lock对象可以非常方便地实现线程安全的类,线程安全的类具有如下特征。

  • 该类的对象可以被多个线程安全地访问。
  • 每个线程在调用该对象的任意方法之后,都将得到正确的结果。
  • 每个线程在调用该对象的任意方法之后,该对象都依然保持合理的状态。

为了更好地封装,将balance改名为_balance

代码如下:

import threading
import time

class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self._balance = balance
        self.lock = threading.RLock()

    # 因为账户余额不允许随便修改,所以只为self._balance提供getter方法
    def getBalance(self):
        return self._balance
    # 提供一个线程安全的draw()方法来完成取钱操作
    def draw(self, draw_amount):
        # 加锁
        self.lock.acquire()
        try:
            # 账户余额大于取钱数目
            if self._balance >= draw_amount:
                # 吐出钞票
                print(threading.current_thread().name\
                    + "取钱成功!吐出钞票:" + str(draw_amount))
                time.sleep(0.001)
                # 修改余额
                self._balance -= draw_amount
                print("\t余额为: " + str(self._balance))
            else:
                print(threading.current_thread().name\
                    + "取钱失败!余额不足!")
        finally:
            # 修改完成,释放锁
            self.lock.release()

下面程序创建并启动了两个取钱线程。
代码如下:

import threading
import Account

# 定义一个函数来模拟取钱操作
def draw(account, draw_amount):
    # 直接调用account对象的draw()方法来执行取钱操作
    account.draw(draw_amount)
# 创建一个账户
acct = Account.Account("1234567" , 1000)
# 模拟两个线程对同一个账户取钱
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以釆用如下策略。

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如,上面Account类中的account_no实例变量就无须同步,所以程序只对draw。方法进行了同步控制。
  • 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

3. 死锁

代码如下:

import threading
import time

class A:
    def __init__(self):
        self.lock = threading.RLock()
    def foo(self, b):
        try:
            self.lock.acquire()
            print("当前线程名: " + threading.current_thread().name\
                + " 进入了A实例的foo()方法" )     # ①
            time.sleep(0.2)
            print("当前线程名: " + threading.current_thread().name\
                + " 企图调用B实例的last()方法")   # ③
            b.last()
        finally:
            self.lock.release()
    def last(self):
        try:
            self.lock.acquire()
            print("进入了A类的last()方法内部")
        finally:
            self.lock.release()
class B:
    def __init__(self):
        self.lock = threading.RLock()
    def bar(self, a):
        try:
            self.lock.acquire()
            print("当前线程名: " + threading.current_thread().name\
                + " 进入了B实例的bar()方法" )   # ②
            time.sleep(0.2)
            print("当前线程名: " + threading.current_thread().name\
                + " 企图调用A实例的last()方法")  # ④
            a.last()
        finally:
            self.lock.release()
    def last(self):
        try:
            self.lock.acquire()
            print("进入了B类的last()方法内部")
        finally:
            self.lock.release()
a = A()
b = B()
def init():
    threading.current_thread().name = "主线程"
    # 调用a对象的foo()方法
    a.foo(b)
    print("进入了主线程之后")
def action():
    threading.current_thread().name = "副线程"
    # 调用b对象的bar()方法
    b.bar(a)
    print("进入了副线程之后")
# 以action为target启动新线程
threading.Thread(target=action).start()
# 调用init()函数
init()

死锁是不应该在程序中出现的,在编写程序时应该尽量避免出现死锁。下面有几种常见的方式用来解决死锁问题

  • 避免多次锁定:尽量避免同一个线程对多个Lock进行锁定。例血上面的死锁程序,主线程要对A、B两个对象的Lock进行锁定,副线程也要对A、B两个对變的Lock进行锁定,这就埋下了导致死锁的隐患。
  • 具有相同的加锁顺序:如果多个线程需要对多个Lock进行锁定,、则应该保证它们以相同的顺序请求加锁。比如上面的死锁程序,主线程先对A对象的Lock加锁,再对B对象的Lock加锁;而副线程则先对B对象的Lock加锁,再对A对象的Lock加锁。这种加锁顺序很容易形成嵌套锁定,进而导致死锁。如果让主线程、副线程按照相同的顺序加锁,就可以避免这个问题。
  • 使用定时锁:程序在调用acquire()方法加锁时可指定timeout参数,该参数指定超过timeout秒后会自动释放对Lock的锁定,这样就可以解开死锁了。
  • 死锁检测:死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。

六、线程通信

1. 使用Condition实现线程通信

将Condition对象与Lock对象组合使用,可以为每个对象提供多个等待集(wait-set)。因此,
Condition对象总是需要有对应的Lock对象。从Condition的构造器_init_(sel£ lock=None河以看出,程序在创建Condition时可通过lock参数传入要绑定的Lock对象;如果不指定lock参数,在创建Condition时它会自动创建一个与之绑定的Lock对象。

Condition类提供了如下几个方法。

  • acquire([timeout])/release():调用 Condition 关联的 Lock 的 acquire()或 release()方法。
  • wait([timeout]):导致当前线程进入Condition的等待池等待通知并释放锁,直到其他线程调用该Condition的notify()或notify_all()方法来唤醒该线程。在调用该wait()方法时可传入一个timeout参数,指定该线程最多導待多少秒。
  • notify():唤醒在该Condition等待池中的单个线程并通知它,收到通知的线程将自动调用acquire()方法尝试加锁。如果所有线程都在该Condition等待池中等待,则会选择唤醒其中一个线程,选择是任意性的。
  • notify_all():唤醒在该Condition等待池中等待的所有线程并通知它们。

代码如下:

import threading

class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self._balance = balance
        self.cond = threading.Condition()
        # 定义代表是否已经存钱的旗标
        self._flag = False

    # 因为账户余额不允许随便修改,所以只为self._balance提供getter方法
    def getBalance(self):
        return self._balance
    # 提供一个线程安全的draw()方法来完成取钱操作
    def draw(self, draw_amount):
        # 加锁,相当于调用Condition绑定的Lock的acquire()
        self.cond.acquire()
        try:
            # 如果self._flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if not self._flag:
                self.cond.wait()
            else:
                # 执行取钱操作
                print(threading.current_thread().name 
                    + " 取钱:" +  str(draw_amount))
                self._balance -= draw_amount
                print("账户余额为:" + str(self._balance))
                # 将标识账户是否已有存款的旗标设为False
                self._flag = False
                # 唤醒其他线程
                self.cond.notify_all()
        # 使用finally块来释放锁
        finally:
            self.cond.release()
    def deposit(self, deposit_amount):
        # 加锁,相当于调用Condition绑定的Lock的acquire()
        self.cond.acquire()
        try:
            # 如果self._flag为真,表明账户中已有人存钱进去,存钱方法阻塞
            if self._flag:            # ①
                self.cond.wait()
            else:
                # 执行存款操作
                print(threading.current_thread().name\
                    + " 存款:" +  str(deposit_amount))
                self._balance += deposit_amount
                print("账户余额为:" + str(self._balance))
                # 将表示账户是否已有存款的旗标设为True
                self._flag = True
                # 唤醒其他线程
                self.cond.notify_all()
        # 使用finally块来释放锁
        finally:
            self.cond.release()
import threading
import Account

#  定义一个函数,模拟重复max次执行取钱操作
def draw_many(account, draw_amount, max):
    for i in range(max):
        account.draw(draw_amount)
#  定义一个函数,模拟重复max次执行存款操作
def deposit_many(account, deposit_amount, max):
    for i in range(max):
        account.deposit(deposit_amount)
# 创建一个账户
acct = Account.Account("1234567" , 0)
# 创建、并启动一个“取钱”线程
threading.Thread(name="取钱者", target=draw_many, 
    args=(acct, 800, 100)).start()
# 创建、并启动一个“存款”线程
threading.Thread(name="存款者甲", target=deposit_many, 
    args=(acct , 800, 100)).start();
threading.Thread(name="存款者乙", target=deposit_many, 
    args=(acct , 800, 100)).start()
threading.Thread(name="存款者丙", target=deposit_many, 
    args=(acct , 800, 100)).start()

2. 使用队列(Queue)控制线程通信

它们的主要区别就在于进队列、出队列的不同。关于这三个队列类的简单介绍如下。

  • queue.Queue(maxsize = 0):代表FIFO (先进先出)的常规队列,maxsize可以限制队列的大小。如果队列的大小达到队列的上限,就会加锁,再次加入元素时就会被阻塞,直到队列中的元素被消费。如果将maxsize设置为0或负数,则该队列的大小就是无限制的
  • queue.LifbQueue(maxsize = 0):代表LIFO (后进先出)的队列,与Queue的区别就是出队列的顺序不同。
  • PriorityQueue(maxsize = 0):代表优先级队列,优先级最小的元素先出队列。
    这三个队列类的属性和方法基本相同,它们都提供了如下属性和方法。
  • Queue.qsize():返回队列的实际大小,也就是该队列中包含几个元素。
  • Queue.empty():判断队列是否为空。
  • Queue.fiillO:判断队列是否已满。
  • Queue.put(item, block=True, timeout=None):向队列中放入元素。如果队列已满,且 block参数为True(阻塞),当前线程被阻塞.timeout指定阻塞时间,如果将timeout设置为None,则代表一直阻塞,直到该队列的元素被消费;如果队列已满,且block参数为False (不阻塞),则直接引发queue.FULL异常。
  • Queue.put nowait(item):向队列中放入元素,不阻塞。相当于在上一个方法中将block参数设置为Falseo
  • Queue.get(item, block=True, timeout=None):从队列中取出元素(消费元素)。如果队列已满,且block参数为True (阻塞),当前线程被阻塞,timeout指定阻塞时间,如果将timeout设置为None,则代表一直阻塞,直到有元素被放入队列中;如果队列已空,且block参数为False (不阻塞),则直接引发queue.EMPTY异常。
  • Queue.get nowait(item):从队列中取出元素,不阻塞。相当于在上一•个方法中将block参数设置为Falseo
import queue

# 定义一个长度为2的阻塞队列
bq = queue.Queue(2)
bq.put("Python")
bq.put("Python")
print("1111111111")
bq.put("Python")  # ① 阻塞线程
print("2222222222")
import threading
import time
import queue

def product(bq):
    str_tuple = ("Python", "Kotlin", "Swift")
    for i in range(99999):
        print(threading.current_thread().name + "生产者准备生产元组元素!")
        time.sleep(0.2);
        # 尝试放入元素,如果队列已满,则线程被阻塞
        bq.put(str_tuple[i % 3])
        print(threading.current_thread().name \
            + "生产者生产元组元素完成!")
def consume(bq):
    while True:
        print(threading.current_thread().name + "消费者准备消费元组元素!")
        time.sleep(0.2)
        # 尝试取出元素,如果队列已空,则线程被阻塞
        t = bq.get()
        print(threading.current_thread().name \
            + "消费者消费[ %s ]元素完成!" % t)
# 创建一个容量为1的Queue
bq = queue.Queue(maxsize=1)
# 启动3个生产者线程
threading.Thread(target=product, args=(bq, )).start()
threading.Thread(target=product, args=(bq, )).start()
threading.Thread(target=product, args=(bq, )).start()
# 启动一个消费者线程
threading.Thread(target=consume, args=(bq, )).start()

3. 使用Event控制线程通道

Event提供了如下方法。

  • is_set():该方法返回Event的内部旗标是否为True。
  • set():该方法将会把Event的内部旗标设置为True,并唤醒所有处于等待状态的线程。
  • clear():该方法将Event的内部旗标设置为False,通常接下来会调用wait()方法来阻塞当前线程。

代码如下:

import threading


```python
import threading
import time

event = threading.Event()
def cal(name):
    # 等待事件,进入等待阻塞状态
    print('%s 启动' % threading.currentThread().getName())
    print('%s 准备开始计算状态' % name)
    event.wait()    # ①
    # 收到事件后进入运行状态
    print('%s 收到通知了.' % threading.currentThread().getName())
    print('%s 正式开始计算!'% name)
# 创建并启动两条,它们都会①号代码处等待
threading.Thread(target=cal, args=('甲', )).start()
threading.Thread(target=cal, args=("乙", )).start()
time.sleep(2)    #②
print('------------------')
# 发出事件
print('主线程发出事件')
event.set()





```python
class Account:
    # 定义构造器
    def __init__(self, account_no, balance):
        # 封装账户编号、账户余额的两个成员变量
        self.account_no = account_no
        self._balance = balance
        self.lock = threading.Lock()
        self.event = threading.Event()
    # 因为账户余额不允许随便修改,所以只为self._balance提供getter方法
    def getBalance(self):
        return self._balance
    # 提供一个线程安全的draw()方法来完成取钱操作
    def draw(self, draw_amount):
        # 加锁
        self.lock.acquire()
        # 如果Event内部旗标为True,表明账户中已有人存钱进去
        if self.event.is_set():
            # 执行取钱操作
            print(threading.current_thread().name 
                + " 取钱:" +  str(draw_amount))
            self._balance -= draw_amount
            print("账户余额为:" + str(self._balance))
            # 将Event内部旗标设为False
            self.event.clear()
            # 释放加锁
            self.lock.release()
            # 阻塞当前线程阻塞
            self.event.wait()
        else:
            # 释放加锁
            self.lock.release()
            # 阻塞当前线程阻塞
            self.event.wait()
    def deposit(self, deposit_amount):
        # 加锁
        self.lock.acquire()
        # 如果Event内部旗标为False,表明账户中还没有人存钱进去
        if not self.event.is_set():
            # 执行存款操作
            print(threading.current_thread().name\
                + " 存款:" +  str(deposit_amount))
            self._balance += deposit_amount
            print("账户余额为:" + str(self._balance))
            # 将Event内部旗标设为True
            self.event.set()
            # 释放加锁
            self.lock.release()
            # 阻塞当前线程阻塞
            self.event.wait()
        else:
            # 释放加锁
            self.lock.release()
            # 阻塞当前线程阻塞
            self.event.wait()

七、线程池

1. 使用线程池

线程池的基类是concurrent.futures模块中的Executor, Executor提供了两个子类,即
ThreadPoolExecutor 和 ProcessPoolExecutor,其中 ThreadPoolExecutor 用于创建线程池,而ProcessPoolExecutor用于创建进程池。

如果使用线程池/进程池来管理并发编程,那么只要将相应的task函数提交给线程池/进程池,剩下的事情就由线程池/进程池来搞定。

Exectuor提供了如下常用方法。

  • submit(fn, *args, **kwargs):将fii函数提交给线程池。*args代表传给fn函数的参数,*kwargs代表以关键字参数的形式为fn函数传入参数。
  • map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func,
    *iterables),只是该函数将会启动多个线程,以异步方式立即对iterables执行map处理。
  • shutdown(wait=True):关闭线程池。

程序将task函数提交(submit)给线程池后,submit方法会返回一个Future对象,Future类主要用于获取线程任务函数的返回值。由于线程任务会在新线程中以异步方式执行,因此,线程执行的函数相当于一个“将来完成”的任务,所以Python使用Future来代表。

Future提供了如下方法。

  • cancel():取消该Future代表的线程任务。如果该任务正在执行,不可取消,则该方法返回False:否则,程序会取消该任务,并返回True。
  • cancelled():返回Future代表的线程任务是否被成功取消。
  • running():如果该Future代表的线程任务正在执行、不可被取消,一该方法返回True。
  • done():如果该Future代表的线程任务被成功取消或执行完成,则该方法返回True。
  • result(timeout=None):获取该Future代表的线程任务最后返回的结果壽如果Future代表的
    线程任务还未完成,该方法将会阻塞当前线程,其中timeout参数指定最多阻塞多少秒。
  • exception(timeout=None):获取该Future代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回Noneo
  • add_done_callback(fn):为该Future代表的线程任务注册一个“回调函数”,当该任务成功
    完成时,程序会自动触发该fh函数。

在用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列。调用shutdown()方法后的线程池不再接收新任务,但会将以前所有的已提交任务执行完成。当线程池中的所有任务都执行完成后,该线程池中的所有线程都会死亡。

使用线程池来执行线程任务的步骤如下。
①调用ThreadPoolExecutor类的构造器创建一个线程池。
②定义一个普通函数作为线程任务。
③调用ThreadPoolExecutor对象的submit()方法来提交线程任务。
④当不想提交任何任务时,调用ThreadPoolExecutor对象的shutdown()方法来关闭线程池。

代码如下:

from concurrent.futures import ThreadPoolExecutor
import threading
import time

# 定义一个准备作为线程任务的函数
def action(max):
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + '  ' + str(i))
        my_sum += i
    return my_sum
# 创建一个包含2条线程的线程池
pool = ThreadPoolExecutor(max_workers=2)
# 向线程池提交一个task, 50会作为action()函数的参数
future1 = pool.submit(action, 50)
# 向线程池再提交一个task, 100会作为action()函数的参数
future2 = pool.submit(action, 100)
# 判断future1代表的任务是否结束
print(future1.done())
time.sleep(3)
# 判断future2代表的任务是否结束
print(future2.done())
# 查看future1代表的任务返回的结果
print(future1.result())
# 查看future2代表的任务返回的结果
print(future2.result())
# 关闭线程池
pool.shutdown()

2. 获取执行结果

add_done_callback()方法
代码如下:

from concurrent.futures import ThreadPoolExecutor
import threading
import time

# 定义一个准备作为线程任务的函数
def action(max):
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + '  ' + str(i))
        my_sum += i
    return my_sum
# 创建一个包含2条线程的线程池
with ThreadPoolExecutor(max_workers=2) as pool:
    # 向线程池提交一个task, 50会作为action()函数的参数
    future1 = pool.submit(action, 50)
    # 向线程池再提交一个task, 100会作为action()函数的参数
    future2 = pool.submit(action, 100)
    def get_result(future):
        print(future.result())
    # 为future1添加线程完成的回调函数
    future1.add_done_callback(get_result)
    # 为future2添加线程完成的回调函数
    future2.add_done_callback(get_result)
    print('--------------')
from concurrent.futures import ThreadPoolExecutor
import threading
import time

# 定义一个准备作为线程任务的函数
def action(max):
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + '  ' + str(i))
        my_sum += i
    return my_sum
# 创建一个包含4条线程的线程池
with ThreadPoolExecutor(max_workers=4) as pool:
    # 使用线程执行map计算
    # 后面元组有3个元素,因此程序启动3条线程来执行action函数
    results = pool.map(action, (50, 100, 150))
    print('--------------')
    for r in results:
        print(r)

八、线程相关类

threading.local()函数

1. 线程局部变量

代码如下:

import threading
from concurrent.futures import ThreadPoolExecutor

# 定义线程局部变量
mydata = threading.local()
# 定义准备作为线程执行体使用的函数
def action (max):
    for i in range(max):
        try:
            mydata.x += i
        except:
            mydata.x = i
        # 访问mydata的x的值
        print('%s mydata.x的值为: %d' % 
            (threading.current_thread().name, mydata.x))
# 使用线程池启动两个子线程
with ThreadPoolExecutor(max_workers=2) as pool:
    pool.submit(action , 10)
    pool.submit(action , 10)

通常建议:如果多个线程之间需要共享资源,以实现线程通信,则使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用线程局部变量。

2. 定时器(执行一次)

代码如下:

from threading import Timer

def hello():
    print("hello, world")
# 指定10秒后执行hello函数
t = Timer(10.0, hello)
t.start()



from threading import Timer
import time

# 定义总共输出几次的计数器
count = 0
def print_time():
    print("当前时间:%s" % time.ctime())
    global t, count
    count += 1
    # 如果count小于10,开始下一次调度
    if count < 10:
        t = Timer(1, print_time)
        t.start()
# 指定1秒后执行print_time函数
t = Timer(1, print_time)
t.start()

3. 任务调度

sched.scheduler类代表一个任务调度器
sched.scheduler(timefunc=time.monotonic, delayfunc=time.sleep)构造器支持两个参数。

  • timefunc:该参数指定生成时间戳的时间函数,默认使用time.monotonic来生成时间戳。
  • delayfunc:该参数指定阻塞程序的函数,默认使用time.sleep函数来阻塞程序。
    sched.scheduler调度器支持如下常用属性和方法。
  • scheduler.enterabs(time, priority, action, argument=(), kwargs={}):指定在 time 时间点执行action函数,argument和kwargs都用于向action函数传入参数,其中argument使用位置参数的形式传入参数;kwargs使用关键字参数的形式传入参数。该方法返回一个event,它可作为cancel()方法的参数用于取消该调度。priority参数指定该任务的优先级,当在同一个时间点有多个任务需要执行时,优先级高(值越小代表优先级越高)的任务会优先执行。
  • scheduler.enter(delay, priority, action, argument=(), kwargs=(}):该方法与上一个方法基本相同,只是delay参数用于指定多少秒之后执行action任务。
  • scheduler.cancel(event):取消任务。如果传入的event参数不是当前调度队列中的event,程序将会引发ValueError异常。
  • scheduler.empty():判断当前该调度器的调度队列是否为空。
  • scheduler.run(blocking=True):运行所有需要调度的任务。如果调用该方法的blocking参数为True,该方法将会阻塞线程,直到所有被调度的任务都执行完成。
  • scheduler.queue:该只读属性返回该调度器的调度队列。

代码如下:

import sched, time
import threading

# 定义线程调度器
s = sched.scheduler()

# 定义被调度的函数
def print_time(name='default'):
    print("%s 的时间: %s" % (name, time.ctime()))
print('主线程:', time.ctime())
# 指定10秒之后执行print_time函数
s.enter(10, 1, print_time)
# 指定5秒之后执行print_time函数,优先级为2
s.enter(5, 2, print_time, argument=('位置参数',))
# 指定5秒之后执行print_time函数,优先级为1
s.enter(5, 1, print_time, kwargs={'name': '关键字参数'})
# 执行调度的任务
s.run()
print('主线程:', time.ctime())

>> 主线程: Sun Feb 16 15:05:11 2020
>> 关键字参数 的时间: Sun Feb 16 15:05:16 2020
>> 位置参数 的时间: Sun Feb 16 15:05:16 2020
>> default 的时间: Sun Feb 16 15:05:21 2020
>> 主线程: Sun Feb 16 15:05:21 2020

九、多进程

1. 使用fork创建新进程

Python的os模块提供了一个fork()方法,该方法可以fork出来一个子进程。简单来说,fork()方法的作用在于:程序会启动两个进程(一个是父进程,一个是fork出来的子进程)来执行从os.fork()开始的所有代码。fork()方法不需要参数,它有一个返回值,该返回值表明是哪个进程在执行。

  • 如果fork()方法返回0,则表明是fbrk出来的子进程在执行。
  • 如果fork()方法返回非0,则表明是父进程在执行,该方法返回fbrk()出来的子进程的进程ID。

代码如下:

import os

print('父进程(%s)开始执行' % os.getpid())
# 开始fork一个子进程
# 从这行代码开始,下面代码都会被两个进程执行
pid = os.fork()
print('进程进入:%s' % os.getpid())
# 如果pid为0,表明子进程
if pid == 0:
    print('子进程,其ID为 (%s), 父进程ID为 (%s)' % (os.getpid(), os.getppid()))
else:
    print('我 (%s) 创建的子进程ID为 (%s).' % (os.getpid(), pid))
print('进程结束:%s' % os.getpid())

2. 使用multiprocessing.Process创建新进程

Python在multiprocessing模块下提供了 Process来创建新进程。与Thread类似的是,使用Process创建新进程也有两种方式。

  • 以指定函数作为target,创建Process对象即可创建新进程。
  • 继承Process类,并重写它的run()方法来创建进程类,程序创建Process子类的实例作为进程。

Process类也有如下类似的方法和属性。

  • run():重写该方法可实现进程的执行体。
  • start():该方法用于启动进程。
  • join([timeout]):该方法类似于线程的join()方法,当前进程必须等待被join的进程执行完成才能向下执行。
  • name:该属性用于设置或访问进程的名字。
  • is_alive():判断进程是否还活着。
  • daemon:该属性用于判断或设置进程的后台状态。
  • pid:返回进程的ID。
  • authkey:返回进程的授权key。
  • terminate():中断该进程。

(1)以指定函数作为target创建新进程

代码如下:

import multiprocessing
import os

# 定义一个普通的action函数,该函数准备作为进程执行体
def action(max):
    for i in range(max):
        print("(%s)子进程(父进程:(%s)):%d" % 
            (os.getpid(), os.getppid(), i))
if __name__ == '__main__':
    # 下面是主程序(也就是主进程)
    for i in range(100):
        print("(%s)主进程: %d" % (os.getpid(), i))
        if i == 20:
            # 创建并启动第一个进程
            mp1 = multiprocessing.Process(target=action,args=(100,))
            mp1.start()
            # 创建并启动第一个进程
            mp2 = multiprocessing.Process(target=action,args=(100,))
            mp2.start()
            mp2.join()
    print('主进程执行完成!')

(2)继承Process类创建子进程

继承Process类创建子进程的步骤如下。
① 定义继承Process的子类,重写其run()方法准备作为进程执行体。
② 创建Process子类的实例。
③ 调用Process子类的实例的start。方法来启动进程。

代码如下:

import multiprocessing
import os

class MyProcess(multiprocessing.Process):
    def __init__(self, max):
        self.max = max
        super().__init__()
    # 重写run()方法作为进程执行体
    def run(self):
        for i in range(self.max):
            print("(%s)子进程(父进程:(%s)):%d" % 
                (os.getpid(), os.getppid(), i))
if __name__ == '__main__':
    # 下面是主程序(也就是主进程)
    for i in range(100):
        print("(%s)主进程: %d" % (os.getpid(), i))
        if i == 20:
            # 创建并启动第一个进程
            mp1 = MyProcess(100)
            mp1.start()
            # 创建并启动第一个进程
            mp2 = MyProcess(100)
            mp2.start()
            mp2.join()
    print('主进程执行完成!')

3. Context和启动进程的方式

根据平台的支持,Python支持三种启动进程的方式。

  • spawn:父进程会启动一个全新的Python解释器进程。在这种方式下,子进程只能继承那些处理run()方法所必需的资源。典型的,那些不必要的文件描述器和handle都不会被继承。使用这种方式来启动进程,其效率比使用fork或forkserver方式要低得多。
  • fork:父进程使用os.fork()来启动一个Python解释器进程。在这种方式下,子进程会继承父进程的所有资源,因此子进程基本等效于父进程。这种方式只在UNIX平台上有效,UNIX平台默认使用这种方式来启动进程。
  • forkserver:如果使用这种方式来启动进程,程序将会启动一个服务器进程。在以后的时间内,当程序再次请求启动新进程时,父进程都会连接到该服务器进程,请求由服务器进程来fork新进程。通过这种方式启动的进程不需要从父进程继承资源。这种方式只在UNIX平台上有效。 .

代码如下:

import multiprocessing
import os

def foo(q):
    print('被启动的新进程: (%s)' % os.getpid())
    q.put('Python')
if __name__ == '__main__':
    # 设置使用fork方式启动进程
    multiprocessing.set_start_method('spawn')
    q = multiprocessing.Queue()
    # 创建进程
    mp = multiprocessing.Process(target=foo, args=(q, ))
    # 启动进程
    mp.start()
    # 获取队列中的消息 
    print(q.get())
    mp.join()
import multiprocessing
import os

def foo(q):
    print('被启动的新进程: (%s)' % os.getpid())
    q.put('Python')
if __name__ == '__main__':
    # 设置使用fork方式启动进程,并获取Context对象
    ctx = multiprocessing.get_context('fork')
    # 接下来就可用Context对象来代替mutliprocessing模块了
    q = ctx.Queue()
    # 创建进程
    mp = ctx.Process(target=foo, args=(q, ))
    # 启动进程
    mp.start()
    # 获取队列中的消息 
    print(q.get())
    mp.join()

4. 使用进程池管理进程

进程池实际上是multiprocessing.pool.Pool类。
进程池具有如下常甲方法。

  • apply(func[, args[, kwds]]):将func函数提交给进程池处理。其中args代表传给func的位置参数,kwds代表传给func的关键字参数。该方法会被阻塞直到func函数执行完成。
  • apply_async(func[, args[, kwds[, callback[, error callback]]]]):这是 apply()方法的异步版本,该方法不会被限塞。其中callback指定func函数完成后的回调函数,error_callback指定func函数出错后的回调函数。
  • map(func, iterable[, chunksize]):类似于Python的map()全局函数,只不过此处使用新进程对iterable的每一个元素执行func函数。
  • map_async(func, iterable[, chunksize[, callback[, error callback]]]):这是 map()方法的异步版本,该方法不会被阻塞。其中callback指定func函数完成后的回调函数,error_callback指定func函数出错后的回调函数。
  • imap(fimc, iterable[, chunksize]):这是 map()方法的延迟版本。
  • imap_unordered(func, iterable[, chunksize]):功能类似于 imap()方法,但该方法不能保证所生成的结果(包含多个元素)与原iterable中的元素顺序一致。
  • starmap(func, iterable[, chunksize]):功能类似于map()方法,但该方法要求iterable的元素也是iterable对象,程序会将每一个元素解包之后作为func函数的参数。
  • close():关闭进程池。在调用该方法之后,该进程池不能再接收新任务,它会把当前进程池中的所有任务执行完成后再关闭自己。
  • terminate():立即中止进程池。
  • join():等待所有进程完成。

代码如下:

import multiprocessing
import time
import os

def action(name='default'):
    print('(%s)进程正在执行,参数为: %s' % (os.getpid(), name))
    time.sleep(3)
if __name__ == '__main__':
    # 创建包含4条进程的进程池
    pool = multiprocessing.Pool(processes=4)
    # 将action分3次提交给进程池
    pool.apply_async(action)
    pool.apply_async(action, args=('位置参数', ))
    pool.apply_async(action, kwds={'name': '关键字参数'})
    pool.close()
    pool.join()




import multiprocessing
import time
import os

# 定义一个准备作为进程任务的函数
def action(max):
    my_sum = 0
    for i in range(max):
        print('(%s)进程正在执行: %d' % (os.getpid(), i))
        my_sum += i
    return my_sum
if __name__ == '__main__':
    # 创建一个包含4条进程的进程池
    with multiprocessing.Pool(processes=4) as pool:
        # 使用进程执行map计算
        # 后面元组有3个元素,因此程序启动3条进程来执行action函数
        results = pool.map(action, (50, 100, 150))
        print('--------------')
        for r in results:
            print(r)

5. 进程通信

Python为进程通信提供了两种机制。

  • Queue: 一个进程向Queue中放入数据,另一个进程从Queue中读取数据。
  • Pipe: Pipe代表连接两个进程的管道。程序在调用Pipe()函数时会产生两个连接端,分别交给通信的两个进程,接下来进程既可从该连接端读取数据,也可向该连接端写入数据。

(1)使用Queue实现进程通信

下面先看使用Queue来实现进程通信。multiprocessing模块下的Queue和queue模块下的Queue基本类似,它们都提供了 qsize()、empty()、full()、put()、put_nowait()、get()、get_nowait()等方法。区别只是multiprocessing模块下的Queue为进程提供服务,而queue模块下的Queue为线程提供服务。

代码如下:

import multiprocessing

def f(q):
    print('(%s) 进程开始放入数据...' % multiprocessing.current_process().pid)
    q.put('Python')
if __name__ == '__main__':
    # 创建进程通信的Queue
    q = multiprocessing.Queue()
    # 创建子进程
    p = multiprocessing.Process(target=f, args=(q,))
    # 启动子进程
    p.start()
    print('(%s) 进程开始取出数据...' % multiprocessing.current_process().pid)
    # 取出数据
    print(q.get())  # Python
    p.join()

(2)使用Pipe实现进程通信

使用Pipe实现进程通信,程序会调用multiprocessing.Pipe()函数来创建一个管道,该函数会返回两个PipeConnection对象,代表管道的两个连接端(一个管道有两个连接端,分别用于连接通信的两个进程)。

PipeConnection对象包含如下常用方法。

  • send(obj):发送一个obj给管道的另一端,另一端使用recv()方法接收。需要说明的是,该obj必须是可picklable的(Python的序列化机制),如果该对象序列化之后超过32MB,则很可能会引发ValueError异常。
  • recv():接收另一端通过send()方法发送过来的数据。
  • fileno():关于连接所使用的文件描述器。
  • close():关闭连接。
  • poll([timeout]):返回连接中是否还有数据可以读取。
  • send_bytes(buffer[, offset[, size]]):发送字节数据。如果没有指定offset、size参数,则默认发送buffer字节串的全部数据;如果指定了。ffset和size参数,则只发送buffer字节串中从offset开始、长度为size的字节数据。通过该方法发送的数据,应该使用recv_bytes()或recv_bytes_into 方法接收。
  • recv_bytes([maxlength]):接收通过send_bytes()方法发送的数据,maxlength指定最多接收的涵数。该方法返回接收到的字节数爲。
  • recv_bytes_mto(buffer[, offset]):功能与recv_bytes()方法类似,只是该方法将接收到的数据放在buffer中。

代码如下:

import multiprocessing

def f(conn):
    print('(%s) 进程开始发送数据...' % multiprocessing.current_process().pid)
    # 使用conn发送数据
    conn.send('Python')
if __name__ == '__main__':
    # 创建Pipe,该函数返回两个PipeConnection对象
    parent_conn, child_conn = multiprocessing.Pipe()
    # 创建子进程
    p = multiprocessing.Process(target=f, args=(child_conn, ))
    # 启动子进程
    p.start()
    print('(%s) 进程开始接收数据...' % multiprocessing.current_process().pid)
    # 通过conn读取数据
    print(parent_conn.recv())  # Python
    p.join()

你可能感兴趣的:(python)