网络并发编程之进程
操作系统发展史
进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的。所以想要真正了解进程,必须事先了解操作系统。
1.穿孔卡片时代
1946年第一台计算机诞生--20世纪50年代中期,计算机工作还在采用手工操作方式。此时还没有操作系统的概念。
程序员将对应于程序和数据的已穿孔的纸带(或卡片)装入输入机,然后启动输入机把程序和数据输入计算机内存,接着通过控制台开关启动程序针对数据运行;计算完毕,打印机输出计算结果;用户取走结果并卸下纸带(或卡片)后,才让下一个用户上机。
2.联机批处理系统
联机批处理系统,即作业的输入/输出由CPU来处理。
主机与输入机之间增加一个存储设备——磁带,在运行于主机上的监督程序的自动控制下,计算机可自动完成:成批地把输入机上的用户作业读入磁带,依次把磁带上的用户作业读入主机内存并执行并把计算结果向输出机输出。完成了上一批作业后,监督程序又从输入机上输入另一批作业,保存在磁带上,并按上述步骤重复处理。监督程序不停地处理各个作业,从而实现了作业到作业的自动转接,减少了作业建立时间和手工操作时间,有效克服了人机矛盾,提高了计算机的利用率。
但是,在作业输入和结果输出时,主机的高速CPU仍处于空闲状态,等待慢速的输入/输出设备完成工作: 主机处于“忙等”状态。
3.脱机批处理系统
为克服与缓解:高速主机与慢速外设的矛盾,提高CPU的利用率,又引入了脱机批处理系统,即输入/输出脱离主机控制。
卫星机:一台不与主机直接相连而专门用于与输入/输出设备打交道的。
其功能是:
(1)从输入机上读取用户作业并放到输入磁带上。
(2)从输出磁带上读取执行结果并传给输出机。
这样,主机不是直接与慢速的输入/输出设备打交道,而是与速度相对较快的磁带机发生关系,有效缓解了主机与设备的矛盾。主机与卫星机可并行工作,二者分工明确,可以充分发挥主机的高速计算能力。
不足之处:
每次主机内存中仅存放一道作业,每当它运行期间发出输入/输出(I/O)请求后,高速的CPU便处于等待低速的I/O完成状态,致使CPU空闲。
为改善CPU的利用率,又引入了多道程序系统。
所谓多道程序设计技术,就是指允许多个程序同时进入内存并运行。即同时把多个程序放入内存,并允许它们交替在CPU中运行,它们共享系统中的各种硬、软件资源。当一道程序因I/O请求而暂停运行时,CPU便立即转去运行另一道程序。
在A程序计算时,I/O空闲, A程序I/O操作时,CPU空闲(B程序也是同样);必须A工作完成后,B才能进入内存中开始工作,两者是串行的,全部完成共需时间=T1+T2。
将A、B两道程序同时存放在内存中,它们在系统的控制下,可相互穿插、交替地在CPU上运行:当A程序因请求I/O操作而放弃CPU时,B程序就可占用CPU运行,这样 CPU不再空闲,而正进行A I/O操作的I/O设备也不空闲,显然,CPU和I/O设备都处于“忙”状态,大大提高了资源的利用率,从而也提高了系统的效率,A、B全部完成所需时间<<T1+T2。
多道程序设计技术不仅使CPU得到充分利用,同时改善I/O设备和内存的利用率,从而提高了整个系统的资源利用率和系统吞吐量(单位时间内处理作业(程序)的个数),最终提高了整个系统的效率。
进程理论
什么是进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
简单的说进程就是运行中的程序。
进程的调度
进程运行的程序需要cpu来执行,假如有多个进程即多个程序同时运行着,cpu该如何工作,或者说是如果保证程序正常运行的呢?cpu该如何工作能保证其高效的工作?由此便有了进程的调度算法。
1.先来先服务(FCFS)调度算法
先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。
由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。
2.短作业(进程)优先调度算法(SJ/PF)
短作业(进程)优先调度算法(SJ/PF)是指对短作业或短进程优先调度的算法,该算法既可用于作业调度,也可用于进程调度。
但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。
3.时间片轮转(Round Robin,RR)法
-
-
- 时间片轮转(Round Robin,RR)法的基本思路是让每个进程在就绪队列中的等待时间与享受服务的时间成比例。在时间片轮转法中,需要将CPU的处理时间分成固定大小的时间片,例如,几十毫秒至几百毫秒。如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。
- 显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。
- 在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。
-
在轮转法中,加入到就绪队列的进程有3种情况:
-
-
-
- 一种是分给它的时间片用完,但进程还未完成,回到就绪队列的末尾等待下次调度去继续执行。
-
-
-
-
-
- 另一种情况是分给该进程的时间片并未用完,只是因为请求I/O或由于进程的互斥与同步关系而被阻塞。当阻塞解除之后再回到就绪队列。
-
-
-
-
-
- 第三种情况就是新创建进程进入就绪队列
- 如果对这些进程区别对待,给予不同的优先级和时间片从直观上看,可以进一步改善系统服务质量和效率。例如,我们可把就绪队列按照进程到达就绪队列的类型和进程被阻塞时的阻塞原因分成不同的就绪队列,每个队列按FCFS原则排列,各队列之间的进程享有不同的优先级,但同一队列内优先级相同。这样,当一个进程在执行完它的时间片之后,或从睡眠中被唤醒以及被创建之后,将进入不同的就绪队列。
-
-
4.多级反馈队列
-
-
- 前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。
- 而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。
- 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
- 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
- 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
-
进程的并行与并发
并行 : 并行是指两者同时执行,比如赛跑,两个人都在不停的往前跑;(资源够用,比如三个线程,四核的CPU )
并发 : 并发是指资源有限的情况下,两者交替轮流使用资源,比如一段路(单核CPU资源)同时只能过一个人,A走一段后,让给B,B用完继续给A ,交替使用,目的是提高效率。
区别:
并行是从微观上,也就是在一个精确的时间片刻,有不同的程序在执行,这就要求必须有多个处理器。
并发是从宏观上,在一个时间段上可以看出是同时执行的,比如一个服务器同时处理多个session。
同步异步阻塞非阻塞
状态介绍
在了解其他概念之前,我们首先要了解进程的几个状态。在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。
(1)就绪(Ready)状态
当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
(2)执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。
(3)阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。
同步和异步
同步:
调用任务原地等待任务完成再继续执行。
异步:
调用任务并且不等待任务完成而继续执行,任务的结果由反馈机制通知
阻塞与非阻塞
阻塞:
在任务执行结果返回之前,会挂起进程,结果返回后才会将阻塞的线程激活。
非阻塞:
调用结果还未返回也不会阻塞进程。
同步/异步与阻塞/非阻塞
1.同步阻塞形式
效率最低。举例来说,就是你在银行取钱时专心排队,在取到钱之前什么别的事都不做。
2.异步阻塞形式
如果在银行等待办理业务的人采用的是异步的方式,也就是领了一张小纸条等通知,假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;
异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息通知时被阻塞。
3.同步非阻塞形式
实际上是效率低下的。
想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,如果把打电话和观察排队的位置看成是程序的两个操作的话,这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的。
4.异步非阻塞状态
效率相对最高
因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,程序没有在两种不同的操作中来回切换。
比如说,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下,那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了。
进程的创建
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。
而对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:
1.系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)
2.一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)
3.用户的交互式请求,而创建一个新进程(如用户双击暴风影音)
4.一个批处理作业的初始化(只在大型机的批处理系统中应用)
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的:
1.在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)
2.在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。
关于创建的子进程,UNIX和windows
1.相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。
2.不同的是:在UNIX中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于windows系统来说,从一开始父进程与子进程的地址空间就是不同的。
进程的终结
1.正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)
2.出错退出(自愿,python a.py中a.py不存在)
3.严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try...except...)
multiprocess.process模块
process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。
以下是创建进程的两种方式:

