一、进程和线程的区别
1.线程是CPU的最小执行单位,直接运行在CPU上的是线程而不是进程;
2.进程是线程资源的集合,一个进程至少包含一个线程
3.线程之间可以共享内存资源,进程之间无法直接共享内存空间。
二、线程
1.Create thread:
1 | import threading,time |
Running result:
1 | In the main threading <_MainThread(MainThread, started 9888)> |
可以发现,运行时间并没有计算进子线程运行的时间,主线程本身并没有等待子线程,创建的子线程独立运行,子线程运行结束后,程序再退出。
2.Join
承接上文,主线程不会等待子线程执行,两者并行互不干扰。如有主线程需用到子线程的执行结果的场景,则需要使主进程阻塞等待子进程。
1 | import threading,time |
Running result
1 | In the threading : <Thread(Thread-4, started 9912)> |
3.Daemon 线程
与子线程独立于主线程与之并行运行不同,守护线程在被主线程(或子线程)创建后,若主线程运行结束,不会等待守护进程,且程序直接退出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import threading,time
def main_run():
time.sleep(1)
print('In the main thread',threading.current_thread())
sub_t=threading.Thread(target=sub_run)
sub_t.start()
def sub_run():
time.sleep(2)
print('In the sub thread ',threading.current_thread())
daemon_t=threading.Thread(target=daemon_run)
daemon_t.setDaemon(True)
daemon_t.start()
def daemon_run():
time.sleep(3)
print('In the daemon thread ',threading.current_thread())
main_run()
Running res:
1 | In the main thread <_MainThread(MainThread, started 10064)> |
可以看到主线程率先执行完成,程序再继续等待子线程,子线程执行完后,没有等待守护线程,程序直接退出。
3.互斥锁
为避免多个线程在同时操作同一个数据时造成混乱,在必要的场景下需要给数据加锁
1 | import threading,time |
Running result
1 | Current thread: |
4.信号量
承接上文,互斥锁即同时只能允许有一个线程在操作数据,引入信号量就可以指定允许的线程数的上限。
1 | import threading,time |
Running reslut:
1 | run the thread: 0 |
可以看到每次打印的数量为指定的信号量semaphore值:2
5.死锁:
当使用多个互斥锁(同步锁)同时使用且造成冲突时,会出现进程死锁情况:
1 | import threading |
执行到bar的时候会阻塞住,结果如下,原因见上方代码内注释:
1 | func foo ClockA lock |
6.rlock 递归锁
在python中为了支持在同一线程中多次请求同一资源,避免死锁竞争,python提供了可重入锁RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。例如:
1 | import threading,time |
running result:
1 | Current num in the main: 1 |
可以到看会卡在第二层锁内无法出来,改为内层使用递归锁则正常运行:
1 | import threading,time |
running result:
1 | Current num in the main: 1 |
7.event
事件触发(信号触发),定义一个flag,在多线程之间通过这一个flag标志位互相传递信号,从而触发下一步动作,起到线程间协调沟通的作用。
一个小游戏:
红灯停,黄绿灯行
1 | import threading,time |
running result:
1 | Green light on |
8.Queue
队列的作用:
1.解耦,实现程序松耦合
2.提升效率,非阻塞
生产者、消费者模式:
1 | import threading,time,queue |
running result:
1 | 包子 1 熟了 |
三、进程
C库 python 的GIL决定了python的多线程模型只是一个伪多线程模型,同时只能有一个线程在执行,通过定时高频的CPU上下文切换来实现多线程的假象,因此在CPU计算密集型的场景中使用C python多线程时,频繁的上下文切换甚至会导致性能更低,因此,多线程适用于I/O密集型场景。为了发挥出多Core硬件的性能,既然不能多线程,那就多进程把Core都占掉吧~
mutilprocessing模块语法与threading模块类似:
1.create process:
1 | import multiprocessing,os |
running result:
1 | father pid of main process: 11524 |
2.进程间通信
2.1、queue队列
1 |
|
running result:
1 | get a |
2.2、pipe
管道传输,语法类似socket
1 | #Author :ywq |
running result:
1 | ['hello'] |
2.3、manager
以上的queue、pipe方式都是在两个进程间传输通信,而不是像线程里一样两个进程都可以直接操作内存中的数据,因为进程的memory是彼此独立的。manager模块内部实现了通过中间代理实时克隆数据的方式,使多个进程都可以同时操作同一份数据。
A manager object support data types include:list, dict, value, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Queue…
1 | #Author :ywq |
running result:
1 | [0] |
3.进程池
生成子进程的过程是直接将父进程的资源克隆一份,因此生成进程相对线程而言需要开销较大,为防止多进程导致资源用尽,所以需要线程池来约束同一时间最多可以运行的子进程数量。
1 | #Author :ywq |
running result:
1 | Main process running: 19436 |
四、测试
4.1计算场景,单进程VS多线程
多线程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#Author :ywq
import threading,time
num=0
def calc():
global num
a=num
num+=1
num+=a
start_time=time.time()
for i in range(100000):
t=threading.Thread(target=calc)
t.start()
#calc()
#print(num) #数值太大,不打印了
print(time.time()-start_time,' sencods')
result:
1 | 16.822962284088135 seconds |
单进程:
注释掉clac(),取消注释t
result:1
0.5590319633483887 seconds
多线程计算完成用时16S,单进程用时0.55S,差距很大。。。
4.2、I/O场景,单进程VS多线程
单进程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import threading,time,os
def write():
f=open('test.txt','a')
f.write('just for test \n')
f.close()
start_time=time.time()
for i in range(30000):
#t=threading.Thread(target=write)
#t.start()
write()
print(time.time()-start_time,'seconds')
result:
1 | 12.502715110778809 seconds |
多线程:
1 |
|
result:
1 | 9.011515378952026 seconds |
多进程:
起三个进程,每个进程写1W行,共3W行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#Author :ywq
import threading,time,os
from multiprocessing import Process
def write():
print('Pid:',os.getpid())
for i in range(10000):
#t=threading.Thread(target=write)
#t.start()
f=open('test.txt','a')
f.write('just for test \n')
f.close()
if __name__=='__main__':
start_time=time.time()
p_list=[]
for i in range(3):
p=Process(target=write,)
p.start()
p_list.append(p)
for process in p_list:
process.join()
print(time.time()-start_time,'seconds')
result:
1 | Pid: 20516 |
总结:
python(C库python)因为早期设定的GIL,在CPU的计算指令到达一定的数量以后立即切换运算另一个线程,频繁快速的上下文切换以此来实现了伪多线程,实际上只有一个线程任务在被计算。python的多线程不适合计算密集型场景,但适用于I/O密集型场景,因此引入多进程。但多进程无法像多线程一样便利地互相共享数据,只能借助于队列或管道来通信,或第三方代理来实现数据共享,另外多进程的开销相比多线程要大很多。根据场景选择。