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

Python
248
0
0
2024-02-22
标签   Python进阶

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⽽⾔)。