from multiprocess import process import time def test(name): print('{} --- start'.format(name)) time.sleep(2) print('{} --- end'.format(name)) if __name__ == 'main': # 创建进程p # target为此进程执行的函数,arg为传入函数的参数(元组) p = Process(target=test,arg=('xie',)) p.start() # 启动进程 print('主结束')

from multiprocess import process class MyProcess(Process): def __init__(self,name): super().__init__() self.name = name def run(self): print('{} runing'.format(self.name)) if __name__ == 'main': p = MyProcess('xie') p.start() # 执行结果: xie runing
process模块方法

import time from multiprocessing import Process def f(name): print('hello', name) time.sleep(1) print('我是子进程') if __name__ == '__main__': p = Process(target=f, args=('bob',)) p.start() p.join() print('我是父进程') # 执行结果: hello,bob -> 我是子进程 -> 我是父进程 # 注释p.join()后执行结果: 我是父进程 -> hello,bob -> 我是子进程
注意要点
多个进程同时运行,子进程的执行顺序不是根据执行顺序决定的

from multiprocessing import Process import time def foo(name): time.sleep(1) print(name) if __name__ == '__main__': for i in range(5): p = Process(target=foo, args=(i,)) p.start() p.join() print('end...') # 执行结果: 大约每隔1秒打印name最后打印end...
默认进程之间数据是隔离的

