一 并发编程

1 定义: 

通过代码编程让计算机在一定时间内同时跑多个程序所进行的编程操作,实现让CPU执行多任务并发编程的目标是充分地利用CPU,以达到最高的处理性能。

多任务的实现有以下3种方式:

  • 进程:是操作系统资源分配和独立运行的最小单位。

  • 线程:是进程内的一个任务执行独立单元,是任务调度和系统执行的最小单位。

  • 协程:是用户态的轻量级线程,协程的调度完全由用户控制,主要为了单线程下模拟多线程。

二 进程

1 进程的组成:

进程一般由程序段、数据集、程序控制块三部分组成:

程序段:也叫指令集,进程执行过程中需要运行的代码段,是存储在内存中对应进程的程序段中。

数据集:也叫数据段,进程执行过程中向操作系统申请分配的所需要使用的资源。程序运行时,使用与产生的运算数据。如全局变量、局部变量等就存放在对应进程的数据集内。

控制块:也叫程序控制块(Program Control Block,简称PCB)),用于记录进程的外部特征,描述进程的执行变化过程,操作系统可以利用它来控制和管理进程是操作系统感知进程存在的唯一标志。创建进程,实质上是创建进程中的进程控制块,而销毁进程,实质上是回收进程中的进程控制块。

 

2 进程的标记PID:

操作系统里每打开一个程序都会创建一个进程ID,即PID(Process Identification),是进程运行时系统分配的,是操作系统用于区分进程的唯一标识符,在进程运行过程中固定不变的,当进程执行任务结束,操作系统会回收进程相关的一切,也包括了PID。同一个程序在运行起来由操作系统创建进程时,每次得到的PID也是可能不一样的。顺序生成PID号

 

3 进程的三大状态:

在实际开发中,往往任务作业的数量要远高于CPU核数,所以在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入以下几个状态:

    • 就绪状态(Ready),当进程已分配到除CPU以外的所有必要的资源,只要获得CPU资源便可立即执行,这时的进程状态称为就绪状态。

    • 执行/运行状态(Running):当进程已获得CPU资源,其程序正在CPU上执行,此时的进程状态称为执行状态。

    • 阻塞状态(Blocked):正在执行的进程,由于等待某个IO事件(网络请求,文件读写)发生而无法执行时,便放弃对CPU资源的占用而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

 

4 同步异步,阻塞非阻塞

 

 概念描述 
同步和异步是个多个任务处理过程的方式或手段 同步 执行A任务时,当A任务产生结果后,B任务可以执行操作,意思是一个任务接着一个任务的执行下来。

同步与异步和阻塞与非阻塞还可以产生不同的组合:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞。如:

同步阻塞:就是执行多个任务时,一个接着一个地执行,如果A任务处于等待(挂起)状态时,CPU也老老实实等待着,不能进行其他操作。是四种组合里面效率最低的一种

异步非阻塞:就是执行多个任务时,多个任务可以交替执行,当任意一个任务A处于等待(挂起)状态时,CPU会切换到其他任务B操作中,当A任务等待(挂起)状态消失以后,CPU接着交替执行多个任务

异步 执行A任务时,当A任务进入等待(挂起)状态后就可以去执行B任务,然后A任务等待(挂起)状态消失后再回来执行A任务的后续操作,意思是多个任务可以交替执行,如果B任务需要A任务的结果,也无需等待A任务执行结束。
阻塞和非阻塞是多个任务处理过程的某个任务的等待状态(往往是因为IO操作带来的阻塞,如网络IO或文件IO) 阻塞 执行A任务时进入等待(挂起)状态,在这个等待(挂起)状态下CPU不能执行其他的任务操作。也就是CPU不工作了。
非阻塞 执行A任务时进入等待(挂起)状态,在这个等待(挂起)状态下,CPU可以执行其他的B任务操作,也就是CPU工作中。

 

 5 并行,并发,串行

并行 多个任务作业在同一时间内分别在各个CPU下执行。在多核CPU中才会有并行。
并发

资源有限的情况下,系统调度只能在同一时间执行一个任务,CPU的控制权在多个任务作业之间来回快速切换,

因为CPU切换速度非常的快,所以会造成看起来就像是同时执行了多个任务作业的幻觉。并发在单核CPU或多核CPU都可以存在。

并发看起来像是并行,实际是串行。

串行 多个任务作业在同一时间内CPU只能执行一个任务作业,当第一个任务作业完成以后,才轮到第二个任务作业,以此类推。

 

6 进程的创建

 

6.1 使用os.fork创建进程

 

6.1.1 常用方法

