暂时踢走这个奇葩
阻塞和非阻塞,异步和同步。经常笛卡尔积。组合成4个概念。其中单个理解起来,脑袋就要炸了,更何况四个组合根本无法理解。没事,这篇文章,我敢肯定会教给你。首先第一步,踢出阻塞异步这个奇葩。我们只看,阻塞同步,非阻塞同步,非阻塞异步。
进程
我们写代码,多简单,一些命令,一点运行,结果出来了。但是操作系统可没闲着。我们写的程序只是文本。如何让电脑运行。电脑会给他分配资源,cpu,内存。并给他分配时间。执行它的逻辑。我们的代码只是文本。操作系统执行它的过程叫进程。
线程
进程会直接去全部的操作系统资源里取。线程可以理解为小进程。它是进程底下的。进程从操作系统那获得了资源。线程又从进程处分配它获得的资源。但本质上。线程也是操作系统执行它的一切相关过程。
主线程
这个线程是个相对概念。不是一个特殊线程。一个线程在执行一个逻辑。只要它开小差去执行其它的了。我们就相对的说它原本的是主线程。
阻塞和非阻塞
这些概念都是对于线程来说的。当有其它任务时,主线程无法在执行它原本的任务,就叫阻塞。线程还可以就叫非阻塞。还记得线程吗,阻塞意味着,操作系统的全部调度去执行其它任务,而不原本。代码想要运行必须由操作系统去在底层真正的执行它。
同步和异步
暂时抛开线程。这些概念也是指代码是否还可以执行原本的逻辑。比如说 一个代码的任务就负责输出1
def download():
for i in range(10):
print("下载")
i=0
for i in range(100):
print(1)
i+=1
if i==10:
download()
可以看到系统无法去执行它原来的任务。必须输出完下载才可以继续输出它的1.此时就叫同步。如果下载时不影响1的输出就叫异步。
# 两个线程一个线程输出1,一个线程输出下载中
import threading
def print_num():
for i in range(1, 11):
print(1)
def print_download():
for i in range(1, 11):
print("下载中")
t=threading.Thread(target=print_download)
t.start()
print_num()
异步,主线程输出1,下载根本不影响1的输出。
正文开始
如果我们踢出阻塞异步这个奇葩。你会发现这些逻辑想吃饭一样简单。
当一个只有一个线程时,切换任务,意味着操作系统的调度换执行逻辑了。比如操作系统正在从内存获取1输出。当切换任务,你只有一个线程,它只能从内存里找下载中。不可能再去执行任何关于1的任何底层逻辑。此时线程阻塞,就意味着同步,阻塞同步。(注意 这个结论是在我们不考虑阻塞异步的情况下,注意,注意,注意)
而我们想实现异步怎么办呢。只能通过多个线程(没有阻塞异步)。很简单的逻辑。线程只能执行一个任务。那我执行其它任务时,我换个线程,你原来的线程该干嘛就干嘛不就行了。这就叫非阻塞异步。
这个代码演示上面已经有了。1和下载中的输出。同步我们只用主线程。线程做其它事情,我们相对来说它原来做的任务叫主线程,主线程阻塞了。而异步,我们用了多线程,主线程没阻塞,逻辑上也实现了异步。
那么非阻塞同步呢。我们讲了同步和异步只是逻辑上的代码执行顺序。虽然主线程没被阻塞。但我可以代码逻辑,这个任务必须等其它任务执行完才能运行。此时就像同步一样,原任务无法执行了,可以理解为主线程可以执行,只是业务逻辑上不运行。
t.start()
print_num()
t.join()
print("下载完成")
这是上面的多线程代码加上join
上面多线程代码,我么已经演示过了,主线程压根没被阻塞。我们输出1很正常,没任何问题。但是代码逻辑上。print(“下载完成”)必须等任务完成才会执行主线程。
精彩的地方快来了
好了重点来了。我们真想逻辑就这样清晰下去。但是抱歉。有阻塞异步。我们逃避了那么久。还是要面对。奇怪了,奇怪了。你不是说。代码执行新任务线程无法继续原先的任务,线程是操作系统执行代码的过程,它只能执行一个吗。原线程无法继续,被阻塞了。哪来的异步。怎么可能逻辑上又可以同时运行。别慌,马上告诉你,如何实现。
并行和并发
我们的电脑可以同时跑多个进程,多个线程。但是同时跑,真的是在同时跑吗?
并行是多核cpu或多台机器一起工作,每个cpu核或机器都可以运行进程,所以是真的同时运行。但是比如我的电脑算高配了。24个核,难道我的电脑只能跑24个进程。(题外话,这也是为啥要分布式。这是真正的并行)。哪台电脑没几百个进程。
这是怎么回事呢。这样的同样运行其实是操作系统的小把戏,它会根据调度算法,让进程交替运行。它运行一个时间,另个再运行点时间。只要换得够快,就实现了同时运行的效果。这叫并发。所以多线程,我们看起来一起运行其实是交替运行。那么阻塞异步。我们想想可不可以用一个线程也模拟这个过程呢? 真是个挑战。你只有一个线程。我们讲了如果给你另个线程。我们就叫非阻塞了。
真是个巨大的挑战
为什么是挑战呢,因为单线程里的代码,都是贪心鬼,让我执行就必须执行完
还是这个例子,你只要敢让下载任务开始,它必须执行完。看起来我们很难实现它执行一会,主线程执行会交替执行的效果。
最精彩的地方终于来了
相信我,真的很精彩。你会感叹程序员大佬的厉害,一个和并发毫不相干的东西,居然会有一天在并发上,大放光彩。
生成器
我们在处理数据时可能会遇到特别长的数据,全部读入你内存是不可能的任务,我们可以动态的生成它,每用一个生成一个。
比如
# 请注意,这个bigdata我们使用列表来保存,实际上,真实的情况应该是,这些数据应该是在文件中。并且很大比如50gb
bigdata = list(range(10))
class readlines:
def __init__(self, bigdata):
self.bigdata = bigdata
self.pos = -1
def __enter__(self):
return self.read_lines_generator()
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def read_lines_generator(self):
while self.pos < len(self.bigdata)-1:
self.pos += 1
yield self.bigdata[self.pos]
with readlines(bigdata) as r:
while True:
try:
print(next(r))
except StopIteration:
break
需要说明一下。我们定义了个列表,来模拟一个超级大的文件。我们的列表会加载到内存中。但不要在意,我们的重点时理解一个新家伙,yield。明白它的执行逻辑。而且这个代码已经抽象出来了大文件的处理方法。
我们不会把大文件全部读到内存中,而是维护self.pos = -1,当前文件的位置。你要时我给你一行,你再要,我就再给你一行。
而这个一行一行读的实现就靠的就是yield。我们第一次调用next()。代码运行到yield处,产出值后它会暂停,不会继续下次循环。直到你下次调用next() 才会去继续下一次循环去生成新的值,所以叫生成器。
看看你有没有大佬的潜质,奇妙的地方来了,想到了什么。
协程
哈哈哈 我们讲生成器,阻塞了一下我们今天的主要任务阻塞异步。但该收回来了,希望不要忘记我们的逻辑。我们想要用单线程模拟多线程的交替运行。但发现,任务都是贪婪的,让他运行,它必须运行完。我们不想,我们不想让他运行完,让它和主线程交替运行。等一下,不运行完,不运行完。yield 不正是这样嘛
没错就是靠这个,我们实现了,用单线程模拟了两个任务的交替运行,然后yield从生成器,步入了并发编程这个战场。
import time
def download():
while True:
try:
url = yield
print("downloading")
time.sleep(1)
except GeneratorExit:
print("GeneratorExit")
break
except Exception as e:
print("Exception", e)
d=download()
for i in range(10):
print("主线程")
next(d)
他们开始交替运行了。
并发,并发,一个生成器用的东西,可以并发编程怎么能不让人惊叹,这部分用最精彩的地方,完全不夸张。 有yield 的函数叫协程
收尾和一点小抱歉
不要忘记我们今天的主题,是异步和同步,阻塞和非阻塞之间的组合。我们认识
到了,代码逻辑上的能否一起运行和线程之间关系。当没有阻塞异步时,这些组合特别容易理解。我们单线程如果它去执行其它任务了,这个线程就不能执行原来的任务,线程阻塞。逻辑上同步这叫阻塞同步。 我们想异步。就多线程去执行其它任务。主线程留着。线程不阻塞,代码逻辑上异步。非阻塞异步。虽然线程没阻塞,但任务逻辑上我们要等其它任务完成。这叫非阻塞同步。
然后我们学到了协程可以模拟线程的交替运行。单个线程也可以实现异步。
篇幅有限,协程的很多东西都没讲,比如为什么叫协程这个名字。具体如何并发编程,他和线程比有什么优势。还有个yield的升级 yield from 也没讲。python为了让协程更好的并发有哪些改进,比如生成器只能产出值,加了send()使我们可以给协程传值。
但是不用怕,可能会迟到,但不会缺席。