from multiprocessing import Process def work(): global n n=0 print('子进程内: ',n) if __name__ == '__main__': n = 100 p=Process(target=work) p.start() print('主进程内: ',n) # 打印的n仍为100
僵尸进程与孤儿进程
僵尸进程
进程代码运行结束之后并没有直接结束而是需要等待回收子进程资源才能结束
孤儿进程
即主进程已经结束(非正常)但是子进程还在运行
守护进程:即守护着某个进程 一旦这个进程结束那么也随之结束
主进程创建守护进程
其一:守护进程会在主进程代码执行结束后就终止
其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

from multiprocessing import Process import time def test(name): print('%s is running' % name) time.sleep(3) print('%s is over' % name) if __name__ == '__main__': p = Process(target=test, args=('tom',)) p.daemon = True # 设置为守护进程(一定要放在start语句上方) p.start() print("主进程结束") time.sleep(1) ''' 结果为: 主进程结束 tom is running '''
互斥锁
进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,但是在并发情况下操作同一份数据 极其容易造成数据错乱。

import json from multiprocessing import Process # 查票 def search(name): with open(r'data.json', 'r', encoding='utf8') as f: data_dict = json.load(f) ticket_num = data_dict.get('ticket_num') print('%s查询余票:%s' % (name, ticket_num)) # 买票 def buy(name): # 先查票 with open(r'data.json', 'r', encoding='utf8') as f: data_dict = json.load(f) ticket_num = data_dict.get('ticket_num') # 模拟一个延迟 # time.sleep(random.random()) # 判断是否有票 if ticket_num > 0: # 将余票减一 data_dict['ticket_num'] -= 1 # 重新写入数据库 with open(r'data.txt', 'w', encoding='utf8') as f: json.dump(data_dict, f) print('%s: 购买成功' % name) else: print('不好意思 没有票了!!!') def run(name, ): search(name) buy(name) if __name__ == '__main__': for i in range(1, 11): p = Process(target=run, args=('用户%s' % i,)) p.start() ''' 结果为: 用户1查询余票:1 用户1: 购买成功 用户4查询余票:1 用户4: 购买成功 用户2查询余票:1 用户2: 购买成功 用户3查询余票:1 用户3: 购买成功 用户6查询余票:1 用户6: 购买成功 用户5查询余票:1 用户5: 购买成功 用户7查询余票:1 用户7: 购买成功 用户9查询余票:1 用户9: 购买成功 用户8查询余票:1 用户8: 购买成功 用户10查询余票:1 用户10: 购买成功 '''

