js打印线程id_Python中的线程和进程(上)

7d8e9e84f24cd729789817ef001e08a6.gif

问题背景: 前段时间在做一个几十万文本数据处理的工作,需求是这样的:读取文本数据,对于每一条文本数据要调用两个接口,分别得到结果后做一个后处理并存入文件中。该需求做起来可以说是非常简单了,但我遇到的问题是效率问题,几十万的文本数据进行简单处理需要几个小时,时间的花费远远超出了我的预期。因此总结了这个Python中多进程和多线程的相关分享。

1. 问题剖析

1.1 问题分析

上述问题的数据流为:读取文件->对每行数据调用两个接口->得到结果并做后处理->写入文件。

上述问题的特点:

  • 数据量大,且在同一个文件
  • 处理后的数据需要写在同一个文件里
  • 两个接口之间没有交互,但是对于每一条数据的后处理,需要得到两个接口返回的结果后进行

目前,该问题针对每一条数据的耗时现状:5e3f0b40ea352b67168c83b5ab39f0fd.png

1.2 解决方案分析

针对上述问题的分析,为了引入Python的多线程和多进程的处理思路,该问题可以有以下提速方案:

  • 大量的文本数据,可以将数据分块读取,用不同的线程/进程来处理数据,该方案面临多线程/进程读写文件的问题
  • 由于两个接口是相互独立的,他们之间没有交互,因此可以采用多线程/进程的思路来做并行化

经过上述分析,我们应该用多线程还是多进程的思路呢?我们该对哪一部分的处理进程并行化呢?接下来做具体分析。

注:有线程/进程基础的同学,可直接跳过2.1节

2. 相关基础及问题解决过程

2.1 相关基础

线程与进程

众所周知,计算机中的计算任务都是CPU(不考虑GPU)提供的,操作系统是计算机的大管家,它控制着CPU对哪个任务进行计算、计算多长时间,这被称为资源分配。对于古老的单核(一个CPU)计算机来讲,它要承担所有的计算任务,但这些任务又不能同时执行,这时CPU的工作是分时间片的,以串行的方式,分别对不同的任务进行计算,由于其计算的速度很快,因此用户端感受不到它频繁的在不同人物之间切换。直到多核计算机的出现,一台计算机有多个CPU,可以满足多个任务同时计算,大大提升效率。因此,多线程和多进程的概念也被提出。

  • 线程:线程也叫做轻量级进程,是程序执行的最小单位。
  • 进程:进程是计算机程序一次执行的实例,是计算机资源分配和调度的基本单位,一个进程至少包含一个线程。

线程是属于进程的,线程运行在它所属的进程空间中,一旦它所属的进程退出运行,那该进程下的线程也将会被强制结束并清除,同属于一个进程的所有线程共享该进程拥有的资源,线程间也可以直接通信。

串行和并行

和我们的认知相同,一个或多个任务由多个生产线同时进行来完成,花费的总时间肯定是比单条生产线依次完成一个或多个任务花费的总时间要短,前者就是我们所说的并行的方式,后者则是串行的方式。对于单核计算机来说,是无法实现真正意义上的并行的,因为CPU是不能对多个任务同时进行处理的,真正的并行运行是在多核CPU上实现的,但是,计算机面临的任务数远远多于计算机的核数,我们也不可能通过增加计算机核数来解决并行问题,因此,计算机的大管家——操作系统还是要发挥其作用,它通常以一定的算法把多个任务轮流调度到不同的核心上运行。对于编写的计算机程序来讲,如何实现多任务呢:一般存在以下几种方案:

(1)多进程模式

(2)多线程模式

(3)多进程+多线程模式

同步和异步

操作系统中的异步和同步在程序设计,特别是多线程和多进程设计中是非常重要的概念。

  • 同步:一个任务的执行需要等待另一个任务执行完毕,任务之间的线性执行的方式,顺序不能跨越,这种情况叫做同步。
  • 异步:两个任务之间没有明确的先后关联关系,一个任务执行完毕不需要等待其他任务执行结束,而是直接进行下一步处理,这种情况叫做异步。

