发布网友 发布时间:2024-09-19 06:04
共1个回答
热心网友 时间:2024-09-29 16:28
导读:今天首席CTO笔记来给各位分享关于python线程多少秒完成的相关内容,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!
pythony中子进程如果一定时间不结束就停止应该如何做?你要用thread模块的话,要main里面sleep一段时间,如果子线程没结束,直接走main下面的代码,子线程会自动被杀掉的。当然,这也是Thread模块的缺点,不管子进程是否结束,一旦继续走主线程,子线程统统杀掉。
如果你用threading模块的话,只要不给子线程设定为守护进程也会在执行main后杀掉子线程。
你这问题有点怪,别人都是要求怎样在执行main线程的时候子线程也不被杀。。。。。。
给个例子吧:
importthread
fromtimeimportsleep,ctime
defloop0():
print'startloop0at:',ctime()
sleep(4)
print'loop0doneat:',ctime()
defloop1():
print'startloop1at:',ctime()
sleep(2)
print'loop1doneat:',ctime()
defmain():
print'startingat:',ctime()
thread.start_new_thread(loop0,())
thread.start_new_thread(loop1,())
sleep(6)
print'allDONEat:',ctime()
if__name__=='__main__':
main()
这是python核心编程里面的例子,主线程会等待6秒,6秒之后,就继续走main程序(也就是主线程)下的print"allDoneat:",ctime()了,子线程不管结没结束都杀掉。
为什么有人说Python的多线程是鸡肋
因为Python中臭名昭著的GIL。
那么GIL是什么?为什么会有GIL?多线程真的是鸡肋吗?GIL可以去掉吗?带着这些问题,我们一起往下看,同时需要你有一点点耐心。
多线程是不是鸡肋,我们先做个实验,实验非常简单,就是将数字“1亿”递减,减到0程序就终止,这个任务如果我们使用单线程来执行,完成时间会是多少?使用多线程又会是多少?showmethecode
单线程
在我的4核CPU计算机中,单线程所花的时间是6.5秒。可能有人会问,线程在哪里?其实任何程序运行时,默认都会有一个主线程在执行。(关于线程与进程这里不展开,我会单独开一篇文章)
多线程
创建两个子线程t1、t2,每个线程各执行5千万次减操作,等两个线程都执行完后,主线程终止程序运行。结果,两个线程以合作的方式执行是6.8秒,反而变慢了。按理来说,两个线程同时并行地运行在两个CPU之上,时间应该减半才对,现在不减反增。
是什么原因导致多线程不快反慢的呢?
原因就在于GIL,在Cpython解释器(Python语言的主流解释器)中,有一把全局解释锁(GlobalInterpreterLock),在解释器解释执行Python代码时,先要得到这把锁,意味着,任何时候只可能有一个线程在执行代码,其它线程要想获得CPU执行代码指令,就必须先获得这把锁,如果锁被其它线程占用了,那么该线程就只能等待,直到占有该锁的线程释放锁才有执行代码指令的可能。
因此,这也就是为什么两个线程一起执行反而更加慢的原因,因为同一时刻,只有一个线程在运行,其它线程只能等待,即使是多核CPU,也没办法让多个线程「并行」地同时执行代码,只能是交替执行,因为多线程涉及到上线文切换、锁机制处理(获取锁,释放锁等),所以,多线程执行不快反慢。
什么时候GIL被释放呢?
当一个线程遇到I/O任务时,将释放GIL。计算密集型(CPU-bound)线程执行100次解释器的计步(ticks)时(计步可粗略看作Python虚拟机的指令),也会释放GIL。可以通过设置计步长度,查看计步长度。相比单线程,这些多是多线程带来的额外开销
CPython解释器为什么要这样设计?
多线程是为了适应现代计算机硬件高速发展充分利用多核处理器的产物,通过多线程使得CPU资源可以被高效利用起来,Python诞生于1991年,那时候硬件配置远没有今天这样豪华,现在一台普通服务器32核64G内存都不是什么司空见惯的事
但是多线程有个问题,怎么解决共享数据的同步、一致性问题,因为,对于多个线程访问共享数据时,可能有两个线程同时修改一个数据情况,如果没有合适的机制保证数据的一致性,那么程序最终导致异常,所以,Python之父就搞了个全局的线程锁,不管你数据有没有同步问题,反正一刀切,上个全局锁,保证数据安全。这也就是多线程鸡肋的原因,因为它没有细粒度的控制数据的安全,而是用一种简单粗暴的方式来解决。
这种解决办法放在90年代,其实是没什么问题的,毕竟,那时候的硬件配置还很简陋,单核CPU还是主流,多线程的应用场景也不多,大部分时候还是以单线程的方式运行,单线程不要涉及线程的上下文切换,效率反而比多线程更高(在多核环境下,不适用此规则)。所以,采用GIL的方式来保证数据的一致性和安全,未必不可取,至少在当时是一种成本很低的实现方式。
那么把GIL去掉可行吗?
还真有人这么干多,但是结果令人失望,在1999年GregStein和MarkHammond两位哥们就创建了一个去掉GIL的Python分支,在所有可变数据结构上把GIL替换为更为细粒度的锁。然而,做过了基准测试之后,去掉GIL的Python在单线程条件下执行效率将近慢了2倍。
Python之父表示:基于以上的考虑,去掉GIL没有太大的价值而不必花太多精力。
关于python多线程的一些问题。创建的子线程默认是非守护的。
非守护:当主线程结束时,子线程继续运行,二者互不影响。
子线程是守护线程:当主线程结束时,子线程也结束(不管子线程工作有没有完成)。
join作用是线程同步,是让主线程等待子线程结束才结束(主线程完成工作了也不结束,阻塞等待,等子线程完成其工作才一起结束)。
相信此时你已经懂你的两个问题了。
没加join的时候主线程结束了,所以命令提示符就出来了,可是子线程还没结束,过了3/5秒后打印了字符串。加了join后主线程等两个子线程都结束才一起结束,所以最后才出来。
理解确实有点偏差。守护是指子线程守护着主线程,你死我也死,谓之守护。
Python多线程的一些问题python提供了两个模块来实现多线程thread和threading,thread有一些缺点,在threading得到了弥补,为了不浪费你和时间,所以我们直接学习threading就可以了。
继续对上面的例子进行改造,引入threadring来同时播放音乐和视频:
#coding=utf-8importthreadingfromtimeimportctime,sleepdefmusic(func):foriinrange(2):print"Iwaslisteningto%s.%s"%(func,ctime())
sleep(1)defmove(func):foriinrange(2):print"Iwasatthe%s!%s"%(func,ctime())
sleep(5)
threads=[]
t1=threading.Thread(target=music,args=(u'爱情买卖',))
threads.append(t1)
t2=threading.Thread(target=move,args=(u'阿凡达',))
threads.append(t2)if__name__=='__main__':fortinthreads:
t.setDaemon(True)
t.start()print"allover%s"%ctime()
importthreading
首先导入threading模块,这是使用多线程的前提。
threads=[]
t1=threading.Thread(target=music,args=(u'爱情买卖',))
threads.append(t1)
创建了threads数组,创建线程t1,使用threading.Thread()方法,在这个方法中调用music方法target=music,args方法对music进行传参。把创建好的线程t1装到threads数组中。
接着以同样的方式创建线程t2,并把t2也装到threads数组。
fortinthreads:
t.setDaemon(True)
t.start()
最后通过for循环遍历数组。(数组被装载了t1和t2两个线程)
setDaemon()
setDaemon(True)将线程声明为守护线程,必须在start()方法调用之前设置,如果不设置为守护线程程序会被无限挂起。子线程启动后,父线程也继续执行下去,当父线程执行完最后一条语句print"allover%s"%ctime()后,没有等待子线程,直接就退出了,同时子线程也一同结束。
start()
开始线程活动。
运行结果:
=========================RESTART================================
Iwaslisteningto爱情买卖.ThuApr1712:51:452014Iwasatthe阿凡达!ThuApr1712:51:452014alloverThuApr1712:51:452014
从执行结果来看,子线程(muisc、move)和主线程(print"allover%s"%ctime())都是同一时间启动,但由于主线程执行完结束,所以导致子线程也终止。
继续调整程序:
...if__name__=='__main__':fortinthreads:
t.setDaemon(True)
t.start()
t.join()print"allover%s"%ctime()
我们只对上面的程序加了个join()方法,用于等待线程终止。join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
注意:join()方法的位置是在for循环外的,也就是说必须等待for循环里的两个进程都结束后,才去执行主进程。
运行结果:
=========================RESTART================================
Iwaslisteningto爱情买卖.ThuApr1713:04:112014Iwasatthe阿凡达!ThuApr1713:04:112014Iwaslisteningto爱情买卖.ThuApr1713:04:122014Iwasatthe阿凡达!ThuApr1713:04:162014alloverThuApr1713:04:212014
从执行结果可看到,music和move是同时启动的。
开始时间4分11秒,直到调用主进程为4分22秒,总耗时为10秒。从单线程时减少了2秒,我们可以把music的sleep()的时间调整为4秒。
...defmusic(func):foriinrange(2):print"Iwaslisteningto%s.%s"%(func,ctime())
sleep(4)
...
子线程启动11分27秒,主线程运行11分37秒。
虽然music每首歌曲从1秒延长到了4,但通多程线的方式运行脚本,总的时间没变化。
Python面试题,线程与进程的区别,Python中如何创建多线程?进程和线程
这两个概念属于操作系统,我们经常听说,但是可能很少有人会细究它们的含义。对于工程师而言,两者的定义和区别还是很有必要了解清楚的。
首先说进程,进程可以看成是CPU执行的具体的任务。在操作系统当中,由于CPU的运行速度非常快,要比计算机当中的其他设备要快得多。比如内存、磁盘等等,所以如果CPU一次只执行一个任务,那么会导致CPU大量时间在等待这些设备,这样操作效率很低。为了提升计算机的运行效率,把机器的技能尽可能压榨出来,CPU是轮询工作的。也就是说它一次只执行一个任务,执行一小段碎片时间之后立即切换,去执行其他任务。
所以在早期的单核机器的时候,看起来电脑也是并发工作的。我们可以一边听歌一边上网,也不会觉得卡顿。但实际上,这是CPU轮询的结果。在这个例子当中,听歌的软件和上网的软件对于CPU而言都是独立的进程。我们可以把进程简单地理解成运行的应用,比如在安卓手机里面,一个app启动的时候就会对应系统中的一个进程。当然这种说法不完全准确,一个应用也是可以启动多个进程的。
进程是对应CPU而言的,线程则更多针对的是程序。即使是CPU在执行当前进程的时候,程序运行的任务其实也是有分工的。举个例子,比如听歌软件当中,我们需要显示歌词的字幕,需要播放声音,需要监听用户的行为,比如是否发生了切歌、调节音量等等。所以,我们需要进一步拆分CPU的工作,让它在执行当前进程的时候,继续通过轮询的方式来同时做多件事情。
进程中的任务就是线程,所以从这点上来说,进程和线程是包含关系。一个进程当中可以包含多个线程,对于CPU而言,不能直接执行线程,一个线程一定属于一个进程。所以我们知道,CPU进程切换切换的是执行的应用程序或者是软件,而进程内部的线程切换,切换的是软件当中具体的执行任务。
关于进程和线程有一个经典的模型可以说明它们之间的关系,假设CPU是一家工厂,工厂当中有多个车间。不同的车间对应不同的生产任务,有的车间生产汽车轮胎,有的车间生产汽车骨架。但是工厂的电力是有限的,同时只能满足一个厂房的使用。
为了让大家的进度协调,所以工厂需要轮流提供各个车间的供电。这里的车间对应的就是进程。
一个车间虽然只生产一种产品,但是其中的工序却不止一个。一个车间可能会有好几条流水线,具体的生产任务其实是流水线完成的,每一条流水线对应一个具体执行的任务。但是同样的,车间同一时刻也只能执行一条流水线,所以我们需要车间在这些流水线之间切换供电,让各个流水线生产进度统一。
这里车间里的流水线自然对应的就是线程的概念,这个模型很好地诠释了CPU、进程和线程之间的关系。实际的原理也的确如此,不过CPU中的情况要比现实中的车间复杂得多。因为对于进程和CPU来说,它们面临的局面都是实时变化的。车间当中的流水线是x个,下一刻可能就成了y个。
了解完了线程和进程的概念之后,对于理解电脑的配置也有帮助。比如我们买电脑,经常会碰到一个术语,就是这个电脑的CPU是某某核某某线程的。比如我当年买的第一台笔记本是4核8线程的,这其实是在说这台电脑的CPU有4个计算核心,但是使用了超线程技术,使得可以把一个物理核心模拟成两个逻辑核心。相当于我们可以用4个核心同时执行8个线程,相当于8个核心同时执行,但其实有4个核心是模拟出来的虚拟核心。
有一个问题是为什么是4核8线程而不是4核8进程呢?因为CPU并不会直接执行进程,而是执行的是进程当中的某一个线程。就好像车间并不能直接生产零件,只有流水线才能生产零件。车间负责的更多是资源的调配,所以教科书里有一句非常经典的话来诠释:进程是资源分配的最小单元,线程是CPU调度的最小单元。
启动线程
Python当中为我们提供了完善的threading库,通过它,我们可以非常方便地创建线程来执行多线程。
首先,我们引入threading中的Thread,这是一个线程的类,我们可以通过创建一个线程的实例来执行多线程。
fromthreadingimportThreadt=Thread(target=func,name='therad',args=(x,y))t.start()
简单解释一下它的用法,我们传入了三个参数,分别是target,name和args,从名字上我们就可以猜测出它们的含义。首先是target,它传入的是一个方法,也就是我们希望多线程执行的方法。name是我们为这个新创建的线程起的名字,这个参数可以省略,如果省略的话,系统会为它起一个系统名。当我们执行Python的时候启动的线程名叫MainThread,通过线程的名字我们可以做区分。args是会传递给target这个函数的参数。
我们来举个经典的例子:
importtime,threading#新线程执行的代码:defloop(n):print('thread%sisrunning...'%threading.current_thread().name)foriinrange(n):print('thread%s%s'%(threading.current_thread().name,i))time.sleep(5)print('thread%sended.'%threading.current_thread().name)print('thread%sisrunning...'%threading.current_thread().name)t=threading.Thread(target=loop,name='LoopThread',args=(10,))t.start()print('thread%sended.'%threading.current_thread().name)
我们创建了一个非常简单的loop函数,用来执行一个循环来打印数字,我们每次打印一个数字之后这个线程会睡眠5秒钟,所以我们看到的结果应该是每过5秒钟屏幕上多出一行数字。
我们在Jupyter里执行一下:
表面上看这个结果没毛病,但是其实有一个问题,什么问题呢?输出的顺序不太对,为什么我们在打印了第一个数字0之后,主线程就结束了呢?另外一个问题是,既然主线程已经结束了,为什么Python进程没有结束,还在向外打印结果呢?
因为线程之间是独立的,对于主线程而言,它在执行了t.start()之后,并不会停留,而是会一直往下执行一直到结束。如果我们不希望主线程在这个时候结束,而是阻塞等待子线程运行结束之后再继续运行,我们可以在代码当中加上t.join()这一行来实现这点。
t.start()t.join()print('thread%sended.'%threading.current_thread().name)
join操作可以让主线程在join处挂起等待,直到子线程执行结束之后,再继续往下执行。我们加上了join之后的运行结果是这样的:
这个就是我们预期的样子了,等待子线程执行结束之后再继续。
我们再来看第二个问题,为什么主线程结束的时候,子线程还在继续运行,Python进程没有退出呢?这是因为默认情况下我们创建的都是用户级线程,对于进程而言,会等待所有用户级线程执行结束之后才退出。这里就有了一个问题,那假如我们创建了一个线程尝试从一个接口当中获取数据,由于接口一直没有返回,当前进程岂不是会永远等待下去?
这显然是不合理的,所以为了解决这个问题,我们可以把创建出来的线程设置成守护线程。
守护线程
守护线程即daemon线程,它的英文直译其实是后台驻留程序,所以我们也可以理解成后台线程,这样更方便理解。daemon线程和用户线程级别不同,进程不会主动等待daemon线程的执行,当所有用户级线程执行结束之后即会退出。进程退出时会kill掉所有守护线程。
我们传入daemon=True参数来将创建出来的线程设置成后台线程:
t=threading.Thread(target=loop,name='LoopThread',args=(10,),daemon=True)
这样我们再执行看到的结果就是这样了:
这里有一点需要注意,如果你在jupyter当中运行是看不到这样的结果的。因为jupyter自身是一个进程,对于jupyter当中的cell而言,它一直是有用户级线程存活的,所以进程不会退出。所以想要看到这样的效果,只能通过命令行执行Python文件。
如果我们想要等待这个子线程结束,就必须通过join方法。另外,为了预防子线程锁死一直无法退出的情况,我们还可以在joih当中设置timeout,即最长等待时间,当等待时间到达之后,将不再等待。
比如我在join当中设置的timeout等于5时,屏幕上就只会输出5个数字。
另外,如果没有设置成后台线程的话,设置timeout虽然也有用,但是进程仍然会等待所有子线程结束。所以屏幕上的输出结果会是这样的:
虽然主线程继续往下执行并且结束了,但是子线程仍然一直运行,直到子线程也运行结束。
关于join设置timeout这里有一个坑,如果我们只有一个线程要等待还好,如果有多个线程,我们用一个循环将它们设置等待的话。那么主线程一共会等待N*timeout的时间,这里的N是线程的数量。因为每个线程计算是否超时的开始时间是上一个线程超时结束的时间,它会等待所有线程都超时,才会一起终止它们。
比如我这样创建3个线程:
ths=[]foriinrange(3):t=threading.Thread(target=loop,name='LoopThread'+str(i),args=(10,),daemon=True)ths.append(t)fortinths:t.start()fortinths:t.join(2)
最后屏幕上输出的结果是这样的:
所有线程都存活了6秒。
总结
在今天的文章当中,我们一起简单了解了操作系统当中线程和进程的概念,以及Python当中如何创建一个线程,以及关于创建线程之后的相关使用。
多线程在许多语言当中都是至关重要的,许多场景下必定会使用到多线程。比如web后端,比如爬虫,再比如游戏开发以及其他所有需要涉及开发ui界面的领域。因为凡是涉及到ui,必然会需要一个线程单独渲染页面,另外的线程负责准备数据和执行逻辑。因此,多线程是专业程序员绕不开的一个话题,也是一定要掌握的内容之一。
结语:以上就是首席CTO笔记为大家整理的关于python线程多少秒完成的相关内容解答汇总