前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多线程、协程和多进程并发编程

多线程、协程和多进程并发编程

原创
作者头像
软件架构师Michael
修改2023-12-23 12:30:25
2321
修改2023-12-23 12:30:25
举报
文章被收录于专栏:软件工程师Michael

1 如何通俗理解线程和进程?

进程:进程就是正在执⾏的程序。

线程:是程序执⾏的⼀条路径, ⼀个进程中可以包含多条线程。

通俗理解:例如你打开抖⾳,就是打开⼀个进程,在抖⾳⾥⾯和朋友聊天就是开启了⼀条线程。

再举⼀个例⼦:

在某⻝堂打饭的时候,此⻝堂安排三个打饭⼤妈打饭,所有同学依次排成三个队伍,每个打饭⼤

妈相当于⼀个线程。

这个⻝堂相当于⼀个进程,他⼀共有三个打饭⼤妈,相当于进程⾥有三个线程。

两者之间的关系:

⼀个进程⾥⾯可以有多条线程,⾄少有⼀条线程。

⼀条线程⼀定会在⼀个进程⾥⾯。

关于协程,我会放在后⾯讲完线程和进程时再讲解。

2 .Python如何启动⼀个线程?

⼀般的,程序默认执⾏只在⼀个线程,这个线程称为主线程,例⼦演示如下:

导⼊线程相关的模块 threading:

代码语言:python
代码运行次数:0
复制
import threading

threading的类⽅法 current_thread()返回当前线程:

代码语言:python
代码运行次数:0
复制
t = threading.current_thread()
print(t)

看到 MainThread,验证了程序默认是在MainThead中执⾏。

t.getName()获得这个线程的名字

其他常⽤⽅法,t.ident获得线程id

is_alive() 判断线程是否存活

那么,如何创建⾃⼰的线程呢?

3 .Python如何创建⼀个新线程?

创建⼀个线程:

代码语言:python
代码运行次数:0
复制
my_thread = threading.Thread()

创建线程的⽬的是告诉它帮助我们做些什么,做些什么通过参数target传⼊,参数类型为

callable,函数就是可调⽤的:

代码语言:python
代码运行次数:0
复制
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中:

代码语言:python
代码运行次数:0
复制
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个线程:

代码语言:python
代码运行次数:0
复制
[t.start() for t in threads]

打印结果如下,t0,t1,t2三个线程,根据操作系统的调度算法,轮询获得CPU时间⽚,注意观察:

代码语言:python
代码运行次数:0
复制
当前线程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 :

代码语言:python
代码运行次数:0
复制
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]
代码语言:python
代码运行次数:0
复制
#执⾏结果:
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的时间,问题很容易就会还原出

来.

代码语言:python
代码运行次数:0
复制
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,所以才出现上⾯的结果。

以上最核⼼的三⾏代码:

代码语言:python
代码运行次数:0
复制
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写⼊所需时间
a = tmp

7 使⽤多线程锁解决多线程并发问题

知道问题出现的原因后,要想修复问题,也没那么复杂。

通过python中提供的锁机制,某段代码只能单线程执⾏时,上锁,其他线程等待,直到释放锁

后,其他线程再争锁,执⾏代码,释放锁,重复以上。

创建⼀把锁locka:

代码语言:python
代码运行次数:0
复制
import threading
import time
locka = threading.Lock()

通过 locka.acquire() 获得锁,通过locka.release()释放锁,它们之间的这些代码,只能单线程执

⾏。

代码语言:python
代码运行次数:0
复制
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]

执⾏结果如下:

代码语言:python
代码运行次数:0
复制
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⽽⾔)。

未完待续...

我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档