Python 多线程
多线程类似于同时执行多个不同程序,多线程运行有如下优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。 程序的运行速度可能加快。
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
线程模块
Python3 通过两个标准库 _thread 和 threading 提供对线程的支持。
_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。
threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate():返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join([time]):等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
最近因为科研需要,我需要使用dtw(Dynamic Time Warping,动态时间归整)计算多个脑区的相关性,但是我有1611个人的数据,每个人需要计算点对点的连接6670个,我测了一下单个连接需要耗费2秒的时间。所以想起了使用python的多线程和多进程(恐怕也是杯水车薪,无妨,也可以不使用DTW)。这里谨做一下记录。如下所示,
我直接从 threading.Thread 继承创建了两个新的子类,并实例化后调用 start() 方法启动新线程,然后线程自己调用线程的 run() 方法
import threading
from time import ctime,sleep
def mddFc():
path1 = r"F:\sklearn-ranforest\all\TimeSeries\MDD"
filelist = os.listdir(path1)
for i in range(832):
file = filelist[i]
m = loadmat(os.path.join(path1, file))
matrix = m.get('ROISignals')
ind = 0
for j in range(0, 115):
for k in range(j + 1, 116):
X = matrix[:, j]
Y = matrix[:, k]
dis, paths = fastdtw(X, Y, dist=euclidean, radius=20)
newX = []
newY = []
for path in paths:
fi = path[0]
se = path[1]
newX.append(X[fi])
newY.append(Y[se])
cc = np.array([newX, newY])
cc_pd = pd.DataFrame(cc.T, columns=['c1', 'c2'])
cc_corr = cc_pd.corr(method='pearson') # 相关系数矩阵
val = cc_corr.iloc[0, 1]
fc[ind][i] = dis
fisherFc[ind][i] = atanh(val)
ind = ind + 1
def ncFc():
path2 = r"F:\sklearn-ranforest\all\TimeSeries\NC"
filelist = os.listdir(path2)
for i in range(779):
file = filelist[i]
m = loadmat(os.path.join(path2, file))
matrix = m.get('ROISignals')
ind = 0
for j in range(0, 115):
for k in range(j + 1, 116):
X = matrix[:, j]
Y = matrix[:, k]
dis, paths = fastdtw(X, Y, dist=euclidean, radius=20)
newX = []
newY = []
for path in paths:
fi = path[0]
se = path[1]
newX.append(X[fi])
newY.append(Y[se])
cc = np.array([newX, newY])
cc_pd = pd.DataFrame(cc.T, columns=['c1', 'c2'])
cc_corr = cc_pd.corr(method='pearson') # 相关系数矩阵
val = cc_corr.iloc[0, 1]
fc2[ind][i] = dis
fisherFc2[ind][i] = atanh(val)
ind = ind + 1
import threading
import time
class mddThread(threading.Thread):
def __init__(self, threadID, name):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
def run(self):
print("开始线程:" + self.name)
mddFc()
print("退出线程:" + self.name)
class ncThread(threading.Thread):
def __init__(self, threadID, name):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
def run(self):
print("开始线程:" + self.name)
ncFc()
print("退出线程:" + self.name)
# 创建新线程
thread1 = mddThread(1, "Thread-mdd")
thread2 = ncThread(2, "Thread-nc")
# 开启新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
#等待两个线程执行完毕,主线程再执行这一行代码
print("运行结束")
大概十分钟计算了300个大脑之间的功能连接。
但是Python的并发机制是这样的,他有一个GIL锁,同一个进程里面只有一个线程拿到。(在Cpython解释器中)
GIL(Global Interpreter Lock)全局解释器锁
为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。
那么,我们改如何解决GIL锁的问题呢?
1.更换cpython为jpython(不建议)
2.使用多进程完成多线程的任务
3.在使用多线程可以使用其他语言去实现
GIL在python中的版本差异:
1、在python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100时进行释放。(ticks可以看作是python自身的一个计数器,专门做用于GIL,每次释放后归零,这个计数可以通过sys.setcheckinterval 来调整)。而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。
2、在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。
多进程和多线程选型:
1、CPU密集型代码(各种计算),在这种情况下,由于计算工作多,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好,因为线程的频繁切换,获取GIL锁,所以性能可能还不如单线程,这种情况使用多进程执行比较好。
2、IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
使用mulitprocess.process模块实现多进程
process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。
方法介绍
- p.start():启动进程,并调用该子进程的p.run()
- p.run():进程启动时运行的方法,正是它取调用target指定的函数
- p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁。
- p.is_alive():如果p任然运行,返回True
- p.join([timeout]):主线程等待p终止。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
属性介绍
- p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
- p.name:进程的名称
- p.pid:进程的pid
- p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功
在Windows中使用process模块的注意事项
在Windows操作系统中由于没有fork(Linux操作系统中创建进程的机制),在创建 子进程的是时候会自动import启动它的这个文件,而在import的时候又执行了整个文件。因此如果讲process()直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用if name =='main’判断保护起来,import的时候,就不会递归运行了。
def mddFc():
row = 6670
col = 832
fc = [[0.0] * col for i in range(row)]
fisherFc = [[0.0] * col for i in range(row)]
# 这里fc为dtw计算的欧氏距离
fc = np.array(fc)
# 这里是利用dtw扭曲的两个时间序列,重新计算皮尔孙,然后fisher
fisherFc = np.array(fisherFc)
path1 = r"F:\sklearn-ranforest\all\TimeSeries\MDD"
filelist = os.listdir(path1)
for i in range(832):
file = filelist[i]
m = loadmat(os.path.join(path1, file))
matrix = m.get('ROISignals')
ind = 0
for j in range(0, 115):
for k in range(j + 1, 116):
X = matrix[:, j]
Y = matrix[:, k]
dis, paths = fastdtw(X, Y, dist=euclidean, radius=20)
newX = []
newY = []
for path in paths:
fi = path[0]
se = path[1]
newX.append(X[fi])
newY.append(Y[se])
cc = np.array([newX, newY])
cc_pd = pd.DataFrame(cc.T, columns=['c1', 'c2'])
cc_corr = cc_pd.corr(method='pearson') # 相关系数矩阵
val = cc_corr.iloc[0, 1]
fc[ind][i] = dis
fisherFc[ind][i] = atanh(val)
ind = ind + 1
printf("处理完第%d个病人" % (i + 1))
savemat(r'F:\sklearn-ranforest\all\TimeSeries\original\dtwDisMdd.mat', {'dtwDis': fc})
savemat(r'F:\sklearn-ranforest\all\TimeSeries\original\fisherDtwCorrMdd.mat', {'dtwFc': fisherFc})
def ncFc():
row = 6670
col = 779
fc2 = [[0.0] * col for i in range(row)]
fisherFc2 = [[0.0] * col for i in range(row)]
fc2 = np.array(fc2)
fisherFc2 = np.array(fisherFc2)
path2 = r"F:\sklearn-ranforest\all\TimeSeries\NC"
filelist = os.listdir(path2)
for i in range(779):
file = filelist[i]
m = loadmat(os.path.join(path2, file))
matrix = m.get('ROISignals')
ind = 0
for j in range(0, 115):
for k in range(j + 1, 116):
X = matrix[:, j]
Y = matrix[:, k]
dis, paths = fastdtw(X, Y, dist=euclidean, radius=20)
newX = []
newY = []
for path in paths:
fi = path[0]
se = path[1]
newX.append(X[fi])
newY.append(Y[se])
cc = np.array([newX, newY])
cc_pd = pd.DataFrame(cc.T, columns=['c1', 'c2'])
cc_corr = cc_pd.corr(method='pearson') # 相关系数矩阵
val = cc_corr.iloc[0, 1]
fc2[ind][i] = dis
fisherFc2[ind][i] = atanh(val)
ind = ind + 1
printf("处理完第%d个正常人"%(i+1))
savemat(r'F:\sklearn-ranforest\all\TimeSeries\original\dtwDisNc.mat', {'dtwDis': fc2})
savemat(r'F:\sklearn-ranforest\all\TimeSeries\original\fisherDtwCorrNc.mat', {'dtwFc': fisherFc2})
if __name__ == '__main__':
#target表示调用对象,即子进程要执行的任务
mddProcess = Process(target=mddFc())
ncProcess = Process(target=ncFc())
mddProcess.start()
ncProcess.start()
mddProcess.join()
ncProcess.join()
print('子进程执行完毕')
如果任务比较多,就使用进程池
进程池multiprocessing.pool
Pool常用参数说明如下
- apply_async(func[, args[, kwds]]):使用非阻塞方式调用func(并行执行,堵塞方式必须等待上一个进程退出才能执行下一个进程),args为传递给func的参数列表,kwds为传递给func的关键字参数列表;
- close():关闭Pool,使其不再接受新的任务;
- terminate():不管任务是否完成,立即终止;
- join():主进程阻塞,等待子进程的结束, 必须在close或terminate之后使用
from multiprocessing import Pool
def job():
if __name__ == '__main__':
#指定进程池中的进程最多为5个
pool = Pool(5)
for i in range(1, 1000):
pool.apply_async(job)
pool.close()
pool.join()
print('子进程运行结束')
参考资料:https://www.cnblogs.com/banyanisdora/p/14313061.html
https://www.cnblogs.com/luyuze95/p/11289143.html
https://www.runoob.com/python3/python3-multithreading.html
https://www.cnblogs.com/Lin2396/p/11568300.html
https://www.cnblogs.com/blueberry-mint/p/13627325.html