✍转载自:https://blog.csdn.net/weixin_42134789/article/details/82992326
功能
》进程,能够完成多任务,比如: 在一台电脑上能够同时运行多个QQ
》线程,能够完成多任务,比如: 一个QQ中的多个聊天窗口
定义的不同
- 进程是系统进行资源分配和调度的一个独立单位.
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
区别
优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反
1、通过multiprocess模块,实现多进程
》python的多进程编程主要依靠 multiprocess 模块。我们先对比两段代码,看看多进程编程的优势。
案例:模拟了一个非常耗时的任务,计算8的20次方,为了使这个任务显得更耗时,我们还让它sleep 2秒。
》第1段代码是单进程计算(代码如下所示),我们按顺序执行代码,重复计算2次,并打印出总共耗时:
import time
import os
def long_time_task():
print('当前进程: {}'.format(os.getpid()))
time.sleep(2)
print("结果: {}".format(8 ** 20))
if __name__ == "__main__":
print('当前母进程: {}'.format(os.getpid()))
start = time.time()
for i in range(2):
long_time_task()
end = time.time()
print("用时{}秒".format((end-start)))
'''
运行结果为:
【总共耗时4秒,至始至终只有一个进程14236。看来电脑计算8的20次方基本不费时】
当前母进程: 14236
当前进程: 14236
结果: 1152921504606846976
当前进程: 14236
结果: 1152921504606846976
用时4.01080060005188秒
'''
》第2段代码是多进程计算代码。我们利用multiprocess模块的Process方法创建了两个新的进程p1和p2来进行并行计算。
Process方法接收两个参数, 第一个是target,一般指向函数名;第二个是args,需要向函数传递的参数。
对于创建的新进程,调用start()方法即可让其开始。且可以使用os.getpid()打印出当前进程的名字。
from multiprocessing import Process
import os
import time
def long_time_task(i):
print('子进程: {} - 任务{}'.format(os.getpid(), i))
time.sleep(2)
print("结果: {}".format(8 ** 20))
if __name__=='__main__':
print('当前母进程: {}'.format(os.getpid()))
start = time.time()
p1 = Process(target=long_time_task, args=(1,))
p2 = Process(target=long_time_task, args=(2,))
print('等待所有子进程完成。')
p1.start()
p2.start()
p1.join()
p2.join()
end = time.time()
print("总共用时{}秒".format((end - start)))
'''
运行结果为:
【输出结果如下所示,耗时变为2秒,时间减了一半,可见并发执行的时间明显比顺序执行要快很多。
尽管只创建了两个进程,可实际运行中却包含里1个母进程和2个子进程。
之所以我们使用join()方法就是为了让母进程阻塞,等待子进程都完成后才打印出总共耗时,否则输出时间只是母进程执行的时间。】
当前母进程: 6920
等待所有子进程完成。
子进程: 17020 - 任务1
子进程: 5904 - 任务2
结果: 1152921504606846976
结果: 1152921504606846976
总共用时2.131091356277466秒
'''
总结:
- 新创建的进程与进程的切换都是要耗资源的,所以平时工作中进程数不能开太大。
- 同时可以运行的进程数一般受制于CPU的核数。
- 除了使用Process方法,我们还可以使用Pool类创建多进程。
2、利用multiprocess模块的Pool类创建多进程
》Pool类可以提供指定数量的进程供用户调用【可以通过传递参数限制并发进程的数量,默认值为CPU的核数】
》当有新的请求提交到Pool中时,如果进程池还没有满,就会创建一个新的进程来执行请求。如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求。
下面介绍一下multiprocessing 模块下的 Pool 类的几个方法:
下例是一个简单的multiprocessing.Pool类的实例。因为小编我的CPU是4核的,一次最多可以同时运行4个进程,所以我开启了一个容量为4的进程池。4个进程需要计算5次,你可以想象4个进程并行4次计算任务后,还剩一次计算任务(任务4)没有完成,系统会等待4个进程完成后重新安排一个进程来计算。
from multiprocessing import Pool, cpu_count
import os
import time
def long_time_task(i):
print('子进程: {} - 任务{}'.format(os.getpid(), i))
time.sleep(2)
print("结果: {}".format(8 ** 20))
if __name__=='__main__':
print("CPU内核数:{}".format(cpu_count()))
print('当前母进程: {}'.format(os.getpid()))
start = time.time()
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('等待所有子进程完成。')
p.close()
p.join()
end = time.time()
print("总共用时{}秒".format((end - start)))
'''
运行结果为:
【5个任务(每个任务大约耗时2秒)使用多进程并行计算只需4.37秒,, 耗时减少了60%】
CPU内核数:4
当前母进程: 2556
等待所有子进程完成。
子进程: 16480 - 任务0
子进程: 15216 - 任务1
子进程: 15764 - 任务2
子进程: 10176 - 任务3
结果: 1152921504606846976
结果: 1152921504606846976
子进程: 15216 - 任务4
结果: 1152921504606846976
结果: 1152921504606846976
结果: 1152921504606846976
总共用时4.377134561538696秒
'''
总结:
- 对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close()或terminate()方法,让其不再接受新的Process了。
3、多进程间的数据共享与通信
》通常,进程之间是相互独立的,每个进程都有独立的内存。通过共享内存(nmap模块),进程之间可以共享对象,使多个进程可以访问同一个变量(地址相同,变量名可能不同)。
》多进程共享资源必然会导致进程间相互竞争,所以应该尽最大可能防止使用共享状态。
》还有一种方式就是使用队列queue来实现不同进程间的通信或数据共享,这一点和多线程编程类似。
下例这段代码中中创建了2个独立进程,一个负责写(pw), 一个负责读(pr), 实现了共享一个队列 queue:
from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行的代码:
def write(q):
print('Process to write: {}'.format(os.getpid()))
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
print('Process to read:{}'.format(os.getpid()))
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__ == '__main__':
# 父进程创建 Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程 pw,写入:
pw.start()
# 启动子进程 pr,读取:
pr.start()
# 等待 pw 结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()
'''
运行结果为:
Process to write: 3036
Put A to queue...
Process to read:9408
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.
'''
1、通过threading.Thread方法,实现多线程
》python 3中的多进程编程主要依靠 threading 模块。创建新线程与创建新进程的方法非常类似。
》threading.Thread方法可以接收两个参数, 第一个是target,一般指向函数名,第二个时args,需要向函数传递的参数。对于创建的新线程,调用start()方法即可让其开始。
》还可以使用current_thread().name打印出当前线程的名字。
下例中,使用多线程技术重构之前的计算代码:
import threading
import time
def long_time_task(i):
print('当前子线程: {} 任务{}'.format(threading.current_thread().name, i))
time.sleep(2)
print("结果: {}".format(8 ** 20))
if __name__ == '__main__':
start = time.time()
print('这是主线程:{}'.format(threading.current_thread().name))
thread_list = []
for i in range(1, 3):
t = threading.Thread(target=long_time_task, args=(i, ))
thread_list.append(t)
for t in thread_list:
t.start()
for t in thread_list:
t.join()
end = time.time()
print("总共用时{}秒".format((end - start)))
'''
运行结果为:
【这时你可以看到主线程在等子线程完成后才答应出总消耗时间(2秒),比正常顺序执行代码(4秒)还是节省了不少时间。】
这是主线程:MainThread
当前子线程: Thread - 1 任务1
当前子线程: Thread - 2 任务2
结果: 1152921504606846976
结果: 1152921504606846976
总共用时2.0166890621185303秒
'''
》当我们设置多线程时,主线程会创建多个子线程,在python中,默认情况下主线程和子线程独立运行互不干涉。
》如果希望让主线程等待子线程实现线程的同步,我们需要使用 join() 方法。
》如果我们希望一个主线程结束时不再执行子线程,那么可以使用t.setDaemon(True)
,代码如下所示:
import threading
import time
def long_time_task():
print('当子线程: {}'.format(threading.current_thread().name))
time.sleep(2)
print("结果: {}".format(8 ** 20))
if __name__ == '__main__':
start = time.time()
print('这是主线程:{}'.format(threading.current_thread().name))
for i in range(5):
t = threading.Thread(target=long_time_task, args=())
t.setDaemon(True)
t.start()
end = time.time()
print("总共用时{}秒".format((end - start)))
'''
运行结果为:
【最后的用时是主线程的时间】
这是主线程:MainThread
当子线程: Thread-1
当子线程: Thread-2
当子线程: Thread-3
当子线程: Thread-4
当子线程: Thread-5
总共用时0.00084114074707秒
'''
2、通过继承Thread类重写run方法创建新进程
》除了使用Thread()方法创建新的线程外,我们还可以通过继承 Thread类 重写run()方法创建新的线程,这种方法更灵活。
下例中我们自定义的类为MyThread, 随后我们通过该类的实例化创建了2个子线程:
import threading
import time
def long_time_task(i):
time.sleep(2)
return 8**20
class MyThread(threading.Thread):
def __init__(self, func, args, name='', ):
threading.Thread.__init__(self)
self.func = func
self.args = args
self.name = name
self.result = None
def run(self):
print('开始子进程{}'.format(self.name))
self.result = self.func(self.args[0],)
print("结果: {}".format(self.result))
print('结束子进程{}'.format(self.name))
if __name__=='__main__':
start = time.time()
threads = []
for i in range(1, 3):
t = MyThread(long_time_task, (i,), str(i))
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()
end = time.time()
print("总共用时{}秒".format((end - start)))
'''
运行结果为:
开始子进程1
开始子进程2
结果: 1152921504606846976
结果: 1152921504606846976
结束子进程1
结束子进程2
总共用时2.005445718765259秒
'''
3、不同线程间的数据共享
》一个进程所含的不同线程间共享内存,这就意味着任何一个变量都可以被任何一个线程修改,因此线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
》如果不同线程间有共享的变量,其中一个方法就是在修改前给其上一把锁lock
,确保一次只有一个线程能修改它。
》 threading.lock() 方法可以轻易实现对一个共享变量的锁定,修改完后release
供其它线程使用。
如下例中账户余额balance是一个共享变量,使用lock可以使其不被改乱:
import threading
class Account:
def __init__(self):
self.balance = 0
def add(self, lock):
# 获得锁
lock.acquire()
for i in range(0, 10000):
self.balance += 1
# 释放锁
lock.release()
def delete(self, lock):
# 获得锁
lock.acquire()
for i in range(0, 10000):
self.balance -= 1
# 释放锁
lock.release()
if __name__ == "__main__":
account = Account()
lock = threading.Lock()
# 创建线程
thread_add = threading.Thread(target=account.add, args=(lock,), name='Add')
thread_delete = threading.Thread(target=account.delete, args=(lock,), name='Delete')
# 启动线程
thread_add.start()
thread_delete.start()
# 等待线程结束
thread_add.join()
thread_delete.join()
print('The final balance is: {}'.format(account.balance))
'''
运行结果为:
The final balance is: 0
'''
(另一种实现不同线程间数据共享的方法就是使用消息队列 queue)
4、使用queue队列通信——经典的生产者和消费者模型
from queue import Queue
import random, threading, time
# 生产者类
class Producer(threading.Thread):
def __init__(self, name, queue):
threading.Thread.__init__(self, name=name)
self.queue = queue
def run(self):
for i in range(1, 5):
print("{} is producing {} to the queue!".format(self.getName(), i))
self.queue.put(i)
time.sleep(random.randrange(10) / 5)
print("%s finished!" % self.getName())
# 消费者类
class Consumer(threading.Thread):
def __init__(self, name, queue):
threading.Thread.__init__(self, name=name)
self.queue = queue
def run(self):
for i in range(1, 5):
val = self.queue.get()
print("{} is consuming {} in the queue.".format(self.getName(), val))
time.sleep(random.randrange(10))
print("%s finished!" % self.getName())
def main():
queue = Queue()
producer = Producer('Producer', queue)
consumer = Consumer('Consumer', queue)
producer.start()
consumer.start()
producer.join()
consumer.join()
print('All threads finished!')
if __name__ == '__main__':
main()
'''
运行结果为:
Producer is producing 1 to the queue!
Consumer is consuming 1 in the queue.
Producer is producing 2 to the queue!
Producer is producing 3 to the queue!
Producer is producing 4 to the queue!
Producer finished!
Consumer is consuming 2 in the queue.
Consumer is consuming 3 in the queue.
Consumer is consuming 4 in the queue.
Consumer finished!
All threads finished!
'''
总结:
- 队列queue的put方法可以将一个对象obj放入队列中。如果队列已满,此方法将阻塞至队列有空间可用为止。queue的get方法一次返回队列中的一个成员。如果队列为空,此方法将阻塞至队列中有成员可用为止。
- queue同时还自带emtpy(),full()等方法来判断一个队列是否为空或已满,但是这些方法并不可靠,因为多线程和多进程,在返回结果和使用结果之间,队列中可能添加/删除了成员。
引入一个概念:“GIL全局解释器锁”
Python自带的解释器是CPython
。CPython解释器的多线程实际上是一个假的多线程(在多核CPU中,只能利用一核,不能利用多核)。同一时刻只有一个线程在执行,为了保证同一时刻只有一个线程在执行,在CPython解释器中有一个东西叫做GIL(Global Intepreter Lock),叫做全局解释器锁。这个解释器锁是有必要的。
因为CPython解释器的内存管理不是线程安全的。当然除了CPython解释器,还有其他的解释器,有些解释器是没有GIL锁的,见下面:
Jython
:用Java实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/JythonIronPython
:用.net实现的Python解释器。不存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/IronPythonPyPy
:用Python实现的Python解释器。存在GIL锁。更多详情请见:https://zh.wikipedia.org/wiki/PyPy》GIL虽然是一个假的多线程。但是在处理一些IO操作(比如文件读写和网络请求)还是可以在很大程度上提高效率的。在IO操作上建议使用多线程提高效率。在一些CPU计算操作上不建议使用多线程,而建议使用多进程
》由于GIL的存在,很多人认为Python多进程编程更快,针对多核CPU,理论上来说也是采用多进程更能有效利用资源。网上很多人已做过比较,我直接告诉你结论吧。
对CPU密集型代码(比如循环计算) - 多进程效率更高
对IO密集型代码(比如文件操作,网络爬虫) - 多线程效率更高。
为什么是这样呢?其实也不难理解。
》对于IO密集型操作,大部分消耗时间其实是等待时间,在等待时间中CPU是不需要工作的,那你在此期间提供双CPU资源也是利用不上的。
》相反,对于CPU密集型代码,2个CPU干活肯定比一个CPU快很多。
》那么为什么多线程会对IO密集型代码有用呢?这是因为python碰到等待会释放GIL供新的线程使用,实现了线程间的切换。