方法名描述
os.fork() 创建子进程,相当于复制一份主进程信息,从而创建一个子进程。os.fork是依赖于linux系统的fork系统调用实现的进程创建,在windows下是没有该操作的
os.getpid() 获取当前进程的PID
os.getppid() 获取当前进程的父进程的PID

 

 6.1.2 示例

 1 import os
 2 if __name__ == '__main__':
 3     # 通过fork创建一个子进程
 4     w = 100
 5     pid = os.fork()  # 调用fork() 函数,程序中会拥有 2 个进程。
 6     # pid 作为函数的返回值,主进程和子进程都会执行该语句,但主进程执行 fork() 函数得到的 pid 值为非 0 值(其实是子进程的进程 ID)
 7     # 而子进程执行该语句得到的 pid 值为 0。因此,pid 常常作为区分父进程和子进程的标志。
 8     print(pid)
 9     print(f'当前进程PID: {os.getpid()}')
10     if pid == 0:
11         print(f'w={w}, 子进程PID={os.getpid()},当前子进程的父进程PID={os.getppid()}')
12     else:
13         print(f'当前进程PID:{os.getpid()},创建了一个子进程,PID={pid}')
14 '''
15 7241  
16 当前进程PID: 7240
17 当前进程PID:7240,创建了一个子进程,PID=7241
18 0  
19 当前进程PID: 7241
20 w=100, 子进程PID=7241,当前子进程的父进程PID=7240
21 '''
22 # 由于进程的创建需要耗费一部分资源导致子进程创建存在细微的时间延迟,所以先运行父进程,再运行子进程
os.fork创建进程示例

 

6.2 使用multiprocessing创建进程 [操作进程的最常用模块]

 

6.2.1 常用方法

假设p为multiprocessing.Process(target=任务函数/函数方法)的返回值,子进程操作对象。

方法名描述
p.start() 在主进程中启动子进程p,并调用该子进程p中的run()方法
p.run() 子进程p启动时运行的方法,去调用start方法的参数target指定的函数/方法。如果要自定义进程类时一定要实现或重写run方法。
p.terminate() 主进程中强制终止子进程p,不会进行任何资源回收操作,如果子进程p还创建自己的子进程(孙子进程),则该孙子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果子进程p还保存了一个锁(lock)那么也将不会被释放,进而导致出现死锁现象。
p.is_alive() 检测进程是否还存活,如果进程p仍然运行中,返回True
p.join([timeout]) 主进程交出CPU资源,并阻塞等待子进程结束(强调:是主进程处于等待的状态,而子进程p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join()只能join住start开启的子进程,而不能join住run开启的子进程

 

6.2.2 常用属性

属性名描述
p.daemon 默认值为False,如果设为True,代表子进程p作为守护进程在后台运行的,当子进程p的父进程终止时,子进程p也随之终止,并且设定为True后,子进程p不能创建自己的孙子进程的,daemon属性的值必须在p.start()之前设置
p.name 进程的名称
p.pid 进程的唯一标识符

 

6.2.3 进程的结束

  1. 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)

  2. 出错退出(自愿,python中程序要读取一个a.py的内容,但是a.py不存在,此时python解释器会收集错误进行退出)

  3. 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)

  4. 被其他进程杀死(非自愿,如终端下根据进程DI杀死一个进程: kill -9 pid)

多进程的使用过程中, 如果没有正常的结束进程,则会产生僵尸进程或孤儿进程的。

 

6.2.4 僵尸进程

任何一个进程(init除外) 在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie process,也叫僵死进程)的数据结构,等待父进程通过系统调用进行资源回收处理。这是每个进程在结束时都要经过的阶段,只是正常的进程结束,父进程回收的速度非常快,所以我们无法感知而已。

如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。这就是僵尸进程,是一种有害的进程,会浪费一定的系统资源,所以在开发中我们一定要避免出现僵尸进程

子进程先退出,父进程没有进行回收操作就会产生僵尸进程。

 

6.2.5 孤儿进程

父进程先退出,而它的子进程还在运行,那么这种情况下,还在运行的进程就会变成没有爸爸的孤儿进程。

孤儿进程会被pid=0的init进程(系统守护进程)所收养,init进程会对所有的孤儿进程进行资源回收,所以孤儿进程不会对系统造成危害。

附:

Linux中的所有进程都是由init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成后,init将变为守护进程监视系统其他进程。

 

 1 import multiprocessing
 2 import os
 3 import time
 4 
 5 def watch(name):
 6     for i in range(2):
 7         print(f"{name}在看电视....", os.getpid())
 8         time.sleep(1)
 9 