import json from multiprocessing import Process, Lock import time import random # 查票 def search(name): with open(r'data.txt', 'r', encoding='utf8') as f: data_dict = json.load(f) ticket_num = data_dict.get('ticket_num') print('%s查询余票:%s' % (name, ticket_num)) # 买票 def buy(name): # 先查票 with open(r'data.txt', 'r', encoding='utf8') as f: data_dict = json.load(f) ticket_num = data_dict.get('ticket_num') # 模拟一个延迟 time.sleep(random.random()) # 判断是否有票 if ticket_num > 0: # 将余票减一 data_dict['ticket_num'] -= 1 # 重新写入数据库 with open(r'data.txt', 'w', encoding='utf8') as f: json.dump(data_dict, f) print('%s: 购买成功' % name) else: print('不好意思 没有票了!!!') def run(name, mutex): search(name) mutex.acquire() # 抢锁 buy(name) mutex.release() # 释放锁 if __name__ == '__main__': mutex = Lock() for i in range(1, 11): p = Process(target=run, args=('用户%s' % i, mutex)) p.start() ''' 结果为: 用户4查询余票:1 用户3查询余票:1 用户1查询余票:1 用户2查询余票:1 用户5查询余票:1 用户6查询余票:1 用户7查询余票:1 用户9查询余票:1 用户8查询余票:1 用户10查询余票:1 用户4: 购买成功 不好意思 没有票了!!! 不好意思 没有票了!!! 不好意思 没有票了!!! 不好意思 没有票了!!! 不好意思 没有票了!!! 不好意思 没有票了!!! 不好意思 没有票了!!! 不好意思 没有票了!!! 不好意思 没有票了!!! Process finished with exit code 0 '''
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
q.get( [ block [ ,timeout ] ] ) 返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。
q.get_nowait( ) 同q.get(False)方法。
q.put(item [, block [,timeout ] ] ) 将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。
q.qsize() 返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。
q.empty() 如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。
q.full()

from multiprocessing import Queue q = Queue(5) # 括号内可以填写最大等待数 # 存放数据 q.put(111) q.put(222) # print(q.full()) # False 判断队列中数据是否满了 q.put(333) q.put(444) q.put(555) # print(q.full()) # q.put(666) # 超出范围原地等待 直到有空缺位置 # 提取数据 print(q.get()) print(q.get()) print(q.get()) print(q.get()) print(q.get()) # print(q.get()) # 没有数据之后原地等待直到有数据为止 # print(q.get_nowait()) # 没有数据立刻报错 ''' 结果为: 111 222 333 444 555 '''
概念介绍
IPC定义:IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
实例

from multiprocessing import Queue, Process def producer(q): q.put("子进程p放的数据") # 将数据存入子进程p的队列 def consumer(q): print('子进程c取的数据', q.get()) # 取出子进程p的队列中的数据 if __name__ == '__main__': q = Queue() p = Process(target=producer, args=(q,)) c = Process(target=consumer, args=(q,)) p.start() c.start() ''' 结果为: 子进程c取的数据 子进程p放的数据 '''
概念介绍
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

from multiprocessing import Queue, Process, JoinableQueue import time import random def producer(name, food, q): for i in range(2): print('%s 生产了 %s' % (name, food)) q.put(food) time.sleep(random.random()) def consumer(name, q): while True: data = q.get() print('%s 吃了 %s' % (name, data)) q.task_done() if __name__ == '__main__': # q = Queue() q = JoinableQueue() p1 = Process(target=producer, args=('大厨a', '美食a', q)) p2 = Process(target=producer, args=('大厨b', '美食b', q)) p3 = Process(target=producer, args=('大厨c', '美食c', q)) c1 = Process(target=consumer, args=('消费者', q)) p1.start() p2.start() p3.start() c1.daemon = True c1.start() # 因而c1也没有存在的价值了,不需要继续阻塞在进程中影响主进程了。应该随着主进程的结束而结束,所以设置成守护进程就可以了。 p1.join() p2.join() p3.join() q.join() # 等待队列中所有的数据被取干净 print('主程序结束') ''' 结果为: 大厨a 生产了 美食a 大厨b 生产了 美食b 大厨c 生产了 美食c 消费者 吃了 美食a 消费者 吃了 美食b 消费者 吃了 美食c 大厨b 生产了 美食b 消费者 吃了 美食b 大厨c 生产了 美食c 消费者 吃了 美食c 大厨a 生产了 美食a 消费者 吃了 美食a 主程序结束 '''
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通