数据共享与通信

如上所说,进程是资源分配的基本单位,每个进程都有独立的内存等计算资源,进程之间是相互独立的,进程之间可以通过共享内存进行共享对象,使得多个进程可以访问同一个变量(地址相同,变量名可能不同)。而同属一个进程的多个线程共用计算机资源,因此个线程之间可以直接进行数据共享。但是上述情况会面临一个问题:多线程、多进程共享资源必然会导致进程和线程之间的资源竞争,因此,这就需要解决进程和线程之间的通信问题。在Python中有很多的方式可以实现线程、进程间的通信问题,详细内容在下面的内容介绍。

2.2 Python编程特点

Python代码的运行需要经过Python的解释器来执行一个.py的文件,在Python解释器Cpython中存在GIL(Global Interpreter Lock,全局解释器锁),这是在Python设计之初,为了数据安全所做的,在我们用Python解释器运行代码时,需要获得GIL,代码运行完或者sleep或者挂起之后,才能释放GIL用于其他的请求,对于一个进程,只有一个GIL,在该进程中,只用获得了GIL的线程才能进行执行,因此,多线程之间是不能并行计算的,也是这样,很多人认为“多线程在Python中是不起作用的,更推荐使用多进程的方式设计代码”,但是Python中的多线程思想真的没有用吗?好像并不是这样的,在下面我们会说明原因。

Python是一个非常友好的编程语言,他提供了很多的第三方模块,当然包含设计多线程和多进程需要用到的模块,在接下来的实例讲解中将引入相关模块的应用。

2.3 多线程和多进程实例

首先来介绍Python中多进程的实现方式,多线程的实现是依赖于multiprocessing第三方模块,

import time
import os
def test():
print('当前子进程id:{}'.format(os.getpid()))
time.sleep(2)
print('计算结果:{}'.format(2*500))

if __name__=='__main__':
print('当前主进程id:{}'.format(os.getpid()))
start=time.time()
for i in range(2):
test()
end=time.time()
print('用时{}秒'.format((end-start)))

在上述代码中,我们定义了一个函数test()来计算2的500次方,为了延长用时,我们还sleep了2秒,在主程序中我们两次调用了该函数,并计算了总耗时长,结果如下:

当前主进程id:180943
当前子进程id:180943
计算结果:1000
当前子进程id:180943
计算结果:1000
用时4.004570245742798秒

从上述代码中,我们可以看出,一个主进程调用了两次test()函数,但他们并没有直接的联系,我们现在用两个线程分别调用函数,看所用时长:

from multiprocessing import Process#利用multiprocessing模块的Process方法创建两个新的进程p1和p2
import os
import time

def test(i):
print('子进程id:{}-任务{}'.format(os.getpid(),i))
time.sleep(2)
print('结果:{}'.format(2**500))

if __name__=='__main__':
print('当前主进程id:{}'.format(os.getpid()))
start=time.time()
p1=Process(target=test,args=(1,))
p2=Process(target=test,args=(2,))
print('等待所有子进程完成。')
p1.start()
p2.start()
p1.join()
p2.join()#join()方法就是为了让主进程阻塞,等待子进程全都完成后才打印总耗时,否则输出只是主进程执行的时间
end=time.time()
print("总耗时{}秒".format((end-start)))

目前的进程id和耗时为:

当前主进程id:180943
等待所有子进程完成。
子进程id:91836-任务2
子进程id:91835-任务1
结果:1152921504606846976
结果:1152921504606846976
总耗时2.0482990741729736秒