10 def drink(name, food):
11     for i in range(2):
12         print(f"{name}喝{food}....",os.getpid())
13         time.sleep(1)
14 
15 def eat(name, food):
16     for i in range(2):
17         print(f"{name}吃{food}....",os.getpid())
18         time.sleep(1)
19 
20 # windows中python创建子进程是通过Import导入父进程代码到子进程中实现的子进程创建方式
21 # 所以import在导入以后会自动执行被导入模块的代码,因此报错。
22 # 把创建进程的代码写到__main__下可避免报错
23 if __name__ == '__main__':
24     print("主进程", os.getpid())
25     # 创建三个子进程
26     watch_process = multiprocessing.Process(target=watch,args=("小花",))
27     # 如果希望在父进程创建子进程时,传递数据给子进程的话,可以选择kwargs,args中的任意一种来传递数据,kwargs字典与args元组,可以传递1个到多个数据
28     drink_process = multiprocessing.Process(target=drink, kwargs={"name":"小明","food": "羊汤"}) # 命名实参
29     eat_process = multiprocessing.Process(target=eat, args=("小明", "米饭", ))  # 位置实参
30     # 注意,start表示调用开启进程操作,但是并不会阻塞等待进程真的开启,是一个异步操作。
31     watch_process.start()
32     drink_process.start()
33     eat_process.start()
34     print("主程序代码运行结束!")
35 '''
36 主进程 7534
37 主程序代码运行结束!
38 小明喝羊汤.... 7537
39 小花在看电视.... 7536
40 小明吃米饭.... 7538
41 小明喝羊汤.... 7537
42 小花在看电视.... 7536
43 小明吃米饭.... 7538
44 '''
多进程创建示例

 

 1 import multiprocessing
 2 import os
 3 import time
 4 
 5 class Humen(object):
 6 
 7     def drink(self, food):
 8         for i in range(2):
 9             print(f"喝{food}....",os.getpid())
10             time.sleep(1)
11 
12     def eat(self, food):
13         for i in range(2):
14             print(f"吃{food}....", os.getpid())
15             time.sleep(1)
16 
17 if __name__ == '__main__':
18     xiaoming = Humen()
19     print("主进程", os.getpid())
20     drink_process = multiprocessing.Process(target=xiaoming.drink, kwargs={"food": "羊汤"})
21     eat_process = multiprocessing.Process(target=xiaoming.eat, args=("米饭", ))
22 
23     drink_process.start()
24     eat_process.start()
25 
26 '''
27 主进程 7829
28 喝羊汤.... 7831
29 吃米饭.... 7832
30 喝羊汤.... 7831
31 吃米饭.... 7832
32 '''
基于对象创建进程代码示例

 

继承Process创建进程代码示例[少用]

 

join方法的作用就是为了监督所有的子进程全部执行结束

 1 import time, random
 2 from multiprocessing import Process
 3 
 4 
 5 def send_sms(i):
 6     """模拟发送邮件的方法"""
 7     # 使用随机数进行程序睡眠,random.random()随机生成0-1之间的小数
 8     time.sleep(random.random())
 9     print(f"成功发送第{i}份邮件!")
10 
11 if __name__ == '__main__':
12     t1 = time.time()
13     process_list = []
14     for i in range(3):
15         p = Process(target=send_sms, args=(i,))
16         p.start()
17         # p.join()  # 同步阻塞,所以会导致整主进程的所有代码,都处于同步阻塞了,使用多进程就变得没有意义了
18         process_list.append(p)
19     t2 = time.time()
20     print(f"整个发送邮件的过程到确认所有邮件都发送成功的时间:{t2-t1}")
21     # 先让主进程把所有子进程创建并启动起来,后续才进行阻塞等待所有子进程执行结束
22     for p in process_list:
23         p.join()
24     t3 = time.time()
25     print(f"主进程从启动子进程以后到所有子进程发送成功以后的事件:{t3-t2}")
26     print(f"整个发送邮件的过程到确认所有邮件都发送成功的时间:{t3-t1}")
27     print("所有邮件已经发送成功了!")
28 
29 '''
30 整个发送邮件的过程到确认所有邮件都发送成功的时间:0.016173124313354492
31 成功发送第2份邮件!
32 成功发送第0份邮件!
33 成功发送第1份邮件!
34 主进程从启动子进程以后到所有子进程发送成功以后的事件:0.905789852142334
35 整个发送邮件的过程到确认所有邮件都发送成功的时间:0.9219629764556885
36 所有邮件已经发送成功了!
37 '''
join方法代码示例

 

 1 import time
 2 from multiprocessing import Process
 3 
 4 
 5 def mydaemon():
 6     while True:
 7         print("daemon is alive!")
 8         time.sleep(1)
 9 
10 if __name__ == '__main__':
11     p = Process(target=mydaemon)
12     p.daemon = True  # 设置当前子进程为守护进程,必须写在start()方法之前
13     p.start()
14     time.sleep(5)
守护进程

 

 1 """
 2 默认主进程等待所有非守护进程,也就是子进程执行结束之后,在关闭程序,释放资源
 3 守护进程在主进程的代码执行结束时, 就会自动关闭了,并非主进程的真正结束;
 4 """
 5 import time
 6 from multiprocessing import Process
 7 
 8 def daemon(i):
 9     print(f"守护进程{i}启动了!!!")
10     while True:
11         time.sleep(1)
12         print(f"{i}:当前程序正常运行...")
13 
14 
15 def func(n):
16     print(f"子进程{n}启动了!")
17     time.sleep(3)
18     print(f"子进程{n}执行结束了!")
19 
20 
21 if __name__ == '__main__':
22     for i in range(2):
23         daemon_process = Process(target=daemon, args=(i,))
24         daemon_process.daemon = True
25         daemon_process.start()
26 
27     p_list = []
28     for i in range(4):
29         process = Process(target=func, args=(i,))
30         process.start()
31         p_list.append(process)
32 
33     for p in p_list: p.join()
34     print("主进程代码执行结束!")
多个守护进程的使用

 

 1 import os
 2 import time
 3 from multiprocessing import Process, current_process
 4 
 5 def watch():
 6     p = current_process()
 7     print(f"当前进程名:{p.name}")
 8     print(f"当前进程ID:{p.pid}")
 9     # p.kill()
