并发编程1
# 1.并发编程基本概念
- 什么是串行?
- “串行”(Serial)是一种数据传输方式,指的是数据以位一次一个的方式进行发送和接收。比如说,一个 8 位的字节,将通过 8 次单独的脉冲来交换,数据一位位按顺序串行传输,这就是串行传输。串行传输的优势是可以通过简单的数据线来实现,节省了成本,并且能跨越较长的距离。但其缺点是数据传输速度相对较慢。
- 在计算机中, 同一时刻, 只能有一条指令, 在一个 CPU 上执行, 后面的指令必须等到前面指令执行完才能执行, 就是串行
- 什么是并行?
- “并行”(Parallel)是一种数据传输方式,指多个数据位同时通过多条通道或数据线进行传输。假设你有一个 8 位的字节,你可以同时在 8 条通道上发送这 8 位,大大提高了数据传输的速度。这就是并行数据传输。这种方式相对于串行方式,利用的是空间换取时间,通过增加传输线实现了高速数据传输。然而并行传输也有其缺点: 首先,它需要更多的物理线路,这就增加了布线的复杂性和成本;其次,由于电磁干扰的原因,当距离较远时可能会导致数据出错,因此并行数据传输通常被限制在短距离,例如在电脑内部或者局部网络中使用。
- 在计算机中, 同一时刻, 有多条指令, 在多个 CPU 上执行, 就是并行
- 什么是并发?
- "并发"是指一个时间段内,两个或更多的任务在单个处理器上交替进行。尽管任务的执行可能会一种重叠的行为,但是在任何给定的时刻,处理器只执行一个任务。例如,在某些操作系统中,我们可以同时打开多个应用程序,这些应用程序都在运行,看起来是同时运行,但实际上是由于计算机的高速操作和任务切换,让我们感觉它们都在同时运行。
- 并发是伪并行, 就好比银行只有 1 个窗口, 有 3 个人要办事, 那么没轮到后面的人时, 后面的人可以用拖鞋先排队, 去吃个早餐,买个东西啥的, 感觉差不多要到自己时再回来办事
- 在计算机中, **同一时刻, 只能有一条指令, 在一个 CPU 上执行, 但是 CPU 会快速的在多条指令之间轮询执行**就是并发
- 什么是程序?
- 程序是指将编译型语言编写好的代码通过编译工具编译之后存储在硬盘上的一个二进制文件, 会占用磁盘空间, 但不会占用系统资源
- 例如我们通过 C++编写了一个聊天程序, 然后通过 C++编译器将编写好的代码编译成一个二进制的文件, 那么这个二进制的文件就是一个程序
- 什么是进程?
- 进程是指程序在操作系统中的一次执行过程, 是系统进行资源分配和调度的基本单位
- 程序可以在系统中开启多个进程互不影响
- 所以程序和进程的关系是 1:N, 所以多个进程的空间是独立的
- 什么是线程?
- 线程是指进程中的一个执行实例, 是程序执行的最小单元, 它是比进程更小的能独立运行的基本单位
- 一个进程中至少有一个线程, 这个线程我们称之为主线程
- 一个进程中除了主线程以外, 我们还可以创建和销毁多个线程
- 什么是协程?
- “协程”(Coroutine)则是一种计算机程序的组件,可以更方便地用来进行多任务处理,尤其是在 I/O 密集和事件驱动的程序中。协程的特性在于它允许多个进入点和挂起、恢复等操作。协程与线程非常相似,但是协程是完全在用户空间中实现的,所以更轻量级,切换的开销较小,并且协程的调度完全由用户来控制,这使得它在一些特殊的应用场景(例如异步 I/O、事件循环等)能够得到更好的性能。
# 2.Go 并发
- Go 在语言级别支持协程(多数语言在语法层面并不直接支持协程), 叫做 goroutine。
- 人们把 Go 语言称之为 21 世纪的 C 语言. 第一是因为 Go 语言设计简单, 第二是因为 21 世纪最重要的就是并行程序设计.而 Go 从语言层面就支持并发和并行。
package main
import (
"fmt"
"time"
)
func sing() {
for i := 0; i < 10; i++ {
fmt.Println("我在唱歌")
time.Sleep(time.Millisecond)
}
}
func dance() {
for i := 0; i < 10; i++ {
fmt.Println("我在跳舞---")
time.Sleep(time.Millisecond)
}
}
func main() {
// 串行: 必须先唱完歌才能跳舞
//sing()
//dance()
// 并行: 可以边唱歌, 边跳舞
// 注意点: 主线程不能死, 否则程序就退出了
go sing() // 开启一个协程
go dance() // 开启一个协程
}
- runtime 包中常用的函数
- Gosched:使当前 go 程放弃处理器,以让其它 go 程运行。
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("hhhh")
// 主协程
for i := 0; i < 2; i++ {
runtime.Gosched() //主协程释放CPU时间片,此时上面的协程得以执行
fmt.Println("main") //CPU时间片回来后继续执行
}
}
- Goexit: 终止调用它的 go 程, 其它 go 程不会受影响。
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit() // 结束当前协程
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("A")
}()
time.Sleep(time.Second) //睡一会儿,不让主协程很快结束
}
【Go】并发编程之 runtime 包及其常用方法runtime.goos想变厉害的大白菜的博客-CSDN 博客 (opens new window)
# 3.高并发的核心工具:协程
1、 线程是系统分配 CPU 资源的单位。协程本身并不能占有 CPU 或者内存资源。
2、runtime 中与协程有关的主要有 3 个结构体:G 代表协程,M 代表线程,P 代表处理器
3、窃取式协程调度的关键是 P 结构体,P 结构体持有了一个线程本地队列,存放需要被调度的协程。相比全局队列, 本地队列的锁冲突情况大大降低了。
4、抢占式调度解决了协程饥饿问题。由于种种原因,协程切换的时机不容易获得,导致很多协程长时间无法调用。 抢占式调度主动释放协程,使得协程饥饿的情况大大降低了。