Python “多线程”及其适用范围和缺点

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

线程的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

你可能感兴趣的:(Python “多线程”及其适用范围和缺点)