10     # 不要直接在子进程中,使用exit()关闭当前进程,容易导致出现僵尸进程
11     # 所以,windows下可以使用当前进程对象的kill方法进行关闭。
12     for i in range(2):
13         print(f"进程{p.name}在看电视....", os.getpid())
14 
15 if __name__ == '__main__':
16     print("主进程", os.getpid())
17     # 创建子进程
18     # name 声明进程名
19     p = Process(target=watch, name="watch")
20     p.start()
21 
22 '''
23 主进程 11062
24 当前进程名:watch
25 当前进程ID:11064
26 进程watch在看电视.... 11064
27 进程watch在看电视.... 11064
28 '''
当前进程current_process代码示例

 

7 进程间的通信

7.1 进程间的数据隔离

 

 1 from multiprocessing import Process
 2 # 全局变量
 3 num = 100
 4 
 5 def func():
 6     # 引入全局变量
 7     global num
 8     num -= 1  # num = num-1
 9 
10 if __name__ == '__main__':
11 
12     process_list = []
13     for i in range(10):
14         p = Process(target=func)
15         p.start()
16         process_list.append(p)
17 
18     for p in process_list:
19         p.join()
20 
21     print(num)  # num= 100
进程间的数据隔离

 

7.2 进程间的通信IPC

 

通信,是基于某种介质实现两者传递数据的技术。实现通信,一般有2种方式:

  1. 基于网络(AF_INET)

  2. 基于文件(AF_UNIX)

在进程间的通信,我们没有必要基于网络,因为需要网络资源而且调用网络IO也会产生延时。所以开发中针对于进程间的通信我们般采用基于文件进行通信的。

IPC(Inter-Process Communication,进程间的通信),multiprocessing模块支持进程间通信的两种主要形式:队列(Queue)和管道(Pipe)。其中管道基本不使用,最常用的是队列,但是队列的实现是基于管道的。

 

7.2.1 常用方法

方法名描述
q.get( [ block [ ,timeout ] ] ) 读取队列q中的一个数据项。 如果队列q为空,此方法将阻塞程序,直到队列中有数据项为止。 block用于控制阻塞行为,默认为True。 如果设置为False,将引发Queue.Empty异常。 timeout是可选超时时间,用在阻塞模式中。 如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。
q.put(item [, block [,timeout ] ] ) 将数据项item放入队列q中。 如果队列已满,此方法将阻塞至有空间可用为止。 block控制阻塞行为,默认为True。 如果设置为False,将引发Queue.Empty异常)。 timeout指定在阻塞模式中等待可用空间的时间长短。 超时后将引发Queue.Full异常。

 

7.2.2 Queue队列的基本使用

Queue是一个基于文件(AF_UNIX)类型实现的socket通信队列对象,通信过程中的数据采用pickle压缩传递,可以让我们很方便地在多进程间实现IPC通信,它不仅具有队列的先进先出,后进后出的特点,而且还内置实现了Lock机制,保证在IPC通信过程中的数据的一致性问题。

 1 from multiprocessing import Process, Queue
 2 
 3 def func(exp, queue):
 4     # 把传递进来的exp字符串当成python代码来运行,并把结果返回给主进程
 5     ret = eval(exp)
 6     print("eval的计算结果:", ret)
 7     # 对于定长队列,如果队列满了,再次使用put会进入阻塞状态,直到另一个进程从队列中提取数据项,让队列腾出空间
 8     """添加数据到队列中"""
 9     queue.put(ret)
10 
11 if __name__ == '__main__':
12     # 创建一个队列
13     q = Queue() # 不定长的队列
14     # q = Queue(3)   # 定长的队列
15     # print(q.qsize())  # 查看队列中的数据项数量
16     # 把队列对象作为参数传递给需要通信的子进程中
17     p = Process(target=func, args=("10+20+30", q)).start()
18     # 从队列中提取数据
19     print("队列中的结果:", q.get()) #put和get是一一对应
20     # print(q.qsize())   # 队列中的数据项全部提取以后,qsize就为0。
21     # 对于空队列,如果使用get提取数据项,因为没有数据,所以当前进程会进入阻塞状态,直到另一个进程添加数据项到队列中。
queue队列基本使用

 

7.2.3 pipe管道的基本使用 [少用]

Pipe是一个基于文件(AF_UNIX)类型实现的socket通信管道对象,通信过程中的数据采用pickle压缩传递,可以让我们轻松地使用IPC通信,但是并没有Queue通信队列的先进先出和Lock的特点,所以是不安全的,因此少用。

 1 from multiprocessing import Process, Queue, Pipe
 2 
 3 def func(exp, con1):
 4     ret = eval(exp)
 5     print("eval的计算结果:", ret)
 6     # 往管道的另一端发送数据
 7     con1.send(ret)
 8 
 9 if __name__ == '__main__':
