Python全栈开发-Day9-线程/GIL/事件/队列

本节内容

  1. 进程与线程的概念
  2. Python threading 模块
  3. GIL——global interpreter lock
  4. Mutex互斥锁(线程锁)
  5. Semaphore信号量
  6. Events事件
  7. Queue队列

 

1、进程与线程的概念

程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。

在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。正是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

有了进程为什么还要线程?

进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。但进程还是有很多缺陷的,主要体现为:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。

  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

什么是进程(process)?

对各种资源管理的集合,称之为进程。各类资源包括:内存的调用、网卡的调用等等。

每一个应用程序就是一个进程。

一个进程至少包含一个线程。

正在运行的进程都有唯一的一个ID号,即PID。如下图:

操作系统调用进程的时候,不会用进程名称进行调用,而是用唯一的PID进行调用。

什么是线程(thread)?

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并发的执行不同的任务。

所以线程可以简单的理解为:  线程 = 一堆指令

【注意】:进程要操作CPU,必须先创建一个线程。

总结:进程就是各种资源的集合,线程是一堆指令。进程包含一个或多个线程,进程的操作是要靠线程进行的。

 

进程、线程与内存的关系:

  进程的内存空间相对独立,进程之间不能互相访问对方的内存空间。

  所有在同一个进程里的线程是共享同一块内存空间的。

 

进程与线程的区别?

Q1:进程快还是线程快?

A:这没有可比性,进程是资源的集合,线程是一堆指令,进程想要执行也是依靠线程进行的。

Q2:启动一个进程快还是启动一个线程快?

A:肯定是启动一个线程快了。启动进程需要向OS申请各种资源,但是启动线程就是生成一堆指令,一下子就出来了。

区别:

  1、线程共享内存空间,进程的内存是相互独立的。

  2、同一个父进程创建的子进程之间,内存空间相互独立。但同一个父进程创建的子线程之间,内存空间共享。

  3、同一个进程的线程之间可以直接交流;两个进程之间想通信,必须通过一个中间代理。

  4、创建新线程很简单,创建新进程需要对其父进程进行克隆

  5、一个线程可以控制和操作同一个进程里的其他线程,但进程只能操作子进程。

  6、对主线程的修改,可能会影响同一个进程里的其他线程的行为。但是对父进程的修改不会影响其他子进程。

 

什么时候使用多线程:

  I/O操作不占CPU

  计算占用CPU

python多线程不适合CPU密集操作型的任务。适合I/O操作密集型的任务。

因为python多线程是伪多线程,其实是在不同的线程之间进行切换,切换就要保持当前线程状态,读取下一线程状态,这些操作会增加CPU负载。所以对于本身就是CPU密集型的任务而言并不适合多线程。

 

2、Python threading模块

线程有2种调用方式,如下:

直接调用(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import  threading
import  time
 
def  sayhi(num):  #定义每个线程要运行的函数
 
     print ( "running on number:%s"  % num)
 
     time.sleep( 3 )
 
if  __name__  = '__main__' :
 
     t1  threading.Thread(target = sayhi,args = ( 1 ,))  #生成一个线程实例
     t2  threading.Thread(target = sayhi,args = ( 2 ,))  #生成另一个线程实例
 
     t1.start()  #启动线程
     t2.start()  #启动另一个线程
 
     print (t1.getName())  #获取线程名
     print (t2.getName())

继承式调用(不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import  threading
import  time
 
 
class  MyThread(threading.Thread):
     def  __init__( self ,num):
         threading.Thread.__init__( self )
         self .num  num
 
     def  run( self ): #定义每个线程要运行的函数
 
         print ( "running on number:%s"  % self .num)
 
         time.sleep( 3 )
 
if  __name__  = '__main__' :
 
     t1  MyThread( 1 )
     t2  MyThread( 2 )
     t1.start()
     t2.start()

这里会出现一个问题:

在用time模块统计一个主线程运行时间时,由于多线程独立运行,所以这里的计时器只能统计主线程运行时间,这个时间不包括子线程运行时间,即不管子线程是否执行完毕,只要主线程执行完毕,计时器就得出结果。

在使用time模块计时时需要注意这个坑。。。。。。

如果非要在主线程中计算所有线程执行的时间的话,可以先让主线程等待子线程的执行结果,然后再计算时间。这里用到的方法是.join()

例如,我想等待t1这个子线程的执行结果,就在主线程中使用指令:  t1.join()

 

如果用循环的方式启动子线程,会出现一个问题,没办法对子线程进行命名,或者说没办法给子线程门牌号。这样不利于后段程序的调用。

这时只需要在实例化子线程时使用一个临时变量,然后通过.append()方法,把每个子线程对应的对象加入之前设置好的列表中即可。

然后用for循环遍历存放子线程的列表,即可按顺序取出之前新建的子线程对象。

 

主线程就是程序本身,自己是看不到的。但是可以通过一个命令来证明,.py文件就是主线程。

  print(threading.current_thread())   #打印当前线程

如果结果中出现 MainThread就是表示这个线程是主线程。没有则表示是子线程。

 

守护线程

守护线程相当于主线程的仆人。当主线程执行完毕后,不管守护线程是否执行完毕,程序都会退出。

主程序退出前,会等待所有的非守护线程执行完毕再退出。

 t.setDaemon(True)  #把t这个子线程设置为守护线程。

上面这条指令一定要在子线程开始之前,即t.star()指令之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#_*_coding:utf-8_*_
__author__  'Alex Li'
 
import  time
import  threading
 
 
def  run(n):
 
     print ( '[%s]------running----n'  n)
     time.sleep( 2 )
     print ( '--done--' )
 
def  main():
     for  in  range ( 5 ):
         threading.Thread(target = run,args = [i,])
         t.start()
         t.join( 1 )
         print ( 'starting thread' , t.getName())
 
 
threading.Thread(target = main,args = [])
m.setDaemon( True #将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时,m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务
m.start()
m.join(timeout = 2 )
print ( "---main thread done----" )

  

3、Python GIL(Global Interpreter Lock)  

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

上面的核心意思就是,无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行。

所以我们之前说的多线程其实是伪多线程,本质也是CPU在代码部分的上下文切换。只是这种切换太快了。

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

这篇文章透彻的剖析了GIL对python多线程的影响:http://www.dabeaz.com/python/UnderstandingGIL.pdf 

 

4、Mutex互斥锁(线程锁

一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import  time
import  threading
 
def  addNum():
     global  num  #在每个线程中都获取这个全局变量
     print ( '--get num:' ,num )
     time.sleep( 1 )
     num   - = #对此公共变量进行-1操作
 
num  100   #设定一个共享变量
thread_list  []
for  in  range ( 100 ):
     threading.Thread(target = addNum)
     t.start()
     thread_list.append(t)
 
for  in  thread_list:  #等待所有线程执行完毕
     t.join()
 
 
print ( 'final num:' , num )

正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0。这个原因很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处理完的结果是99,但此时B线程运算完的结果也是99,两个线程同时将CPU运算的结果再赋值给num变量后,结果就都是99。所以每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。 

*注:python3.x上的结果总是正确的,可能是自动加了锁。

所以多个线程同时修改同一份数据时,必须加互斥锁即Mutex。

加锁版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import  time
import  threading
 
def  addNum():
     global  num  #在每个线程中都获取这个全局变量
     print ( '--get num:' ,num )
     time.sleep( 1 )
     lock.acquire()  #修改数据前加锁
     num   - = #对此公共变量进行-1操作
     lock.release()  #修改后释放
 
num  100   #设定一个共享变量
thread_list  []
lock  threading.Lock()  #生成全局锁
for  in  range ( 100 ):
     threading.Thread(target = addNum)
     t.start()
     thread_list.append(t)
 
for  in  thread_list:  #等待所有线程执行完毕
     t.join()
 
print ( 'final num:' , num )

 

GIL VS Lock 

Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 这里的lock是用户级的lock,跟那个GIL没关系 。原因如下图:

加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在写的python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时自己写的程序里的线程和 py解释器自己的线程是并发运行的,假设自己的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题。

 

RLock(递归锁)

就是在一个大锁中还要再包含子锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import  threading,time
 
def  run1():
     print ( "grab the first part data" )
     lock.acquire()
     global  num
     num  + = 1
     lock.release()
     return  num
def  run2():
     print ( "grab the second part data" )
     lock.acquire()
     global   num2
     num2 + = 1
     lock.release()
     return  num2
def  run3():
     lock.acquire()
     res  run1()
     print ( '--------between run1 and run2-----' )
     res2  run2()
     lock.release()
     print (res,res2)
 
 
if  __name__  = '__main__' :
 
     num,num2  0 , 0
     lock  threading.RLock()
     for  in  range ( 10 ):
         threading.Thread(target = run3)
         t.start()
 
while  threading.active_count() ! 1 :
     print (threading.active_count())
else :
     print ( '----all threads done---' )
     print (num,num2)

上述代码中,如果用lock threading.Lock()代替lock threading.RLock(),会出现程序把解锁的钥匙弄混,导致死循环,永远出不去的情况。

所以当连续锁很多次时,就需要使用递归锁。

 

5、Semaphore(信号量)

互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据,当之前启动的线程完成后,新的线程才能启动、执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import  threading,time
 
def  run(n):
     semaphore.acquire()  #获得一个信号量
     time.sleep( 1 )
     print ( "run the thread: %sn"  % n)
     semaphore.release()  #释放一个信号量
 
if  __name__  = '__main__' :
     semaphore   threading.BoundedSemaphore( 5 #最多允许5个线程同时运行
     for  in  range ( 20 ):
         threading.Thread(target = run,args = (i,))
         t.start()
 
while  threading.active_count() ! 1 :
     pass  #print threading.active_count()
else :
     print ( '----all threads done---' )

 

6、Events事件

事件是一个简单的同步对象,事件代表一个内置的标志位,并且线程可以等待标志位被设置为真或清除标志位。

调用事件前,需要先实例化一个事件对象。

event = threading.Event()

事件的4个方法:
  1、event.wait()  # 一个客户端线程可以等待事件标志位被设置为真。此时wait()使得线程处于阻塞

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_33695082/article/details/94541396
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-14 16:51
  • 阅读 ( 161 )
  • 分类:Linux

相关文章

文章浏览阅读1.8k次,点赞63次,收藏54次。Linux下的目录权限...
文章浏览阅读1.6k次,点赞44次,收藏38次。关于Qt的安装、Wi...
本文介绍了使用shell脚本编写一个 Hello
文章浏览阅读1.5k次,点赞37次,收藏43次。【Linux】初识Lin...
文章浏览阅读3k次,点赞34次,收藏156次。Linux超详细笔记,...
文章浏览阅读6.8k次,点赞109次,收藏114次。【Linux】 Open...