python中的多线程和多进程
python中的多线程和多进程
@@ python中实现并发编程主要有三种方式
- 多线程
- 多进程
- 多线程+多进程
python中的多进程
Unix和Linux系统提供的fork()函数来创建子进程,子进程是父进程的一个拷贝,但是子进程有自己的PID。
fork()函数会返回两次,父进程调用的时候得到的是子进程的PID,但是子进程调用的时候返回的永远是0。
python的os模块提供了fork函数,以创建子进程。但是,需要注意的是,这仅是Linux/Unix的情况下。
Windows并没有提供fork()函数,因此,想要在Windows上使用python创建多进程,需要用到的模块是multiprocessing里面的Process类。
官方参考文档:Python multiprocessing references
from multiprocessing import Process
而且该模块提供了更佳高级的封装:
- Pool - 一个进程池:用于批量启动进程
- Queue - 用于进程间通信的队列
- Pipe - 管道
以下是一个简单的创建进程的例子
# 加载需要创建进程的基本类
from multiprocessing import Process
from os import getpid # os模块提供了一个getpid方法,用来获取当前运行的进程的id号
# 定义一个需要使用多进程来运行的函数
def out_num(num):
print('pid: %-8d, num: %d'%(getpid(), num))
if __name__ == '__main__':
nums = [i for i in range(1, 8)]
proc = [] # 定义一个进程的集合
for num in nums:
p = Process(target=out_num, args=(num,)) # 传入的参数需要注意,args是一个元组
p.start() # 当进程被创建的时候,将其启动
proc.append(p) # 将创建的进程添加到进程的集合里面
for p in proc:
# 当进程运行结束之后,将子进程合并到主进程里面
# 当然,如果不使用p.join()方法也没有出现什么问题(暂时没有发现)
p.join()
运行结果: 从下面的结果可以看到,每个进程都有有自己的pid,而且每个进程的执行开始时间是不一样的,因此输出的数字顺序也是不同的。
注意点
如果想要多进程运行的话,不能够将进程start之后紧跟着join方法,因为join方法会阻塞子进程,让子进程运行完毕之后才能够继续运行,这样的话没有多大意义,如:
for num in nums:
p = Process(target=out_num, args=(num,))
p.start()
p.join()
这样运行之后,结果是这样的:
即相当于顺序执行了,没有发挥多进程的作用,这并不是我们想要的。
python 中的多线程
在python中,我们使用的多线程模块板是threading
,-- from threading import Thread
,这样就引入了我们需要的多线程类。
创建线程和启动线程的方法其实同Process进程的创建是类似的。
以下是一个简单的创建线程的例子
#!usr/bin/python
# coding : utf-8
from threading import Thread
from random import randint
from time import sleep, time
def walk(name):
# 随机走动一段时间
sec = randint(1, 3)
print('>> %s is walking ...'% (name))
sleep(sec)
print('%s stoped. ' % (name))
if __name__ == "__main__":
begin = time()
# 创建两个线程并将其启动
t1 = Thread(target=walk, args=('LiBai',))
t2 = Thread(target=walk, args=('DuFu',))
t1.start()
t2.start()
t1.join()
t2.join()
end = time()
# 打印出两个线程云心完毕一共花费了多长时间
print('Total time: %.3f' % (end - begin))
运行结果:
从上述结果可以看出,第二个线程最先执行完毕。
除了直接使用Thread类来创建线程之外,我们还可以将Thread类进行继承封装,实现自定义的多线程对象。比如将上述的代码进行如下改写
#!usr/bin/python
# coding : utf-8
from threading import Thread
from random import randint
from time import sleep, time
class WalkTask(Thread):
def __init__(self, name):
super().__init__()
self._name = name
def run(self):
# 需要在继承的类中实现run方法,这样在调用start之后才会启动对应的业务流程
sec = randint(1, 3)
print('>> %s is walking ...'% (self.name))
sleep(sec)
print('%s stoped. ' % (self.name))
if __name__ == "__main__":
begin = time()
t1 = WalkTask('Libai')
t1.start()
t2 = WalkTask('DuFu')
t2.start()
t1.join()
t2.join()
end = time()
print('Total time: %.3f' % (end - begin))
一般情况下,在使用多线程的时候还是推荐对其进行封装
因为在跑多线程的时候可以实现更多自定义的特性,更加灵活且方便。
python怎样在线程之间进行通信
相信学过操作系统原理的同学都应该知道临界区这个概念
每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
– 来自百度百科
python中的多线程如果想要通信,也需要使用到临界区。而使用到临界区便需要对进行访问的资源进行加锁操作:
- 当使用资源时,先加上锁,此时别的线程无法访问
- 使用完毕后,将锁进行释放,别的线程就又可以访问了
这样,才不会造成线程间的访问冲突,从而造成结果出错。
那如何给临界资源加锁呢?
python 的threading模块给我们提供了Lock
类,这是一个资源锁。
这是实现原始锁对象的类。一旦一个线程获得一个锁,会阻塞随后尝试获得锁的线程,直到它被释放;任何线程都可以释放它。
来自 - Python threading Lock
如何使用?
# 先引入Lock类
from threading import Lock
_lock = Lock() # 实例化
_lock.acquire() # 加上这一句,只有获得锁 - 即锁是打开状态的时候,才能够访问这一句下方的资源
# .... 这里面是资源
_lock.release() # 释放锁,让其他的线程也可以访问
接下来实现一个小实例
#!usr/bin/python
# coding : utf-8
from threading import Thread, Lock
from random import randint
from time import sleep, time
class Person:
# 一个Person类
def __init__(self):
self._food = [] # 吃过的食物
self._lock = Lock() # 锁
self._weight = 50 # 体重
def eat(self, food):
# 吃东西 - 方法
# Person每次只能吃一个东西,因此,需要加上锁
self._lock.acquire()
try:
print('Is eating ', food[0], '...')
self._food.append(food[0])
self._weight += food[1]
sleep(randint(1,3)) # 吃一个东西将随机花费1-3秒的时间
finally:
self._lock.release() # 吃完东西后,可以将锁释放了,这样就可以吃其他东西了
# pass
@property
def food(self):
# 返回吃过的食物
return self._food
def __str__(self):
# 返回任务的体重和吃过的食物列表
s = 'weight: {}, food {}'.format(self._weight, self._food)
return s
class EatTask(Thread):
# 将吃东西采用多线程来处理
def __init__(self, person:Person, food:str):
super().__init__()
self._person = person
self._food = food
def run(self):
self._person.eat(self._food)
if __name__ == "__main__":
# 每种食物包括食物的种类和食物的重量 -- 吃了后体重会上升
foods = [('apple', 1), ('banana', 1), ('tomato', 1),
('potato', 1), ('orange', 1), ('noodles', 1),
('rice', 1), ('soup', 1)]
threads = []
# 创建一个Person对象 -- xiaoming
xiaoming = Person()
begin = time()
for food in foods:
t = EatTask(xiaoming, food)
t.start()
threads.append(t)
for t in threads:
t.join()
end = time()
print('Total time: %.3f' % (end - begin))
print(xiaoming)
运行结果
-
加上了互斥锁之后的运行结果
从上述结果可以看到,因为吃一个食物需要花费1-3秒不等的时间,任务吃完所有的食物一共花费了14.01s的时间。这是因为,在线程执行吃东西的时候,如果当前的“Person”正在吃东西的话,会将当前的线程进行阻塞,因此,程序也就相当于顺序执行了 -
去掉互斥锁的运行结果
从结果可以看到,整个程序一共花费了3.002s的时间,这是由于没有给变量添加互斥锁,多个线程可以直接一起访问。而sleep的最大时间是3s,因此程序运行结束后的最大花费时间是3s。
因此,想要在线程间进行通信,可以定义一个全局变量,但是,这个变量最好需要添加一个互斥锁,这样能够避免由于多个线程同时访问而造成的冲突。(建议的做法是和上述操作一样,将需要访问的变量封装到类里面)。
我也是一边学习一边写博客,如果还有其他的方法,希望你可以在评论里告诉我
关于如何在多线程运行的过程中获得运行结果,还可以使用队列,当线程运行完毕后,将结果放入到Queue里面,(这个Queue是一个全局变量),然后在其他的线程里面调用结果。
采用多线程还是多进程
在Python语言中,单线程+异步I/O的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。
协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。
协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
– 转自 《Python 100 Day》