10     # 创建一个管道,返回值是一个元组,对应的就是管道的两端(可以用于进行输入输出)
11     con1, con2 = Pipe()
12     p = Process(target=func, args=("10+20+30", con1)).start()
13     # 接受来自管道的另一端发送过来的数据
14     print("队列中的结果:", con2.recv())
15 
16 '''
17 eval的计算结果: 60
18 队列中的结果: 60
19 '''
pypi.py

 

三 线程

进程是负责分配和隔离资源(内存,CPU)的,而线程则是负责执行具体任务(代码)的。线程没有自己的系统资源的,同一个进程下的多个线程之间共享进程的系统资源。同时线程也需要操作系统创建、管理、切换、回收,但是线程所需要的资源开销远远要比进程要小。

线程和进程的区别:

  1. 地址空间和其它资源(如打开文件):进程间相互独立同一进程的各线程间共享资源。某进程内的线程在其它进程不可见。

  2. 通信:进程间通信采用IPC机制,而线程间可以直接读写进程进程的数据集(如全局变量)来进行通信——线程也存在并发问题,线程可以使用进程同步(Lock)和互斥手段的辅助,以保证数据的一致性。

  3. 调度和切换:线程切换比进程的切换要快得多,资源开销要少。

  4. 在多线程操作系统中,进程不是一个可执行的实体,进程的执行需要依赖于线程的。

 1 线程的组成

一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成,同时因为线程属于进程的的实体,所以线程也拥有包括进程的程序段、数据集和独立的线程控制块(叫TCB,注意这不是进程控制块PCB)。 

2 线程的创建

 1 import time,os
 2 
 3 from threading import Thread,current_thread
 4 
 5 def func(name):
 6     time.sleep(2)
 7     print(f'{name}子线程运行了!')
 8 
 9 
10 if __name__ == '__main__':
11     print("主进程的PID=", os.getpid())  # 主进程ID
12     # 单进程下的多线程
13     for i in range(10):
14         t = Thread(target=func, args=(f'{i}号',))
15         t.start()
16         # python的运行时,就会创建一个主进程,所以我们不需要手动创建主进程
17         # 所以,我们上面创建的Thread线程是属于给主进程创建了一个子线程(child thread)
18         # 注意:是子线程!!!因为主进程是在创建进程时,默认会顺道创建一个主线程的
19         # 所以,当前我们运行了这个python程序,实际上是在系统中启动了1个主进程,1个主线程和1个子线程
20     thread = current_thread()
21     print('主线程PID', thread.ident)  # 获取当前线程的线程号(TID)
开启多个子线程

打印的先后顺序是并发不可控的,因为不管是线程还是进程都是CPU随机调度的

 1 import time
 2 from multiprocessing import Process
 3 from threading import Thread
 4 
 5 
 6 def thread_func(name, i):
 7     time.sleep(2)
 8     print(f'{name}.{i}号子线程运行了!')
 9 
10 
11 def process_func(name):
12     time.sleep(2)
13     print(f'{name}号子进程运行了!')
14     # 在子进程中,开启多线程
15     for i in range(3):
16         t = Thread(target=thread_func, args=(name, f'{i}',))
17         t.start()
18 
19 
20 if __name__ == '__main__':
21     # 先开启多进程下
22     for i in range(3):
23         t = Process(target=process_func, args=(f'{i}',))
24         t.start()
多个进程下开启多线程

 

 1 import os
 2 import time
 3 import threading
 4 
 5 class Humen(object):
 6     def watch(self):
 7         for i in range(3):
 8             print("看电视....", os.getpid())
 9             time.sleep(1)
10 
11     def drink(self, food):
12         for i in range(3):
13             print(f"喝{food}....",os.getpid())
14             time.sleep(1)
15 
16     def eat(self, food):
17         for i in range(3):
18             print(f"吃{food}....", os.getpid())
19             time.sleep(1)
20 
21 if __name__ == '__main__':
22     xiaoming = Humen()
23     watch_thread = threading.Thread(target=xiaoming.watch)
24     drink_thread = threading.Thread(target=xiaoming.drink, kwargs={"food": "羊汤"})
25     eat_thread = threading.Thread(target=xiaoming.eat, args=("米饭", ))
26 
27     watch_thread.start()
28     drink_thread.start()
29     eat_thread.start()
面向对象的方式创建线程

 

 1 from threading import Thread
 2 import time
 3 
 4 class MyThread(Thread):
 5     def run(self):
 6         """run里面编写线程运行时要执行的任务代码"""
 7         time.sleep(2)
 8         print(f'{self.name}子线程运行了!')
 9 