上述展示了用python中multiprocessing模块下的Process方法创建进程的方法,从代码来看,涉及一个主进程和两个子进程,两个子进程分别执行调用test()函数的任务;在从结果来看,总耗时减少了一半。在这段代码中,为了打印总耗时,我们使用了join()方法,如果不用join()主进程是不会等待所有子进程执行完毕的,打印出来的总耗时也只是主线程执行的时间。

除了利用Process方法创建进程,我们还可以用Pool类来创建多进程

#pool方法实现多进程的例子
from multiprocessing import Pool,cpu_count
import os
import time

def test(i):
print('子进程id:{}-任务{}'.format(os.getpid(),i))
time.sleep(2)
print('结果:{}'.format(2**500))

if __name__=='__main__':
print('CPU内核总数:{}'.format(cpu_count()))
print('当前主进程id:{}'.format(os.getpid()))
start=time.time()
p=Pool(4)
for i in range(5):
p.apply_async(long_time_task,args=(i,))
p.close()
p.join()
end=time.time()
print('总耗时{}秒'.format((end-start)))

上述代码的运行结果:

CPU内核总数:32
当前主进程id:180943
子进程id:116899-任务1
子进程id:116897-任务2
子进程id:116898-任务0
子进程id:116900-任务3
结果:1152921504606846976
结果:1152921504606846976
子进程id:116899-任务4
结果:1152921504606846976
结果:1152921504606846976
结果:1152921504606846976
总耗时4.184627532958984秒

注:上述两种创建进程的方式有什么区别呢?Process()方法创建进程,用start()方法进行进程的启动,在只需要创建少量进程时,使用该方法,但是如果需要创建较多的进程,使用Process()方法手动创建的方式就不太好了,可以使用Pool()类来创建进程池,可以指定固定数量的进程来供用户调用,若进程池没满,就会根据请求创建一个进程,如果池满,就会被告知等待,直到有进程结束,才会被创建成功,因此,如果进程池不在接受其他请求是,一定要用close()方法关闭进程池。join()方法也应该用在close()之后。

通过上述多进程的方式来提升代码的运行效率在我的问题中运行的效果:

当前主进程id:180436
接口1进程id: 9833
调用接口1用时: 0.062297821044921875
接口2id: 9847
调用接口2用时: 0.08954691886901855
后处理并写入用时: 5.9604644775390625e-06
总用时: 0.15250635147094727

上述结果将两个接口的调用分别启了两个线程来分别处理,待两个结果拿到后进行后处理,可以看出总耗时大大降低,明显提升效率。

上面的介绍已经说明了Python解释器中的GIL使得同一个进程中的多个线程并不能实现真正意义上的并行,这并不意味着Python中的线程就没有什么用了,接下来我们看下Python中的多线程是如何实现的。多线程的实现在Python中主要依赖于threading第三方模块,当然之前的还有thread模块,但是thread模块的是一个比较基础的多项成模块,基本不太用,后来为了防止混淆,Python中将thread模块定义成了_thread模块。接下来基于threading模块来对上述代码进行重构:

import threading
import time
def test(i):
print('当前子线程:{}-任务{}'.format(threading.current_thread().name,i))
time.sleep(2)
print('结果:{}'.format(2**50))

