![cover](https://img-home.csdnimg.cn/images/20230309035550.png)
线程、进程、协程的总结详细
线程、进程、协程的同步机制、通信机制、生命周期的详细描述,以及线程、进程、协程之间的对比
线程、进程、协程的总结详细
1 、进程
1.1 进程是什么呢?
进程是资源分配的最小单位,是最小的资源管理单元。
直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。
每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
1.2 生命周期
当程序需要运行时,操作系统将代码和所有静态数据记载到内存和进程的地址空间(每个进程都拥有唯一的地址空间,见下图所示)中,通过创建和初始化栈(局部变量,函数参数和返回地址)、分配堆内存以及与IO相关的任务,当前期准备工作完成,启动程序,OS将CPU的控制权转移到新创建的进程,进程开始运行。
PCB:
操作系统对进程的控制和管理通过PCB(Processing Control Block),PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息(进程标识号,进程状态,进程优先级,文件系统指针以及各个寄存器的内容等),进程的PCB是系统感知进程的唯一实体。
一个进程至少具有5种基本状态:初始态、执行状态、等待(阻塞)状态、就绪状态、终止状态。
- 初始状态:进程刚被创建需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。此时进程刚被创建,由于其他进程正占有CPU所以得不到执行,只能处于初始状态。
- 就绪状态:进程已经准备好,且已分配到所需资源,只要分配到cpu就能立即运行。
- 执行状态:进程处于就绪状态的经过调度才能到执行状态。任意时刻处于执行状态的进程只能有一个。
- 阻塞状态:正在执行的进程由于某些事件(IO请求,申请缓存区失败)而暂时无法运行,进程受到阻塞,在满足请求时进入就绪状态等待系统调用。
- 停止状态:进程结束或出现错误或系统终止,进入终止状态,无法再执行。
1.3 进程同步机制
一、同步机制的主要任务
进程同步机制的主要任务是对多个相关的进程在执行次序上进行协调,使并发执行的诸多进程之间能够按照一定的规则共享系统资源,并能很好的相互合作,从而使程序之间的执行具有可再现性。
一、进程间的两种制约关系:
- 间接相互制约(互斥):因为进程在并发执行的时候共享临界资源而形成的相互制约的关系,需要对临界资源互斥地访问;
- 直接制约关系(同步):多个进程之间为完成同一任务而相互合作而形成的制约关系。
二、 临界资源:
指同一时刻只允许一个进程可以该问的资源称之为临界资源,诸进程之间采取互斥方式,实现对临界资源的共享。
三、临界区
进程中访问临界资源的那段代码。
每个进程在进入临界区之前,应先对欲访问的临界资源的“大门”状态进行检测,如果“大门”敞开,进程便可进入临界区,并将临界区的“大门”关上;否则就表示有进程在临界区内,当前进程无法进入临界区。
二、同步机制应遵循的规则
- 空闲让进:当临界区的“大门”敞开时,应当允许一个请求的进入临界区的进程立即进入临界区。
- 忙则等待:当临界区的“大门”关闭时,因而其他试图进入临界区的进程必须等待,以保证对临界资源的互斥访问。
- 有限等待:对要求进入临界区的进程,应保证有限的时间能进入自已的临界区,以免陷入“死等” 状态。
- 让权等待:当进程不能进入自已的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。
“忙等 ”和 “死等” 都是没能进入临界区,它们的区别如下:
- 死等: 对行死等的进程来说,这个进程可能是处于阻塞状态,等着别的进程将其唤醒(signal 原语),但是如果唤醒原语一直无法执行,对于阻塞的进程来说,就是一直处于死等的状态,是无法获得处理机的。
- 忙等:忙等状态比较容易理解,处于忙等状态的进程是一直占有处理机去不断的判断临界区是否可以进入,在此期间,进程一直在运行,这就是忙等状态。有一点需要注意的是,忙等是非常可怕的,因为处于忙等的进程会一直霸占处理机的,相当于陷入死循环了。 忙等的状态在单CPU 系统中是无法被打破的,只能系统重启解决。
1.4 进程通信机制
进程通信( InterProcess Communication,IPC)就是指进程之间的信息交换。为了保证安全,每个进程的用户地址空间都是独立的,一般而言一个进程不能直接访问另一个进程的地址空间,不过内核空间是每个进程都共享的,所以 「进程之间想要进行信息交换就必须通过内核」
进程间通信有六大机制,如下:
1.4.1 管道
管道的本质就是内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。
- 匿名管道
Linux 管道使用竖线 | 连接多个命令,这被称为管道符 :$ command1 | command2
管道的功能是将前一个命令(command1)的输出,作为后一个命令(command2)的输入,从这个功能描述中,我们可以看出 「管道中的数据只能单向流动」,也就是半双工通信,如果想实现相互通信(全双工通信),我们需要创建两个管道才行,一个方向一个。
另外,通过管道符 | 创建的管道是匿名管道,用完了就会被自动销毁。并且,匿名管道只能在具有亲缘关系(父子进程)的进程间使用。也就是说,「匿名管道只能用于父子进程之间的通信」。
- 有名管道
匿名管道由于没有名字,只能用于父子进程间的通信。为了克服这个缺点,提出了有名管道,也称做FIFO,因为数据是先进先出的传输方式。
所谓有名管道也就是提供一个路径名与之关联,这样,即使与创建有名管道的进程不存在亲缘关系的进程,只要可以访问该路径,就能够通过这个有名管道进行相互通信。
使用 Linux 命令 mkfifo 来创建有名管道:$ mkfifo mypipe
1.4.2 消息队列
管道这种进程通信方式虽然使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。为此,消息传递机制(Linux 中称消息队列)应用而生。
消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。 比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程在需要的时候自行去消息队列中读取数据就可以了。同样的,B 进程要给 A 进程发送消息也是如此。
如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。对比一下管道机制:
- 消息队列允许一个或多个进程向它写入或读取消息。
- 消息队列可以实现消息的 「随机查询」,不一定非要以先进先出的次序读取消息,也可以按消息的类型读取。比有名管道的先进先出原则更有优势。
- 对于消息队列来说,在某个进程往一个队列写入消息之前,并不需要另一个进程在该消息队列上等待消息的到达。而对于管道来说,除非读进程已存在,否则先有写进程进行写入操作是没有意义的。
- 消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列就会一直存在。而匿名管道(父子进程之间使用)随进程的创建而建立,随进程的结束而销毁。
消息队列对于交换较少数量的数据很有用,因为无需避免冲突。但是,由于用户进程写入数据到内存中的消息队列时,会发生从用户态拷贝数据到内核态的过程;同样的,另一个用户进程读取内存中的消息数据时,会发生从内核态拷贝数据到用户态的过程。因此,如果数据量较大,使用消息队列就会造成频繁的系统调用,也就是需要消耗更多的时间以便内核介入。
1.4.3 共享内存
为了避免像消息队列那样频繁的拷贝消息、进行系统调用,共享内存机制出现了。
共享内存就是允许不相干的进程将同一段物理内存链接到它们各自的地址空间中,使得这些进程可以访问同一个物理内存,这个物理内存就称为共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
共享内存的原理:首先,每个进程都有属于自己的进程控制块(PCB)和逻辑地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的逻辑地址(虚拟地址)与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存。
不同于消息队列频繁的系统调用,对于共享内存机制来说,仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间切换CPU状态来回拷贝,所以这是最快的一种进程通信方式。
1.4.4 信号量和PV操作
对具有多 CPU 系统的最新研究表明,在这类系统上,消息传递的性能其实是要优于共享内存的,因为消息队列无需避免冲突,而共享内存机制可能会发生冲突 。也就是说如果多个进程同时修改同一个共享内存,先来的那个进程写的内容就会被后来的覆盖。
并且,在多道批处理系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进(异步性)。但有时候我们又希望多个进程能密切合作,按照某个特定的顺序依次执行,以实现一个共同的任务。
举个例子,如果有 A、B 两个进程分别负责读和写数据的操作,这两个线程是相互合作、相互依赖的。那么写数据应该发生在读数据之前。而实际上,由于异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区还没有被写入数据,读进程 A 没有数据可读,因此读进程A被阻塞。
因此,为了解决上述这两个问题,保证共享内存在任何时刻只有一个进程在访问(互斥),并且使得进程们能够按照某个特定顺序访问共享内存(同步),我们就可以使用进程的同步与互斥机制,常见的比如信号量与 PV 操作。
进程的同步与互斥其实是一种对进程通信的保护机制,并不是用来传输进程之间真正通信的内容的,但是由于它们会传输信号量,所以也被纳入进程通信的范畴,称为低级通信.
信号量其实就是一个变量 ,我们可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为 1 的信号量。用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现进程互斥或同步。这一对原语就是 PV 操作:
P 操作和 V 操作必须成对出现,缺少 P 操作就不能保证对共享内存的互斥访问,缺少 V 操作就会导致共享内存永远得不到释放、处于等待态的进程永远得不到唤醒。
1.4.5 信号
信号是进程通信机制中唯一的 「异步」 通信机制,它可以在任何时候发送信号给某个进程。「通过发送指定信号来通知进程某个异步事件的发生,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行」。用户、内核进程都能生成和发送信号
信号事件的来源主要有硬件来源和软件来源。所谓硬件来源就是说我们可以通过键盘输入某些组合键给进程发送信号,比如常见的组合键 Ctrl+C 产生 SIGINT 信号,表示终止该进程;而软件来源就是通过 kill 系列的命令给进程发送信号,比如 kill -9 1111 ,表示给 PID 为 1111 的进程发送 SIGKILL 信号,让其立即结束。
1.4.6 socket
上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通信的,如果想要「跨网络与不同主机上的进程进行通信」,那该怎么做呢?这就是 Socket 通信做的事情了(「当然,Socket 也能完成同主机上的进程通信」)
Socket 被翻译为「套接字」,它是计算机之间进行通信的一种约定或一种方式。通过 Socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
从计算机网络层面来说,「Socket 套接字是网络通信的基石」,是支持 TCP/IP 协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的 IP 地址,本地进程的协议端口,远地主机的 IP 地址,远地进程的协议端口。
Socket 的本质其实是一个编程接口(API),是应用层与 TCP/IP 协议族通信的中间软件抽象层,它对 TCP/IP 进行了封装。它「把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面」。对用户来说,只要通过一组简单的 API 就可以实现网络的连接。
1.4.7 总结 Linux 内核提供的进程通信机制
总结一下上面六种 Linux 内核提供的进程通信机制:
- 首先,最简单的方式就是 「管道」,管道的本质是存放在内存中的特殊的文件。也就是说,内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区的操作。管道分为匿名管道和有名管道,匿名管道只能在父子进程之间进行通信,而有名管道没有限制。
- 虽然管道使用简单,但是效率比较低,不适合进程间频繁地交换数据,并且管道只能传输无格式的字节流。为此 「消息队列」 应运而生。消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。如果进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。
- 消息队列的速度比较慢,因为每次数据的写入和读取都需要经过用户态与内核态之间数据的拷贝过程,「共享内存」 可以解决这个问题。所谓共享内存就是:两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
对于共享内存机制来说,仅在建立共享内存区域时需要系统调用,一旦建立共享内存,所有的访问都可作为常规内存访问,无需借助内核。这样,数据就不需要在进程之间来回拷贝,所以这是最快的一种进程通信方式。 - 享内存速度虽然非常快,但是存在冲突问题,为此,我们可以使用信号量和 PV 操作来实现对共享内存的互斥访问,并且还可以实现进程同步。
- 「信号」 和信号量是完全不同的两个概念!信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程。通过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。
- 上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通信的,如果想要跨网络与不同主机上的进程进行通信,就需要使用 「Socket」 通信。另外,Socket 也能完成同主机上的进程通信。
2、线程
2.1 线程是什么呢?
线程是CPU调度的最小单位,是最小的执行单元。 线程又叫做轻量级进程,线程从属于进程,是程序的实际执行者。
一个进程至少包含一个主线程,也可以有更多的子线程。
多个线程共享所属进程的资源,同时线程也拥有自己的专属资源、拥有自己的栈空间。
线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
2.2 线程同步机制
一个进程中的各个线程都有共享的资源而且是完全开放的,那么在进程运行中会出现多个线程访问同一个公共资源的问题。这种现象我们称之为线程之间产生了资源竞争,这种竞争会导致程序异常甚至崩溃。
线程同步四种方法:互斥锁、信号量、条件变量、读写锁
2.2.1 互斥锁
又称互斥体或互斥量,是最简单的一种线程同步机制,顾名思义,当一个线程访问的时候,就会把资源“锁”上,直到访问结束才会解锁,给其他线程访问。互斥锁本身就是一个全局变量:unlock 和 lock
- ”unlock“:表示当前资源可以访问,第一个访问资源的线程将互斥锁修改为”lock“,访问完以后再修改为”unlock“
- "lock"表示线程正在访问资源,其他线程需要等待互斥锁的值为"unlock"才能继续访问。
该线程负责的加锁,解锁也需要该线程。
2.2.2 信号量
又称信号灯,控制同时访问公共资源的线程数量,当线程数量小于等于1时,这种信号量可以叫二元信号量,同理多的时候,叫多元信号量,是指同一时刻最后只有这么多个线程可以访问该资源。
信号量的取值范围必须>=0;值得一提的是信号量可以执行加一和减一的操作,而且这种操作还是原子操作来实现的。原子操作,你可以理解为多个线程修改信号量,但是在修改值的过程中互不干扰。
具体操作流程:
- 信号量不能小于零
- 线程访问资源时,信号量减一,访问完成加一。
- 信号量为0时候,其他访问线程需要等待,直到大于0。
信号量分类:二元信号量,计数信号量
1、二元信号量初始值为1,信号量的值只有0和1,一定程度上替代互斥锁进行线程同步。
2、计数信号量,初始值大于1,可以允许多个线程同一时间访问同一资源,起到限制访问个数的作用。
2.2.3 条件变量
功能类似现实中的门,只有打开和关闭两种状态,对应条件中的成立与不成立两种判定,一旦关闭,所有线程都不得访问该资源,一旦打开,那就恢复执行。通常条件变量和互斥锁是搭配使用的。
条件变量的本质也是全局变量,它的功能是阻塞线程,直到收到条件成立的信号,被阻塞的程序才能继续执行。
具体流程:
- 阻塞线程,直到收到信号
- 向等待队列中一个或者所有线程发送条件成立的信号,解除被阻塞的状态。
为了避免多线程抢资源的情况发生,条件变量必须和互斥锁搭配使用。
2.2.4 读写锁
如果很多线程只是进行读取操作,只有少部分是写操作(修改),可以使用读写锁。读写锁的核心思想是将线程访问共同数据发出的请求分类:
- 读请求:只读,不修改共享数据
- 写请求: 存在修改共享数据的操作
当有多个读线程的时候,他们可以同时访问,但是写线程就必须要等他们读完才能访问,反过来也是,只不过写线程必须要一个一个来访问。读线程访问时候,读写锁称之为读锁,写线程访问时候,读写锁称之为写锁。
死锁
死锁指的是线程一直被阻塞的情况。比如:给线程加上互斥锁但是忘了解锁,那就会出现一直阻塞的情况。
避免死锁的建议:
- 使用互斥锁,信号量,条件变量,读写锁的时候
(1)占用互斥锁的进程要及时解锁
(2)通过sem_wait()函数占用信号量资源的线程,及时调用sem_post()函数进行释放
(3)当线程phtread_cond_wait()函数被阻塞时,一定要保证有其他线程唤醒此线程
(4)无论线程占用的是读锁还是写锁,都要及时解锁 - POSIX标准中,很多阻塞线程的函数都提供两个版本tryxxx(),一种是timexxx();其中try是不会阻塞线程,time不会一直阻塞线程,多使用这两种可以大大降低死锁的概率。(pthread_mutex_lock,pthread_mutex_trylock)
(sem_wait,sem_trywait)(pthread_cond_wait,pthread_cond_timewait) - 多线程程序中,多个线程申请资源的顺序最好一致,比如线程1先申请matex1在申请matex2,而线程2先申请matex2,再申请matex1,就会发生顺序不一致导致的死锁。
2.3 线程通信机制
线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。线程通信主要可以分为三种方式,分别为共享内存、消息传递和管道流。每种方式有不同的方法来实现:
- 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。volatile共享内存
- 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。wait/notify等待通知方式、join方式
- 管道流:管道输入/输出流的形式。
2.4 线程生命周期
线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、死亡。当线程进入运行状态后,一般的操作系统是采用抢占式的方式来让线程获得CPU。所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞、就绪之间切换。
- 新建:使用new方法,new出来线程,此时仅仅由JAVA虚拟机为其分配内存,并初始化成员变量的值。此时仅仅是个对象。
- 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;该线程进入就绪状态,JAVA虚拟机会为其创建方法调用栈和程序计数器。线程的执行是由底层平台控制, 具有一定的随机性。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;(当处于就绪状态的线程获得CPU,它就会执行run()方法)。对于一个单核cpu(或者是一个内核)来说,只能同时执行一条指令,而JVM通过快速切换线程执行指令来达到多线程的,真正处理器就能同时处理一条指令,只是这种切换速度很快,我们根本不会感知到。为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。当一个线程开始运行后,它不可能一直持有CPU(除非该线程执行体非常短,瞬间就执行结束了)。所以,线程在执行过程中需要被中断,目的是让其它线程获得执行的CPU的机会。线程的调度细节取决于底层平台所采用的策略。
- 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态。原因如下:
1.等待I/O流的输入输出
2.等待网络资源,即网速问题
3.调用sleep()方法,需要等sleep时间结束
4.调用wait()方法,需要调用notify()唤醒线程
5.其他线程执行join()方法,当前线程则会阻塞,需要等其他线程执行完。
- 死亡:
(1)run()/call()方法执行完成,线程正常结束;
(2)线程抛出一个未捕获的Exception或Error;
(3)直接调用线程的stop()方法结束该线程——该方法容易导致死锁,通常不建议使用。
3、 协程
3.1 协程是什么?
在多核场景下,如果是I/O密集型场景,就算开多个线程来处理,也未必能提升CPU的利用率,反而会增加线程切换的开销。另外,多线程之间假如存在临界区或者共享数据,那么同步的开销也是不可忽视的。协程,正好可以解决上面的相关问题。
协程是一种用户态的轻量级线程。在一个用户线程上可以跑多个协程,这样就提高了单核的利用率。协程不像进程或者线程,可以让系统负责相关的调度工作,协程是处于一个线程中,系统是无感知的,所以需要在该线程中阻塞某个协程的话,就需要手工进行调度。
- 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。
- 一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
- 与其让操作系统调度,不如我自己来,这就是协程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程与线程主要区别是它将不再被内核调度,而是交给了程序自己而线程是将自己交给内核调度,所以也不难理解golang中调度器的存在。
4、进程、线程、协程对比
4.1 进程与线程比较
线程是指进程内的一个执行单元,也是进程内的可调度实体。线程与进程的区别:
- 地址空间:进程有自己独立的地址空间,进程之间相互独立。线程是进程内的一个执行单元,一个进程内的所有线程共享地址空间。
- 一个进程内至少有一个线程,线程是一个进程中代码的不同执行路线。某进程内的线程在其它进程不可见。
- 资源拥有:进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见。
- 线程是处理器调度的基本单位,但进程不是。
- 二者均可并发执行。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
4.2 协程与线程比较
- 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。
- 线程进程都是同步机制,而协程则是异步。
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
总结:
- IO密集型一般使用多线程或者多进程,
- CPU密集型一般使用多进程,
- 强调非阻塞异步并发的一般都是使用协程,当然有时候也是需要多进程线程池结合的,或者是其他组合方式。
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)