10 if __name__ == '__main__':
11     t = MyThread()
12     t.start()
13 
14 #另一种写法
15 from threading import Thread
16 import time
17 
18 def func():
19     print("子线程要执行的任务代码")
20 
21 class MyThread(Thread):
22     # def __init__(self, name, *args, **kwargs):
23     #     super().__init__(*args, **kwargs)
24     #     self.name = name
25 
26     def run(self):
27         """run里面编写线程运行时要执行的任务代码"""
28         print(f"{self.name}线程运行前!")
29         super().run()
30         print(f"{self.name}线程运行后!")
31 
32 if __name__ == '__main__':
33     t = MyThread(name="1号", target=func)
34     t.start()
继承线程类开启子线程

 

 1 import time
 2 
 3 from multiprocessing import Process
 4 from threading import Thread
 5 
 6 
 7 def func(a,b):
 8     return a,b
 9 
10 if __name__ == '__main__':
11     t1 = time.time()
12     for i in range(500):
13         Thread(target=func,args=(10,20,)).start()
14     t2 = time.time()
15 
16     t3 = time.time()
17     for i in range(500):
18         Process(target=func, args=(10,20)).start()
19     t4 = time.time()
20     print(t2-t1)  # 0.03694486618041992 执行500个线程
21     print(t4-t3)  # 7.676598072052002  执行500个进程
多进程与多线程的效率比较

 

 1 import time, random
 2 from multiprocessing import Process
 3 from threading import Thread
 4 
 5 # 全局变量
 6 num = 100
 7 
 8 def func():
 9     global num
10     num -= 1
11 
12 if __name__ == '__main__':
13     obj_list = []
14     for i in range(10):
15         t = Thread(target=func)
16         t.start()
17         obj_list.append(t)
18 
19     for t in obj_list:
20         t.join()
21 
22     print(num)  # num = 90
多线程共享进程资源

 

 1 import time, random
 2 from threading import Thread
 3 
 4 
 5 def send_sms(i):
 6     """模拟发送邮件的方法"""
 7     # 使用随机数进行程序睡眠,random.random()随机生成0-1之间的小数
 8     time.sleep(random.random())
 9     print(f"成功发送第{i}份邮件!")
10 
11 if __name__ == '__main__':
12     t1 = time.time()
13     # 与多进程操作一样,join阻塞监控所有的子线程全部执行结束也是类似的操作
14     thread_list = []
15     for i in range(100):
16         t = Thread(target=send_sms, args=(i,))
17         t.start()
18         thread_list.append(t)
19     t2 = time.time()
20     for t in thread_list: t.join()
21     t3 = time.time()
22     print("所有邮件已经发送成功了!")
23 
24     print(f"主进程从启动子进程以后到所有子进程发送成功以后的时间:{t3-t2}")
25     print(f"整个发送邮件的过程到确认所有邮件都发送成功的时间:{t3-t1}")
多个子线程join阻塞等待

 

 1 from threading import Thread, currentThread, activeCount, enumerate
 2 import time
 3 
 4 
 5 def func():
 6     # 当前线程对象
 7     time.sleep(2)
 8     t = currentThread()
 9     print(f'线程[{t.name}]运行了')
10 
11 
12 if __name__ == '__main__':
13     for i in range(10):
14         t = Thread(target=func, name=f"{i}号")
15         t.start()
16     print(activeCount(), enumerate()) #11 [<_Main..
17     # enumerate() 列表,等于 [所有子线程对象,主线程对象]
18     # activeCount() = len(enumerate())
activeCount和enumerate

 

 1 import random
 2 import time
 3 from threading import Thread
 4 # 守护进程/守护线程,都是用于实现报活的,也就是心跳包发送
 5 
 6 def funcDeamon():
 7     """守护线程:报活"""
 8     while True:
 9         time.sleep(1)
10         print("心跳。。。。。")
11 
12 def func():
13     """工作线程:执行任务"""
14     for i in range(5):
15         time.sleep(random.random()*3)
16         print("任务线程在工作....")
17 
18 if __name__ == '__main__':
19     # 先启动守护线程
20     d = Thread(target=funcDeamon)
21     d.setDaemon(True)
22     d.start()
23 
24     # 开启任务程序
25     t = Thread(target=func)
26     t.start()
27 '''
28 任务线程在工作....
29 心跳。。。。。
30 任务线程在工作....
31 心跳。。。。。
32 '''
守护进程基本用途:报活

3 锁

3.1 全局解释器锁GIL

3.1.1 定义:是Cpython解释器中提供的一个全局锁(注意:不是python语法)。

GIL会限制每个线程在执行的时候都需要先获取GIL锁,它保证了CPython解释器在运行多个线程时,同一时间内只有一个线程被CPU执行。

GIL只在CPython与Pypy解释器中存在

扩展:

python解释器:解析和运行python代码的翻译程序。
常见的python解释器:
Cpython (官方) C语言 底层就是C语言代码,可以把python代码集成到C语言项目中,也可以基于C编写python底层扩展与模块
pypy Python语言 底层就是python代码
Jpython Java语言 底层就是Java代码,可以把python代码集成到java项目中,也可以基于java编写python底层扩展与模块
IronPython .NET语言 底层就是.net代码,可以把python代码集成到.net项目中的,也可以通过.net语言编写python底层扩展与模块

 

