多线程程序则可以包含多个顺序执行流,这些顺序执行流之间互不干扰。
进程包含如下三个特征。
线程(Thread)也被称作轻量级进程(LightweightProcess),线程是进程的执行单元。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。
一个程序运行后至少有一个进程,在一个进程中可以包含多个线程,但至少要包含一个主线程。
操作系统可以同时执行多个任务,每一个任务就是一个进程;进程可以同时执行多个任务,每一个任务就是一个线程。
线程在程序中是独立的、并发的执行流。与分隔的进程相比,进程中线程之间的隔离程度要小,它们共享内存、文件句柄和其他进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性——多个线程共享同一个进程的虚拟空间。线程共享的环境包括进程代码段、进程的公有数据等,利用这些共享的数据,线程之间很容易实现通信。
使用多线程编程具有如下几个优点。
进程之间不能共享内存,但线程之间共享内存非常容易。
操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。
因此,使用多线程来实现多任务并发执行比使用多进程的效率高。
Python语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简
化了 Python的多线程编程。
Python提供了_thread和threading两个模块来支持多线程。
Python主要通过两种方式来创建线程。
调用Thread类的构造器创建线程很简单,直接调用threading.Thread类的如下构造器创建线程。
_ init _ (self, group=None, target=None, name=None, args=(), kwargs=None, *,
daemon=None)
上面的构造器涉及如下几个参数。
通过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('主线程执行完成!')
除此之外,上面程序还用到了如下函数和方法。 .
程序可以通过setName(name)方法为线程设置名字,也可以通过getName()方法返
回指定线程的名字,这两个方法可通过name属性来代替。在默认情况下,主线程的名
字为MainThread,用户启动的多个线程的名字依次为Thread-1、Thread-2、Thread-3、
Thread-n 等。
通过继承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需要在多个线程之间切换,于是线程状态也会多次在运行、就绪之间转换。
当程序创建了一个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。方法之后,该线程立即进入就绪状态——相当于“等待执行”,但该线程并未真正进入运行状态。
当一个线程调用了它的sleep()或yield()方法后才会放弃其所占用的资源——也就是必须由该线程主动放弃其所占用的资源。
当发生如下情况时,线程将会进入阻塞状态。
>线程调用sleep()方法主动放弃其所占用的处理器资源。
>线程调用了一个阻塞式I/O方法,在该方法返回之前,该线程被阻塞。
>线程试图获得一个锁对象,但该锁对象正被其他线程所持有。关于锁对象的知识,后面将有更深入的介绍。
>线程在等待某个通知(Notify)。
针对上面几种情况,当发生如下特定的情况时可以解除阻塞,让该线程重新进入就绪态。
线程会以如下三种方式结束,结束后就处于死亡状态。
当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。
为了测试某个线程是否已经死亡,可以调用线程对象的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()
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))
有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为“后台线程(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))
# -----程序执行到此处,前台线程(主线程)结束------
# 后台线程也应该随之结束
创建后台线程有两种方式。
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用time模块的sleep(secs)函数来实现。该函数可指定一个secs参数,用于指定线程阻塞多少秒。
代码如下:
import time
for i in range(10):
print("当前时间: %s" % time.ctime())
# 调用sleep()函数让当前线程暂停1s
time.sleep(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()
如下两个方法来加锁和释放锁。
Lock和RLock的区别如下。
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()
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以釆用如下策略。
代码如下:
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()
死锁是不应该在程序中出现的,在编写程序时应该尽量避免出现死锁。下面有几种常见的方式用来解决死锁问题
将Condition对象与Lock对象组合使用,可以为每个对象提供多个等待集(wait-set)。因此,
Condition对象总是需要有对应的Lock对象。从Condition的构造器_init_(sel£ lock=None河以看出,程序在创建Condition时可通过lock参数传入要绑定的Lock对象;如果不指定lock参数,在创建Condition时它会自动创建一个与之绑定的Lock对象。
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()
它们的主要区别就在于进队列、出队列的不同。关于这三个队列类的简单介绍如下。
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()
Event提供了如下方法。
代码如下:
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()
线程池的基类是concurrent.futures模块中的Executor, Executor提供了两个子类,即
ThreadPoolExecutor 和 ProcessPoolExecutor,其中 ThreadPoolExecutor 用于创建线程池,而ProcessPoolExecutor用于创建进程池。
如果使用线程池/进程池来管理并发编程,那么只要将相应的task函数提交给线程池/进程池,剩下的事情就由线程池/进程池来搞定。
Exectuor提供了如下常用方法。
程序将task函数提交(submit)给线程池后,submit方法会返回一个Future对象,Future类主要用于获取线程任务函数的返回值。由于线程任务会在新线程中以异步方式执行,因此,线程执行的函数相当于一个“将来完成”的任务,所以Python使用Future来代表。
Future提供了如下方法。
在用完一个线程池后,应该调用该线程池的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()
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()函数
代码如下:
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)
通常建议:如果多个线程之间需要共享资源,以实现线程通信,则使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用线程局部变量。
代码如下:
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()
sched.scheduler类代表一个任务调度器
sched.scheduler(timefunc=time.monotonic, delayfunc=time.sleep)构造器支持两个参数。
代码如下:
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
Python的os模块提供了一个fork()方法,该方法可以fork出来一个子进程。简单来说,fork()方法的作用在于:程序会启动两个进程(一个是父进程,一个是fork出来的子进程)来执行从os.fork()开始的所有代码。fork()方法不需要参数,它有一个返回值,该返回值表明是哪个进程在执行。
代码如下:
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())
Python在multiprocessing模块下提供了 Process来创建新进程。与Thread类似的是,使用Process创建新进程也有两种方式。
Process类也有如下类似的方法和属性。
代码如下:
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('主进程执行完成!')
继承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('主进程执行完成!')
根据平台的支持,Python支持三种启动进程的方式。
代码如下:
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()
进程池实际上是multiprocessing.pool.Pool类。
进程池具有如下常甲方法。
代码如下:
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)
Python为进程通信提供了两种机制。
下面先看使用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()
使用Pipe实现进程通信,程序会调用multiprocessing.Pipe()函数来创建一个管道,该函数会返回两个PipeConnection对象,代表管道的两个连接端(一个管道有两个连接端,分别用于连接通信的两个进程)。
PipeConnection对象包含如下常用方法。
代码如下:
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()