if __name__=='__main__':
start=time.time()
print('主线程:{}'.format(threading.current_thread().name))
t1=threading.Thread(target=test,args=(1,))
t2=threading.Thread(target=test,args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()
end=time.time()
print('总共用时{}秒'.format((end-start)))

上述重构的代码运行的结果如下:

主线程:MainThread
当前子线程:Thread-33-任务1
当前子线程:Thread-34-任务2
结果:1125899906842624
结果:1125899906842624
总共用时2.008694887161255秒

注: 当我们需要主线程等待子线程同步进行时,想进城一样,需要用join()方法,如果我们希望一个主线程结束时,不在执行子线程,可以调用t.setDaemon(True)来实现。

除了上述的方法实现线程,还可以通过继承Thread类重写run方法来创建新线程,实现的方法如下:

import threading
import time
def test(i):
time.sleep(2)
return 2**50
class cxSample(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('start子进程:{}'.format(self.name))
self.result=self.func(self.args[0],)
print('结果:{}'.format(self.result))
print('end子进程{}'.format(self.name))

if __name__=='__main__':
start = time.time()
threads = []
for i in range(1, 3):
t = cxSample(test, (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)))

上述代码实现了对threading.Thread的重写并定义为cxSample,在主程序中对该类进行了实例化,创建了3个线程。

根据上述讲解,我们知道,属于同一个进程的线程是资源共享的,这就意味着一个变量可能会被多个线程修改,因此,多线程应用时面临最大的问题就是多个线程对同一个变量的操作,使得该变量的最终结果得到的不是我们想要的,因此,怎样才能控制多线程可以合理共享变量呢?其中一个方法就是利用锁机制,确保每次只能有一个进程修改它。Python中的threading模块也给我们提供了lock()方法,可以轻易的实现对一个变量的锁定和释放,代码示例如下:

#锁机制来模拟一个账户的进账、出帐的例子
import threading
class Account():
def __init__(self):
self.balance=5
def add(self,lock):
#获得锁
lock.acquire()
for i in range(0,100):
self.balance+=i
#释放锁
lock.release()

def delete(self,lock):
#获得锁
lock.acquire()
for i in range(0,100):
self.balance-i
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('最终账户余额:{}'.format(account.balance))

上述代码通过锁机制实现add()和delete()操作的有序、合理的进行。除了利用锁机制还可以利用消息队列queue的方式,相比于列表,queue是线程安全的。消息队列的方式也是进程间进行数据共享的方式之一。示例代码如下:

#这段代码中创建了2个独立的进程,一个负责写(pw),一个负责读(pr),实现了共享一个队列queue
from multiprocessing import Process,Queue
import os,time,random

#写数据进程的执行代码:
def writer(q):
print('写进程id:{}'.format(os.getpid()))
for value in ['内容1','内容2','内容3']:
print('把 %s 写入队列...'% value)
q.put(value)
time.sleep(random.random())

def reader(q):
print('读进程id:{}'.format(os.getpid()))
value=q.get(True)
print('从队列取出 %s.'%value)

if __name__=='__main__':
#父进程创建Queue,并传给各个子进程:
q=Queue()
pwriter=Process(target=writer,args=(q,))
preader=Process(target=reader,args=(q,))
#启动
pwriter.start()
preader.start()
#同步
pwriter.join()
preader.join()

上述代码利用消息队列模拟了读写进程的,其结果为:

写进程id:78473
把 内容1 写入队列...
读进程id:78474
从队列取出 内容1.
把 内容2 写入队列...
把 内容3 写入队列...

3. 多进程下的文件读写

对于问题背景中遇到的问题,我认为,既然大量的文本数据都存在同一个文本文件中,那我们是否可以将文件分到不同的进程中去处理,然而对于多进程对于同一个文件的读写又会面临资源抢占的问题和进程管理的问题,下面将要给出多进程读写文件的实:

3.1 多进程读写文件

感觉这是一个好复杂的问题啊,真的伤脑子...

写到这儿感觉篇幅有点长了,打算过几天再写个 Python中的线程和进程(下),这篇就先写到这儿吧...

4. 总结

多进程和多线程能在一定程度上提高了程序执行的效率,由于Cpython解释器中特有的GIL,使得很多人认为Python中的多线程是应该摒弃的,应该推崇多进程,但是我们知道,进程的切换和调度都是需要耗费时间的,因此并没有特定的结论说多线程更快或者多进程更快,如果非要给个结论的话,我只能将网上大家一致的结论写下来:

(1)多线程对IO密集型代码比较友好

(2)多进程对CPU密集型代码比较友好

如果要最大化的发挥多核计算机的优势,多进程可能更好一些。

参考资料

进程和线程

谈谈python的GIL、多线程、多进程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值