在具有多核CPU多线程操作系统中,GIL也会限制CPU在同一时间内只执行一个线程,所以造成了Python的多线程虽然可以实现并发效果,但是并不能发挥多核CPU的性能

 

3.1.2 多线程是否会导致数据并发性的问题:

因为python解释器给执行的所有的线程加了一把全局共享的GIL锁。所以以下的代码中,没有数据不一致的这个问题。

数据量少时,不受影响

 

 1 from threading import Thread
 2 
 3 num = 0
 4 
 5 def func():
 6     global num
 7     # 大批量执行计算操作,一般叫计算密集型的任务
 8     for i in range(200000):
 9         num += 1
10 
11 if __name__ == '__main__':
12     thread_list = []
13     for i in range(10):
14         t = Thread(target=func)
15         t.start()
16         thread_list.append(t)
17 
18     for t in thread_list:
19         t.join()
20 
21     print(num) # num < 2000000
计算密集型/数据量大出现少值现象

 

少值原因:

由于操作系统时间片轮调度算法与GIL全局解释器之间的冲突导致的问题。在python程序中的赋值操作中(+=,-=,*=,/=,%=等)容易导致数据不一致的情况。因为在密集型计算任务的计算过程中有可能出现运算结果产生,但是因为时间片到时了,CPU切换了进程/线程,导致计算结果没有保存到数据集中,所以下次切换回来的时候,就会丢失上次计算的结果。例: num+=1操作,num +1 和num的赋值  这两步会出现只执行了一步,即num +1 执行结束但没有赋值,所以会导致num少值

 

3.1.3 GIL问题的解决方案

程序一般可以分2种:计算密集型和IO密集型。

 

计算密集型

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。针对这种任务,我们就应该减少GIL对程序的影响,所以可以采用多进程或者改用其他的解释器来完成任务操作。

 

IO密集型

大部分的程序在运行时,都需要大量IO操作,比如网络数据的收发,大文件的读写,这样的程序称为IO密集型程序。IO密集型程序在运行时,需要大量的时间进行等待,如果IO操作不完成,程序无法执行后面的操作,一直处于等待状态,导致CPU空闲。 由于GIL的存在,同一时刻只能有一个线程执行,在程序进行IO操作时,CPU实际并没有做任何工作,程序执行效率非常低。 为了提高CPU的使用率,Python解释在程序执行IO等待时,会释放GIL锁,让其它线程执行,提高Python程序的执行效率。 所以GIL对于IO密集型的影响很小,多线程适合用来做IO密集型的程序,如网络爬虫,web服务器/框架。

 

3.1.4 Lock与GIL的区别:

  1. Lock和GIL,Lock是我们程序中针对单个进程进行添加的。GIL锁是Cpython解释器中默认添加的。

    Lock针对的进程本身,GIL是针对所有进程下的所有线程。

  2. Lock与GIL都是一种互斥锁。GIL是属于CPython解释器管理的,所以不会出现死锁现象。但是Lock是开发人员自己添加的,所以Lock的加锁与解锁都是开发人员自己处理的,因此使用不当则会出现死锁现象。

3.2 互斥锁

全局解释器锁并没有帮我们保证线程在并发编程中真正的保证了数据安全,而是因为不同线程同时抢占资源导致出现了代码运行结果存在无错偏差。

非线程安全:线程之间存在资源竞争的情况,一个全局变量经过多个线程的并发操作,最终的结果可能会出现异常情况

线程安全:使用互斥锁来保证这个全局变量在多个线程的并发操作下,代码执行结果不会出现错误偏差

资源抢占问题

并发编程中不管是进程还是线程并发,都会导致出现资源抢占现象,使用锁可以解决资源抢占问题,但是同时也会带来阻塞问题,我们需要合理地使用锁来降低阻塞的时间。

 1 import time
 2 from threading import Thread, Lock
 3 
 4 num = 0
 5 
 6 def func(lock):
 7     global num
 8     # # 不合理使用:被锁代码的粒度太小,导致程序运行中,因为频繁的加锁与解锁浪费了大量CPU资源
 9     for i in range(500000):
10         with lock:
11             num += 1
12 
13     # 合理使用:
14     # with lock:
15     #     for i in range(500000):
16     #         num += 1
17 
18 if __name__ == '__main__':
19     t1 = time.time()
20     thread_list = []
21     lock = Lock()
22     for i in range(10):
23         t = Thread(target=func, args=(lock,))
24         t.start()
25         thread_list.append(t)
26 
27     for t in thread_list:
28         t.join()
29     t2 = time.time()
30     print(f"num={num}, {t2-t1}") # num=2000000
解决了多线程计算密集型倒是数据并发性问题

 

并发条件下的单例模式

