Python多线程的一些理解:
1.多线程采用的是分时复用技术,即不存在真正的多线程,cpu做的事是快速地切换线程,以达到类似同步运行的目的(对于多核CPU可实现真正的多线程);
2.多线程对于计算密集型程序没有用,因为计算计算密集型程序没有等待,是连续计算的,但对I/O密集型程序(延迟IO,网络等)有用,因为这类程序有等待,等待时就可以切换其他线程,避免浪费时间。
3.原子操作:最小的操作步骤,这件事情是不可再分的,如变量的赋值,不可能一个线程在赋值,到一半切到另外一个线程工作去了……但是一些数据结构的操作,如栈的push什么的,并非是原子操作,比如要经过栈顶指针上移、赋值、计数器加1等等,在其中的任何一步中断,切换到另一线程再操作这个栈时,就会产生严重的问题,因此要使用锁来避免这样的情况。比如加锁后的push操作就可以认为是原子的了……
4.阻塞:所谓阻塞,就是只执行某些线程,而让其他线程等待,等待的线程就是被阻塞的线程,一直到这些线程执行结束。最简单的例子就是某一线程在原子操作下,则其它线程都是阻塞状态,这是微观的情况。对于宏观的情况,比如服务器等待用户连接,如果始终没有连接,那么这个线程就在阻塞状态。同理,最简单的input语句,在等待输入时也是阻塞状态。
5.在创建线程后,如果只执行t.start(),则这个线程t是非阻塞的,即主线程会继续执行其他的指令,相当于主线程和子线程都并行地执行。t.start()后,若启动t.join(),则t就是阻塞的,即只有当t结束后才执行其他指令。
#该段函数是后面所有函数的公共部分,定义了猫和狗的叫声
import threading, time
def cat(num): #猫叫
print('cat_start:' + time.strftime('%H:%M:%S'))
for i in range(num):
print(" cat:喵 "+ time.strftime('%H:%M:%S'))
time.sleep(1)
print('cat_stop:' + time.strftime('%H:%M:%S'))
def dog(num): #狗叫
print('dog_start:' + time.strftime('%H:%M:%S'))
for i in range(num):
print(" dog:汪 "+ time.strftime('%H:%M:%S'))
time.sleep(1)
print('dog_stop:' + time.strftime('%H:%M:%S'))
上面程序定义了猫和狗的叫声。
#case 1
if __name__ == "__main__":
cat(3)
dog(3)
print('print:'+ time.strftime('%H:%M:%S'))
输出:
cat_start:14:17:34
cat:喵 14:17:34
cat:喵 14:17:35
cat:喵 14:17:36
cat_stop:14:17:37
dog_start:14:17:37
dog:汪 14:17:37
dog:汪 14:17:38
dog:汪 14:17:39
dog_stop:14:17:40
Over:14:17:40
上述程序按常规方法执行猫和狗的叫声,根据时间可以看出,完全是顺序执行的,即先猫叫,再狗叫,最后是print()。总时间为:16s
#case 2
if __name__ == "__main__":
thread1 = threading.Thread(target = cat,args=(3,)) #让第一个线程执行“猫叫”
thread1.start() #启动猫叫线程
thread2 = threading.Thread(target = dog,args=(3,)) #让第二个线程执行“狗叫”
thread2.start() #启动狗叫线程
print('over:'+ time.strftime('%H:%M:%S'))
输出:
cat_start:14:26:22
cat:喵 14:26:22
dog_start:14:26:22
dog:汪 14:26:22
over:14:26:22
cat:喵 14:26:23
dog:汪 14:26:23
cat:喵 14:26:24
dog:汪 14:26:24
cat_stop:14:26:25
dog_stop:14:26:25
上述程序利用“多线程”的方式执行程序,从结果可以看出,猫、狗、print()三者在14:26:22时刻几乎同时启动,即几乎是并行执行,总耗时只要:3s,相比于前面的常规方法,节省了大量时间。
注意:
(1) x.start()是必须的,否则线程不会启动。
(2) 虽然从代码位置而言,thread1、thread2、print()有前后顺序,但实际上几乎是同时执行的,这说明,在无指定的情况下,代码位置不会影响多线程之间的执行先后顺序。
(3) 这里我们显式指定了线程专门用来执行猫和狗的叫,但显然print()函数也是同时刻就启动的,这说明print()函数也是由一个独立的线程来执行的。实际上启动python后自动启动一个主线程,其他线程都在该主线程的基础上执行,当关闭python后自动退出主线程,并杀死子线程(如前面的猫、狗、print())(该句话还有待考证)。
(4)上述几个多线程的执行时间我们按秒为单位输出,结果三者相同,但如果把时间单位再细化,其实这三者并非真正在相同时刻启动,这是因为多线程并非真正意义上的并行,只是多个线程在短时间内的相互切换,减少等待时间。
#case 3
if __name__ == "__main__":
threads = [] #线程列表
thread1 = threading.Thread(target = cat,args=(3,))
threads.append(thread1) #将第一个线程放入线程列表
thread2 = threading.Thread(target = dog,args=(3,))
threads.append(thread2) #将第二个线程放入线程列表
for i in threads: #一起启动所有线程
i.start()
print('over:'+ time.strftime('%H:%M:%S'))
输出:
cat_start:14:54:40
cat:喵 14:54:40
dog_start:14:54:40
dog:汪 14:54:40
over:14:54:40
cat:喵 14:54:41
dog:汪 14:54:41
cat:喵 14:54:42
dog:汪 14:54:42
cat_stop:14:54:43
dog_stop:14:54:43
上述程序将产生的多线程加入线程列表threads = [],并用for循环一起启动。当有很多线程需要启动时,这种方式是高效的,避免为每个线程写x.start()语句。从结果可以看出,虽然我们自定义的线程(猫叫,狗叫)在list中有先后顺序,但各个线程依然是同时执行的。而且,在for循环之外的print()也与猫和狗同时执行,原因前面已经提及,在主线程中子线程猫、狗、print()有完全相同的地位。
线程的join()方法
作用:使主程序进入阻塞状态(等待状态),一直等启动join()方法的子线程执行结束之后,再执行其他线程。如t.start(),t.join()则,线程t将优先执行,其他线程被阻塞,直到t执行结束,阻塞自动取消。
python中不仅线程有join()方法,其他很多模块都有join()方法(如进程、queue等),其基本作用都是用于阻塞其他程序执行,保证本程序优先执行。
join()不带时间参数
#case 4
if __name__ == "__main__":
thread1 = threading.Thread(target = cat,args=(3,))
thread1.start()
thread1.join() #线程1启动了join()方法
thread2 = threading.Thread(target = dog,args=(3,))
thread2.start()
thread2.join() #线程2也启动了join()方法
print('over:'+ time.strftime('%H:%M:%S'))
输出:
cat_start:15:44:04
cat:喵 15:44:04
cat:喵 15:44:05
cat:喵 15:44:06
cat_stop:15:44:07
dog_start:15:44:07
dog:汪 15:44:07
dog:汪 15:44:08
dog:汪 15:44:09
dog_stop:15:44:10
over:15:44:10
上述程序两个线程先后启动了join方法,从输出结果可以看出,跟case1完全相同,变成了顺序执行,没有起到多线程的作用,因为thread1启动后,立即执行thread1.join(),使得其他线程立即进入阻塞状态,直到thread1执行结束后再取消阻塞,后面的thread2也相同。继续看下例。
#case 5
if __name__ == "__main__":
thread1 = threading.Thread(target = cat,args=(3,))
thread1.start()
thread1.join()
thread2 = threading.Thread(target = dog,args=(3,))
thread2.start()
#thread2.join()
print('over:'+ time.strftime('%H:%M:%S'))
输出:
cat_start:15:47:12
cat:喵 15:47:12
cat:喵 15:47:13
cat:喵 15:47:14
cat_stop:15:47:15
dog_start:15:47:15
dog:汪 15:47:15
over:15:47:15
dog:汪 15:47:16
dog:汪 15:47:17
dog_stop:15:47:18
上述程序只有线程1启动了join()方法,而且在狗和print()都start()之前,因此先执行“猫”叫,猫叫全部结束后才启动“狗”和print()。注意因为线程2(狗)并没有启用join(),因此狗和print()是同时启动的。
#case 6
if __name__ == "__main__":
thread1 = threading.Thread(target = cat,args=(3,))
thread1.start()
#thread1.join()
thread2 = threading.Thread(target = dog,args=(3,))
thread2.start()
thread2.join()
print('over:'+ time.strftime('%H:%M:%S'))
输出:
cat_start:15:58:07
cat:喵 15:58:07
dog_start:15:58:07
dog:汪 15:58:07
cat:喵 15:58:08
dog:汪 15:58:08
cat:喵 15:58:09
dog:汪 15:58:09
cat:喵 15:58:10
dog_stop:15:58:10
over:15:58:10
cat:喵 15:58:11
cat_stop:15:58:12
上例中猫叫改成了5声,同时只启动了线程2的join()方法,但该方法在线程1和线程2都start()之后,因此猫和狗是同时叫的,而print()位于线程2的join()之后,因此要等狗叫完了才能执行print()。但注意到狗只要叫3声,而猫得叫5声,因此狗叫完了之后就没狗啥事了,于是就立即启动print()输出,而print()只需极短的时间就能完成,此时猫还没叫完,于是如上述结果,猫停止叫声在最后。
#case 7
if __name__ == "__main__":
threads = [] #线程列表
thread1 = threading.Thread(target = cat,args=(3,))
thread1.start() #启动第一个线程
threads.append(thread1) #将第一个线程放入线程列表
thread2 = threading.Thread(target = dog,args=(3,))
thread2.start() #启动第二个线程
threads.append(thread2) #将第二个线程放入线程列表
for i in threads: #一起启动所有join()方法
i.join()
print('over:'+ time.strftime('%H:%M:%S'))
输出:
cat_start:16:35:38
cat:喵 16:35:38
dog_start:16:35:38
dog:汪 16:35:38
cat:喵 16:35:39
dog:汪 16:35:39
cat:喵 16:35:40
dog:汪 16:35:40
cat_stop:16:35:41
dog_stop:16:35:41
over:16:35:41
上述程序先将所有线程先加入线程列表,再一起执行join()函数,而print()位于最后。从结果可以看出,猫和狗确实是同时叫的(在join()之间,猫和狗都start()了),都叫完之后才执行print(),因为有i.join()的存在。注意上述程序的两个start()虽然在代码位置上有先后,但实际上几乎是同时执行的,前面已经说明。当自定义线程很多时,通常用for形式统一启动,因此,多线程通常会写成以下一般形式:
#case 8
#★★
if __name__ == "__main__":
threads = []
thread1 = threading.Thread(target = cat,args=(3,))
threads.append(thread1) #将线程1加入线程列表
thread2 = threading.Thread(target = dog,args=(3,))
threads.append(thread2) #将线程2加入线程列表
for i in threads: #先并行启动所有线程
i.start()
for i in threads: #确保所有这些线程都执行完,再执行后面程序
i.join()
print('over:'+ time.strftime('%H:%M:%S')) #最后再执行print()
输出:
cat_start:16:41:37
cat:喵 16:41:37
dog_start:16:41:37
dog:汪 16:41:37
cat:喵 16:41:38
dog:汪 16:41:38
cat:喵 16:41:39
dog:汪 16:41:39
cat_stop:16:41:40
dog_stop:16:41:40
over:16:41:40
★★上述程序首先将自定义的线程加入线程列表,然后用一个for循环启动每个线程(特别注意:虽然是循环执行,但是所有线程几乎是并行启动执行的,而非顺序执行);再用一个for循环为每个线程启用join()方法,保证所有线程都执行完后再执行这些线程之外的其他步骤。注意这两个for循环不能写在一起,否则就成了顺序执行,使用多线程将变得无意义。但这不是说多线程一定比单线程运行时间要快,因为有GiL的存在,具体见相关文章!
join(timeout=)带时间参数
#case 9
if __name__ == "__main__":
thread1 = threading.Thread(target = cat,args=(3,))
thread1.start()
thread1.join(0.2)
thread2 = threading.Thread(target = dog,args=(3,))
thread2.start()
print('over:'+ time.strftime('%H:%M:%S')) #最后再执行print()
输出:
cat_start:17:09:56
cat:喵 17:09:56
dog_start:17:09:56
dog:汪 17:09:56
over:17:09:56
cat:喵 17:09:57
dog:汪 17:09:57
cat:喵 17:09:58
dog:汪 17:09:58
cat_stop:17:09:59
dog_stop:17:09:59
上述程序中,线程1的join()方法启动,如果不带参数,则先等猫叫完了再执行狗和print(),但此处带了时间参数timeout=0.2秒,即线程1启动0.2s之后即可执行其他程序),因此输出结果也是猫、狗、print()同时的。如果将0.2换成6,则先执行猫,再同时执行狗和print()。需要注意的是,某线程的执行时间run_time和timeout是两个概念,如果run_time>timeout,即某线程的运行时间大于阻塞时间,则timeout时间后该线程继续执行,同时启动其他线程并行执行;如果run_time<timeout,即运行时间小于阻塞时间,则一旦该线程运行结束就直接执行其他线程,而不会继续白等timeout的剩余时间(避免时间浪费)。(即如果主线程等待timeout,子线程还没有结束,则主线程强制结束子线程???????????前述结论与此相违背)
线程的setDaemon()方法
作用:使对应线程在主线程结束后强制结束,不管该线程处于什么状态。
注意:python程序执行结束并不表示主线程结束,要关闭python才真正结束主线程。
python 多线程为什么有时候比顺序执行还慢?
来源:https://blog.csdn.net/qq_39338671/article/details/87457812
写出了正确的多线程代码,运行速度反而比单线程慢很多,原来是由于GIL(Global Interpreter Lock)!
GIL 是Cpython(Python语言的主流解释器)特有的全局解释器锁(其他解释器因为有自己的线程调度机制,所以没有GIL机制),GIL锁定Python线程中的CPU执行资源。线程在执行代码时,必须先获得这把锁,才获得CPU执行代码指令。如果这把锁被其他线程占用,该线程就只能等待,等到占有该锁的线程释放锁。
在Cpython解释器中,线程要想执行CPU指令需要2个条件:
1)被操作系统调度出来(操作系统允许它占用CPU)
2)获取到GIL(Cpython解释器允许它执行指令)
如果写出正确的多线程代码,执行的情况就是会有线程满足条件1不满足条件2,这时只能等待。在单核CPU机器上,多线程与单线程在本质上并无不同,因为所有线程都是轮流占用CPU。多个线程慢于一个线程,因为其他线程还要先调度出来,再等待。在多核CPU机器上,多线程代码运行性能会非常糟糕,比单核更糟糕。因为这时候多一个步骤,不同的CPU再竞争GIL,GIL只有一个。Python在多核CPU上的多核CPU也只有单线程在跑程序。
我们用的主流python叫cpython,在同一时刻,多个线程运行是相互抢占资源允许的,cpython无法把线程分配到多个CPU运行,就造成了计算密集型无法使用多个CPU 同时运行。这是由于cpython在运行的时候就加了一把锁(GIL),这是一个历史问题。说白了python是没有多线程,因为同一时刻只能运行一个线程(多个线程分配到多个CPU运行,才是真正意义上面多线程,python无法做到。
如何绕开GIL?
1)使用多进程(多进程之间没有GIL限制)
2)使用Jython, IronPython等无GIL的解释器
3)使用协程(高效的单线程模式)
GIL的设置有其优点和可取之处,在Cpython解释器框架之下难以绕过这一限制。可以用PyPy解释器,麻烦之处在于很多第三方库在PyPy下无法使用,或者重新安装第三方库的PyPy版本。运行时候,PyPy **.py即可。Cpython下是Python **.py。
什么时候用多线程?
I/O密集型程序
I/O的多线程还是快于单线程,因为优先级在获取GIL之上,I/O并行运算的时候,GIL处于release状态。
爬虫大部分时间在网络交互上,所以可以使用多线程来编写爬虫。
使用多线程的缺点就是要注意死锁的问题、阻塞的问题、以及需要注意多线程之间通信的问题,避免多个线程执行同一个任务。
Python多线程主要是为了提高程序在IO方面的优势,在爬虫的过程中显得尤为重要。
CPU密集型 vs IO密集型
我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
总之,计算密集型程序适合C语言多线程,I/O密集型适合脚本语言开发的多线程。
注意:程序中如果有使CPU等待的操作,如python中的time.sleep(),则该程序也是I/O密集型程序。
计算密集型程序实例:
#计算密集型程序,采用单线程执行
from threading import Thread
import time
def my_counter(): #主程序为计算密集型,只有i +=1一个计算步骤,只需用CPU即可
i = 0
for _ in range(100000000):
i += 1
#time.sleep(0.0001)
def main():
start_time = time.time()
#单线程执行
for _ in range(2):
t = Thread(target=my_counter)
t.start()
t.join() #start()后直接join(),是确保当前线程先执行结束,相当于单线程
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
输出:
Total time: 10.542109727859497
#计算密集型程序,采用多线程执行
from threading import Thread
import time
def my_counter(): #主程序为计算密集型,只有i +=1一个计算步骤,只需用CPU即可
i = 0
for _ in range(100000000):
i += 1
#time.sleep(0.0001)
def main():
start_time = time.time()
#多线程执行
threads = []
for _ in range(2):
t = Thread(target=my_counter)
t.start()
threads.append(t)
for i in threads: #对所有线程统一执行join(),是多线程
i.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
输出:
Total time: 11.222445964813232
上述两个程序的主程序都是“计算密集”的(因为my_counter()内只有计算步骤i += 1。前一个程序用两个线程“串行”地执行两次my_counter()(因为start()后直接启用对应线程的join()方法),即相当于for里面顺序地调用了两次my_counter(),也就是“单线程”,总耗时为10.542s;后一个程序与前一个程序除了采用“多线程”方式执行2次my_counter()(因为start()后没有直接启用join()方法,而是后面统一在for内部对所有线程启动join())外,其他与第一个程序完全相同,总耗时为11.222s。可见,对于计算密集型程序,python的多线程比单线程反而耗时多更,这种情况下不适合用多线程。
I/O密集型程序实例:
在前述计算密集型程序的基础上,启用my_counter()函数内的time.sleep(0.0001)函数,则程序就从计算密集型程序转变成了I/O密集型程序,同时将循环参数由100000000改为10000(不然计算时间太长),则单线程(第一个程序)的总耗时为38.314s,多线程(第二个程序)的总耗时为19.287s。可见对于I/O密集型程序,python多线程确实能节省时间。
参考网址:https://blog.csdn.net/myhao846707/article/details/26576271
网友评论