1 如何通俗理解线程和进程?
进程:进程就是正在执⾏的程序。
线程:是程序执⾏的⼀条路径, ⼀个进程中可以包含多条线程。
通俗理解:例如你打开抖⾳,就是打开⼀个进程,在抖⾳⾥⾯和朋友聊天就是开启了⼀条线程。
再举⼀个例⼦:
在某⻝堂打饭的时候,此⻝堂安排三个打饭⼤妈打饭,所有同学依次排成三个队伍,每个打饭⼤
妈相当于⼀个线程。
这个⻝堂相当于⼀个进程,他⼀共有三个打饭⼤妈,相当于进程⾥有三个线程。
两者之间的关系:
⼀个进程⾥⾯可以有多条线程,⾄少有⼀条线程。
⼀条线程⼀定会在⼀个进程⾥⾯。
关于协程,我会放在后⾯讲完线程和进程时再讲解。
2 .Python如何启动⼀个线程?
⼀般的,程序默认执⾏只在⼀个线程,这个线程称为主线程,例⼦演示如下:
导⼊线程相关的模块 threading:
import threading
threading的类⽅法 current_thread()返回当前线程:
t = threading.current_thread()
print(t)
看到 MainThread,验证了程序默认是在MainThead中执⾏。
t.getName()获得这个线程的名字
其他常⽤⽅法,t.ident获得线程id
is_alive() 判断线程是否存活
那么,如何创建⾃⼰的线程呢?
3 .Python如何创建⼀个新线程?
创建⼀个线程:
my_thread = threading.Thread()
创建线程的⽬的是告诉它帮助我们做些什么,做些什么通过参数target传⼊,参数类型为
callable,函数就是可调⽤的:
def print_i(end):
for i in range(end):
print(f'打印i={i}')
my_thread = threading.Thread(target=print_i, args=(10,))
my_thread线程已经全副武装,但是我们得按下发射按钮,启动start(),它才开始真正起⻜。
my_thread.start()
打印结果如下,其中args指定函数print_i需要的参数i,类型为元祖。
打印i=0 打印i=1 打印i=2 打印i=3 打印i=4 打印i=5 打印i=6 打印i=7 打印i=8 打印i=9
⾄此,多线程相关的核⼼知识点,已经总结完毕。但是,仅仅知道这些,还不够!光纸上谈兵,
当然远远不够
4 【案例】如何理解多线程的⼯作(交替获得时间
⽚)?
为了更好解释多线程之间的⼯作,开辟3个线程,装到threads中:
import time
from datetime import datetime
import threading
def print_time():
for _ in range(5): # 在每个线程中打印5次
time.sleep(0.1) # 模拟打印前的相关处理逻辑
print('当前线程%s,打印结束时间为:%s'%(threading.current_thread().getName(
threads = [threading.Thread(name='t%d'%(i,),target=print_time) for i in range
启动3个线程:
[t.start() for t in threads]
打印结果如下,t0,t1,t2三个线程,根据操作系统的调度算法,轮询获得CPU时间⽚,注意观察:
当前线程t1,打印结束时间为:2023-12-22 17:37:57.996914
当前线程t2,打印结束时间为:2023-12-22 17:37:57.997869
当前线程t3,打印结束时间为:2023-12-22 17:37:57.998647
当前线程t1,打印结束时间为:2023-12-22 17:37:58.098255
当前线程t2,打印结束时间为:2023-12-22 17:37:58.102946
当前线程t3,打印结束时间为:2023-12-22 17:37:58.102986
当前线程t1,打印结束时间为:2023-12-22 17:37:58.202542
当前线程t3,打印结束时间为:2023-12-22 17:37:58.205183
当前线程t2,打印结束时间为:2023-12-22 17:37:58.205239
当前线程t1,打印结束时间为:2023-12-22 17:37:58.302782
当前线程t2,打印结束时间为:2023-12-22 17:37:58.307849
当前线程t3,打印结束时间为:2023-12-22 17:37:58.310220
当前线程t1,打印结束时间为:2023-12-22 17:37:58.407185
当前线程t2,打印结束时间为:2023-12-22 17:37:58.412851
当前线程t3,打印结束时间为:2023-12-22 17:37:58.415361
当前线程t1,打印结束时间为:2023-12-22 17:38:53.747102
当前线程t2,打印结束时间为:2023-12-22 17:38:53.748492
当前线程t3,打印结束时间为:2023-12-22 17:38:53.748573
当前线程t1,打印结束时间为:2023-12-22 17:38:53.848614
当前线程t3,打印结束时间为:2023-12-22 17:38:53.850400
当前线程t2,打印结束时间为:2023-12-22 17:38:53.850453
当前线程t1,打印结束时间为:2023-12-22 17:38:53.949232
当前线程t3,打印结束时间为:2023-12-22 17:38:53.951598
当前线程t2,打印结束时间为:2023-12-22 17:38:53.951879
当前线程t1,打印结束时间为:2023-12-22 17:38:54.051355
当前线程t3,打印结束时间为:2023-12-22 17:38:54.056651
当前线程t2,打印结束时间为:2023-12-22 17:38:54.056700
当前线程t1,打印结束时间为:2023-12-22 17:38:54.155642
当前线程t3,打印结束时间为:2023-12-22 17:38:54.157077
当前线程t2,打印结束时间为:2023-12-22 17:38:54.157187
5 【案例】如何理解多线程抢夺同⼀个变量?
多线程编程,存在抢夺同⼀个变量的问题。
⽐如下⾯例⼦,创建的10个线程同时竞争全局变量 a :
import threading
a = 0
def add1():
global a
a += 1
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
#执⾏结果:
t0 adds a to 1: 1
t1 adds a to 1: 2
t2 adds a to 1: 3
t3 adds a to 1: 4
t4 adds a to 1: 5
t5 adds a to 1: 6
t6 adds a to 1: 7
t7 adds a to 1: 8
t8 adds a to 1: 9
t9 adds a to 1: 10
结果⼀切正常,每个线程执⾏⼀次,把 a 的值加1,最后 a 变为10,⼀切正常。
运⾏上⾯代码⼗⼏遍,⼀切也都正常。
所以,我们能下结论:这段代码是线程安全的吗?
NO!
多线程中,只要存在同时读取和修改⼀个全局变量的情况,如果不采取其他措施,就⼀定不是线
程安全的。
尽管,有时,某些情况的资源竞争,暴露出问题的概率 极低极低 :
本例中,如果线程0 在修改a后,其他某些线程还是get到的是没有修改前的值,就会暴露问题。
但是在本例中, a = a + 1 这种修改操作,花费的时间太短了,短到我们⽆法想象。所以,线
程间轮询执⾏时,都能get到最新的a值。所以,暴露问题的概率就变得微乎其微。
6 【案例】多线程变量竞争引起的脏数据问题
只要弄明⽩问题暴露的原因,叫问题出现还是不困难的。
想象数据库的写⼊操作,⼀般需要耗费我们可以感知的时间。
为了模拟这个写⼊动作,简化期间,我们只需要延⻓修改变量a的时间,问题很容易就会还原出
来.
import threading
import time
a = 0
def add1():
global a
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写⼊所需时间
a = tmp
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
重新运⾏代码,只需⼀次,问题⽴⻢完全暴露,结果如下
t0 adds a to 1: 1 t1 adds a to 1: 1 t2 adds a to 1: 1 t3 adds a to 1: 1 t4 adds a to 1: 1 t5 adds a
to 1: 1 t7 adds a to 1: 1 t6 adds a to 1: 1 t8 adds a to 1: 1 t9 adds a to 1: 1
看到,10个线程全部运⾏后,a的值只相当于⼀个线程执⾏的结果。
下⾯分析,为什么会出现上⾯的结果:
这是⼀个很有说服⼒的例⼦,因为在修改a前,有0.2秒的休眠时间,某个线程延时后,CPU⽴即
分配计算资源给其他线程。直到分配给所有线程后,根据结果反映出,0.2秒的休眠时⻓还没耗
尽,这样每个线程get到的a值都是0,所以才出现上⾯的结果。
以上最核⼼的三⾏代码:
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写⼊所需时间
a = tmp
7 使⽤多线程锁解决多线程并发问题
知道问题出现的原因后,要想修复问题,也没那么复杂。
通过python中提供的锁机制,某段代码只能单线程执⾏时,上锁,其他线程等待,直到释放锁
后,其他线程再争锁,执⾏代码,释放锁,重复以上。
创建⼀把锁locka:
import threading
import time
locka = threading.Lock()
通过 locka.acquire() 获得锁,通过locka.release()释放锁,它们之间的这些代码,只能单线程执
⾏。
a = 0
def add1():
global a
try:
locka.acquire() # 获得锁
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写⼊所需时间
a = tmp
finally:
locka.release() # 释放锁
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
执⾏结果如下:
t0 adds a to 1: 1
t1 adds a to 1: 2
t2 adds a to 1: 3
t3 adds a to 1: 4
t4 adds a to 1: 5
t5 adds a to 1: 6
t6 adds a to 1: 7
t7 adds a to 1: 8
t8 adds a to 1: 9
t9 adds a to 1: 10
⼀切正常,其实这已经是单线程顺序执⾏了,就本例⼦⽽⾔,已经失去多线程的价值,并且还带
来了因为线程创建开销,浪费时间的副作⽤。
程序中只有⼀把锁,通过 try...finally还能确保不发⽣死锁。但是,当程序中启⽤多把锁,还是很
容易发⽣死锁。
注意使⽤场合,避免死锁,是我们在使⽤多线程开发时需要注意的⼀些问题。
8 讨论GIL锁存在何时选⽤多线程、进程问题?
GIL是什么?GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考
虑,为了数据安全所做的决定。
由于锁的存在,每个CPU在同⼀时间,只能执⾏⼀个线程。
并⾏:同⼀时刻,多个线程同时执⾏
并发:多线程交替获取时间⽚,并发执⾏,同⼀个时刻可以只有⼀个线程执⾏
mac系统检查cpu核数:
命令:sysctl -n machdep.cpu.core_count 结果:8
某个线程想要执⾏,必须先拿到GIL,我们可以把GIL看作是“通⾏证”,并且在⼀个python进程
中,GIL只有⼀个。拿不到通⾏证的线程,就不允许进⼊CPU执⾏
那么是不是python的多线程就完全没⽤了呢?
在这⾥进⾏分类讨论:
1、CPU密集型代码(各种循环处理、计数等等),在这种情况下,ticks计数很快就会达到阈值,然
后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程
对CPU密集型代码并不友好。
2、IO密集型代码(⽂件处理、⽹络爬⾍等),多线程能够有效提升效率(单线程下有IO操作会进⾏IO
等待,造成不必要的时间浪费,⽽开启多线程能在线程A等待时,⾃动切换到线程B,可以不浪费
CPU的资源,从⽽能提升程序执⾏效率)。所以python的多线程对IO密集型代码⽐较友好。
尤其对于密集型任务,“python下想要充分利⽤多核CPU,就⽤多进程”,原因是什么呢?
原因是:每个进程有各⾃独⽴的GIL,互不⼲扰,这样就可以真正意义上的并⾏执⾏,所以在
python中,多进程的执⾏效率优于多线程(仅仅针对多核CPU⽽⾔)。