因为有GIL锁并且线程切换的开销非常小,所以通常情况下单例模式在多线程下不会轻易暴露问题,但是如果资源有限或创建对象遇到阻塞时,会出现GIL锁问题。

 1 import time
 2 from threading import Thread
 3 
 4 class Singleton(object):
 5     __instance = None
 6 
 7     def __new__(cls, *args, **kwargs):
 8         if cls.__instance is None:  # 阻塞1
 9             time.sleep(0.01) # 阻塞2
10             cls.__instance = super().__new__(cls)
11         return cls.__instance
12 
13 
14 class Humen(Singleton):
15     def __init__(self, name, age):
16         self.name = name
17         self.age = age
18 
19 
20 def create_perple():
21     xm = Humen("xiaoming", 20)
22     print(xm)
23 
24 
25 if __name__ == '__main__':
26     for i in range(2):
27         thread = Thread(target=create_perple)
28         thread.start()
并发条件下的单例模式
 1 import time
 2 from threading import Thread, Lock
 3 
 4 class Singleton(object):
 5     __instance = None
 6     __lock = Lock()
 7     def __new__(cls, *args, **kwargs):
 8         with cls.__lock:
 9             if cls.__instance is None:
10                 time.sleep(0.01)
11                 cls.__instance = super().__new__(cls)
12             return cls.__instance
13 
14 
15 class Humen(Singleton):
16     def __init__(self, name, age):
17         self.name = name
18         self.age = age
19 
20 
21 def create_perple():
22     xm = Humen("xiaoming", 20)
23     print(xm)
24 
25 
26 if __name__ == '__main__':
27     for i in range(2):
28         thread = Thread(target=create_perple)
29         thread.start()
线程安全单例模式

 

3.3 递归锁

 

 

递归锁(RLock):一般用来解决多把锁产生的死锁问题

 

可以针对同一个进程或同一个线程内,同一把锁可以添加多次的锁。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

递归锁也属于互斥锁,所以如果同时出现多把递归锁,还是会导致死锁情况出现的。

死锁

互斥锁的使用过程中, 如果使用不当,不管是进程或线程的Lock都会出现死锁。

死锁,是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称程序处于死锁状态或产生了死锁,这些永远在互相等待的进程或线程称为死锁进程或死锁线程。

除一把锁被添加多次会导致死锁以外,多把锁有时候在编程中也会出现死锁现象

import time
from threading import Thread, RLock

fork_lock = noodle_lock = RLock()


def eat1(name):
    noodle_lock.acquire()
    print(f"{name}抢到了面条")
    fork_lock.acquire()
    print(f"{name}抢到了叉子")
    print(f"{name}吃面")
    time.sleep(0.1)
    fork_lock.release()
    noodle_lock.release()


def eat2(name):
    fork_lock.acquire()
    print(f"{name}抢到了叉子")
    noodle_lock.acquire()
    print(f"{name}抢到了面条")
    print(f"{name}吃面")
    time.sleep(0.1)
    noodle_lock.release()
    fork_lock.release()


if __name__ == '__main__':
    for name in ["小白", "小明", "小红", "小胡"]:
        Thread(target=eat1, args=(name,)).start()
        Thread(target=eat2, args=(name,)).start()
'''
小白抢到了面条
小白抢到了叉子
小白吃面
小白抢到了叉子
小白抢到了面条
小白吃面
小明抢到了面条
小明抢到了叉子
小明吃面
小明抢到了叉子
小明抢到了面条
小明吃面
'''
递归锁的应用

 

3.4 队列

先进先出队列:可以使用到排队购物付款,排队进入地铁站等场景

 1 import queue
 2 
 3 q=queue.Queue(3)
 4 q.put('1')
 5 q.put('2')
 6 q.put('3')
 7 # q.put('4')  # 阻塞,超过队列长度
 8 
 9 print(q.get())
10 print(q.get())
11 print(q.get())
12 # print(q.get()) # 阻塞,空队列
先进先出队列

 

后进后出队列中:可以使用到历史记录、电梯、字符翻转等场景。

 1 import queue
 2 
 3 q = queue.LifoQueue(3)
 4 q.put('1')
 5 q.put('2')
 6 q.put('3')
 7 # q.put('4')  # 阻塞,超过队列长度
 8 
 9 print(q.get())
10 print(q.get())
11 print(q.get())
12 print(q.get())  # 阻塞,空队列
后进先出队列

 

优先级队列,会给每一个成员设置一个优先级,优先级数字最低的,先出队,当优先级一样时,先进先出。

优先级队列,一般常用于任务调度,银行VIP业务等场景。

 1 import queue
 2 
 3 q = queue.PriorityQueue(3)
 4 # put进入一个元组, 元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较), 数字越小优先级越高
 5 q.put((20,'a'))
 6 q.put((10,'b'))
 7 q.put((30,'c'))
 8 
 9 print(q.get())
10 print(q.get())
11 print(q.get())
12 
13 '''
14 (10, 'b')
15 (20, 'a')
16 (30, 'c')
17 '''
优先级队列

 

posted on 2022-05-22 22:42  大明花花  阅读(37)  评论(0编辑  收藏  举报