go的调度
Golang
的调度器采用M:N
调度模型,其中M代表用户级别的线程(也就是goroutine
),而N代表的事内核级别的线程。Go
调度器的主要任务就是N个OS线程上调度M个goroutine
。这种模型允许在少量的OS线程上运行大量的goroutine
。
Go
调度器使用了三种队列来管理goroutine
- 全局队列(Global Queue):此队列中包含了所有刚创建的
goroutine
。 - 本地队列(Local Queue):每个P(Processor,处理器)都有一个本地队列,P会有限从本地队列中取出
goroutine
来执行。 - 网络轮循器(Netpoller):此队列中包含了所有在等待网络时间(如IO操作)的
goroutine
。当网络事件就绪时,对应的goroutine
会被放入到全局队列中,等待被P取出。
Go
的调度器采用了工作窃取(Work Stealing)和手动抢占(Preemption)的策略
- 工作窃取:当一个P的本地队列中没有
goroutine
时,它会尝试从全局队列或其他P的本地队列中窃取goroutine
来执行。 - 手动抢占:为了防止一个
goroutine
长时间占用P而导致其他goroutine
饿死,Go
的调度器会定期地进行抢占操作。在Go 1.14
之前,Go
的调度器只在函数调用时才会进行抢占,从Go 1.14
开始引入了异步抢占,即允许在任何安全点进行抢占。
go struct能不能比较
Golang
的struct
是否能比较取决于struct
内变量的类型:
所有字段可比较:只有当结构体的所有字段都可以进行比较时,结构体本身才可以进行比较。如果结构体包含不可比较的字段,如切片(slice)、映射(map)或函数(func),则该结构体不能进行比较。
逐字段比较:结构体的比较是逐字段进行的,即比较结构体的每个字段的值。如果所有字段的值都相等,则两个结构体相等;否则,它们不相等。
go defer
基本用法
在Go语言中,defer
关键字用于在函数返回前执行一段代码或调用一个清理函数。这对于处理文件关闭、解锁或者返回一些资源到资源池等操作非常有用。
其基本用法如下所示:
package main
import "fmt"
func main() {
example()
}
func example() {
defer fmt.Println("world")
fmt.Println("hello")
}
defer fmt.Println("world")
语句会在函数example
返回之前执行,所以输出的结果是:
执行顺序
当我们在一个函数内部调用defer
关键字,Go实际上会把它后面的函数(通常是一个匿名函数或者清理函数)压入一个栈中。当外部函数准备返回的时候,Go会按照先进先出的(LIFO)的顺序调用并执行这个栈中的所有函数。
比如,如下示例:
package main
import "fmt"
func main() {
example()
}
func example() {
defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
fmt.Println("function body")
}
其输出结果为:
参数计算时机
需要注意的是,defer
语句的参数会在defer
语句处就计算好,而不是在外部函数返回时才计算。比如如下例子
package main
import "fmt"
func main() {
example()
}
func example() {
i := 0
defer fmt.Println(i)
i++
fmt.Println(i)
}
/*
1
0
*/
实际应用举例
关闭文件
package main
import (
"fmt"
"io/ioutil"
"os"
)
func readFile(fileName string) (string, error) {
file, err := os.Open(fileName)
if err != nil {
return "", err
}
defer file.Close() // 确保文件在函数返回前被关闭
content, err := ioutil.ReadAll(file)
if err != nil {
return "", err
}
return string(content), nil
}
func main() {
content, err := readFile("example.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("File content:", content)
}
解锁互斥锁
package main
import (
"fmt"
"sync"
)
var mtx sync.Mutex
var cnt int
const N int = 10
func increment() {
mtx.Lock()
defer mtx.Unlock()
cnt++
}
func main() {
var wg sync.WaitGroup
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Final count: ", cnt)
}
释放网络连接
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func fetchURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close() // 确保连接在函数返回前被关闭
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func main() {
content, err := fetchURL("http://baidu.com")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("URL content:", content)
}
select可以用于什么
select
关键字用于处理同时来自多个通道的数据。它的基本工作原理是“随机选择”满足条件的分支去执行。如果没有分支满足条件(即所有通道都无法读/写),select
会阻塞,直到有分支满足条件。如果select
包含default
分支,当其他分支都不满足条件时,default
分支会被执行。
Go
的select
底层使用了一种名为scase
的结构体,表示一个select
分支,包含了通道和对应的操作类型(发送或者接收)。同时,它还会使用一个名为hchan
的结构体来表示通道的内部结构。
以下是select
的一些重要的特性:
- 公平性:在
Go
语言中,select
语言会随机选择一个可以运行的case执行,保证了每一个case都有公平的机会被执行,避免了饥饿问题。 - 非阻塞:如果
select
中所有的case都无法运行,而且存在default
分支,那么select
就不会被阻塞,而是执行default
分支。 - 可用于时间操作:
select
经常与time.After
,time.Tick
等函数一起使用,用于实现超时操作或者定时操作。 - 可用于退出操作:
select
经常和context
一起使用,当收到context
的取消信号时,可以安全地退出协程。
package main
import (
"fmt"
"time"
)
func selectExample(c1, c2 chan int, quit chan bool) {
for {
select {
case v := <-c1:
fmt.Println("Received from c1:", v)
case v := <-c2:
fmt.Println("Received from c2:", v)
case <-quit:
fmt.Println("Quit signal received. Exiting.")
return
default:
fmt.Println("No data received.")
time.Sleep(1 * time.Second) // 添加延迟以避免过度占用CPU
}
}
}
func main() {
// 创建通道
c1 := make(chan int)
c2 := make(chan int)
quit := make(chan bool)
// 启动selectExample函数作为goroutine
go selectExample(c1, c2, quit)
// 启动发送数据到c1的goroutine
go func() {
for i := 0; i < 5; i++ {
c1 <- i
time.Sleep(1 * time.Second)
}
}()
// 启动发送数据到c2的goroutine
go func() {
for i := 0; i < 5; i++ {
c2 <- i + 10
time.Sleep(2 * time.Second)
}
}()
// 从主goroutine发送退出信号
time.Sleep(10 * time.Second) // 等待足够时间以查看输出
quit <- true
fmt.Println("Sent quit signal and waiting to exit.")
}
context包的用途
Golang
中,context
为我们提供了在跨API
边界和进程之间传递请求作用域的deadline
,取消信号,和其他请求响应的值的能力。
context
包定义了Context
类型,它在API
边界和进程之间提供了一种传递传递请求作用域的deadline
,取消信号,和其他请求响应的值的能力。一个Context
的生命周期通常与请求处理的生命周期相同,并且可以包含在多个API
调用和goroutine
之间共享数据和取消信号。
context
的主要方法有:
Deadline
:返回当前Context
合适会被取消。如果Context
不会被取消,则返回ok为false
;Done
:返回一个通道,当Context
被取消或超时的时候,该通道会被关闭。Err
:返回Context
为何被取消。Value
:返回与Context
相关的值,这些值必须是线程安全的。
Go
语言的context
包提供了两个函数用于创建Context
对象:context.Background()
和context.TODO()
,前者通常用在主函数、初始化以及测试代码中,表示一个空的Context
,后者通常用在不确定应该使用什么Context
,或者函数以后会更新以便接受一个Context
参数。
此外,context
包还提供了WithCancel
,WithDeadline
,WithTimeout
和WithValue
函数,从现有的Context
派生出新的Context
。
- context.Background(): 返回一个空的Context,这个Context通常被用在main函数、初始化以及测试时。
- context.TODO(): 当不确定应该使用什么Context,或者还未决定如何传递Context时,可以使用这个。
- context.WithCancel(parent Context) (ctx Context, cancel CancelFunc): 根据已有的Context创建一个新的可取消的Context。
- context.WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc): 根据已有的Context和设置的截止时间创建一个新的Context。
- context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc): 基于已有的Context和指定的超时时间创建一个新的Context。
使用场景:
- 超时控制:我们可以通过
context.WithTimeout
创建一个超时的Context
,当超时时间到达,该Context
就会自动取消。比如,在数据库操作或外部服务调用时,可以通过设置超时的context
来防止系统因为某个部分的响应缓慢而整体性能下降。 - 请求传递:在微服务或者并发编程的环境中,我们可以通过
context.WithValue
将请求相关的数据绑定到Context
中,在函数调用链路上下游之间传递。 - 请求取消:我们可以通过
Context.WithCancel
或context.WithTimeout
创建一个可被取消的Context
,并在需要取消的时候调用Context
的cancel
函数。比如在Web服务器中,如果客户端中断了请求,服务器可以通过context
来取消所有相关的goroutine
,从而避免资源浪费。 - 优雅地处理goroutine的生命周期: 通过
context
的Done
通道,可以监听到取消信号,从而在goroutine
中适时地释放资源,停止执行,保证程序的健売性和响应性。
package main
import (
"context"
"fmt"
"time"
)
func calculate(ctx context.Context, ch chan []int) {
for {
select {
case data := <-ch:
// 模拟一些处理过程
process(data)
case <-ctx.Done():
// 如果context收到取消信号(超时或显式取消),则停止操作
return
}
}
}
func process(data []int) {
// 实际处理数据的函数
fmt.Println(data)
}
func ContextCase() {
ctx := context.Background() // 创建一个背景context
ctx = context.WithValue(ctx, "desc", "ContextCase") // 向context中添加键值对,这里添加描述信息"ContextCase"
// 创建一个带有超时的context,超时时间为2秒
ctx, cancel := context.WithTimeout(ctx, time.Second*2)
defer cancel() // 确保在函数结束时取消context,以释放相关资源
data := [][]int{ // 初始化数据
{1, 2},
{3, 2},
}
ch := make(chan []int) // 创建一个传递数据的channel
go calculate(ctx, ch) // 启动一个goroutine来处理从channel接收的数据
for i := 0; i < len(data); i++ {
ch <- data[i] // 向channel发送数据
}
time.Sleep(time.Second * 10) // 主goroutine睡眠10秒
}
func main() {
ContextCase()
}
client如何实现长连接
- 使用
net
包进行低级 TCP 连接,适用于自定义协议或非 HTTP 通信。 - 使用
http
包配置http.Client
保持 HTTP 连接。 - 使用 WebSocket 实现实时双向通信,适用于需要实时数据传输的应用。
主协程如何等其余协程完再操作
使用sync.WaitGroup()
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
// 启动多个协程
numGoroutines := 3
for i := 1; i <= numGoroutines; i++ {
wg.Add(1) // 每启动一个协程前调用 Add(1)
go func(id int) {
defer wg.Done() // 协程完成后调用 Done()
fmt.Printf("Goroutine %d is working...\n", id)
time.Sleep(time.Second * time.Duration(id))
fmt.Printf("Goroutine %d is done.\n", id)
}(i)
}
// 等待所有协程完成
wg.Wait()
fmt.Println("All goroutines have completed. Main function can proceed.")
}
slice,len,cap,共享,扩容
len
求当前元素的数量,即长度。cap
求切片的容量。
Golang
切片的扩容机制如下:
- 初始容量:
- 新创建的切片默认容量为0或根据创建时提供的元素数量确定。
- 扩容规则:
- 当需要扩容时,Go语言的切片扩容机制通常是将其容量翻倍(
cap *= 2
),直到达到一定的阈值后(例如1024),扩容策略改为每次增长大约1/4(cap += cap / 4
),以降低大型切片的扩容开销。 - 自Go 1.16版本开始,扩容策略稍有改变,但仍基于翻倍原则,但在容量大于1024时,并非简单地增加1/4,而是使用更复杂的算法,旨在更好地平衡内存使用和性能。
- 当需要扩容时,Go语言的切片扩容机制通常是将其容量翻倍(
- 特殊情况:
- 当切片的当前容量小于1000时,Go可能并不会严格遵循上述翻倍策略,而是采用更保守的扩容策略,以适应小容量切片的需求。
- 如果所需的新增容量远大于当前容量,Go可能会直接将容量设置为所需容量,而不是简单地翻倍。
- 性能考量:
- 扩容操作伴随着内存分配和数据拷贝,因此在编写代码时应尽量避免过于频繁的扩容操作,可以预先估计数据量并使用make函数指定合适的初始容量。
map如何顺序读取
map
本身是无顺序的,所以要想顺序读取,就必须将map
中的内容存到一个切片中,然后对切片进行排序。
实现set
通过map
来实现,map
的键可以用来存储集合中的元素,而值可以被设为 struct{}
或 bool
。使用 struct{}
的好处是它不占用额外的内存空间。
package main
import (
"fmt"
)
// Set 结构体表示一个集合
type Set struct {
elements map[string]struct{}
}
// NewSet 创建一个新的 Set
func NewSet() *Set {
return &Set{
elements: make(map[string]struct{}),
}
}
// Add 向集合中添加元素
func (s *Set) Add(element string) {
s.elements[element] = struct{}{}
}
// Remove 从集合中删除元素
func (s *Set) Remove(element string) {
delete(s.elements, element)
}
// Contains 检查集合中是否包含某个元素
func (s *Set) Contains(element string) bool {
_, exists := s.elements[element]
return exists
}
// Size 返回集合中元素的数量
func (s *Set) Size() int {
return len(s.elements)
}
// Clear 清空集合
func (s *Set) Clear() {
s.elements = make(map[string]struct{})
}
// Elements 返回集合中的所有元素
func (s *Set) Elements() []string {
keys := make([]string, 0, len(s.elements))
for k := range s.elements {
keys = append(keys, k)
}
return keys
}
func main() {
mySet := NewSet()
mySet.Add("apple")
mySet.Add("banana")
mySet.Add("orange")
fmt.Println("Set contains 'apple':", mySet.Contains("apple"))
fmt.Println("Set size:", mySet.Size())
mySet.Remove("banana")
fmt.Println("Set contains 'banana':", mySet.Contains("banana"))
fmt.Println("Set elements:", mySet.Elements())
}
实现消息队列(多生产者,多消费者)
定义生产者和消费者函数:
producer
函数生成消息并将其发送到通道。consumer
函数从通道接收消息并处理它们。
创建一个带缓冲的通道:
queue
是一个带缓冲的通道,用来在生产者和消费者之间传递消息。
启动多个生产者和消费者:
- 使用
sync.WaitGroup
来跟踪生产者和消费者的完成情况。 - 每个生产者和消费者在各自的 goroutine 中运行。
等待所有生产者完成并关闭通道:
- 使用
wg.Wait()
等待所有生产者完成。 - 关闭通道以通知消费者没有更多消息。
等待所有消费者完成:
- 使用另一个
sync.WaitGroup
来跟踪消费者的完成情况。 - 确保所有消费者在通道关闭后都能处理完剩余的消息。
package main
import (
"fmt"
"sync"
"time"
)
// 生产者函数
func producer(id int, queue chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
msg := id*10 + i
fmt.Printf("Producer %d: produced %d\n", id, msg)
queue <- msg
time.Sleep(time.Millisecond * 100) // 模拟生产时间
}
}
// 消费者函数
func consumer(id int, queue <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for msg := range queue {
fmt.Printf("Consumer %d: consumed %d\n", id, msg)
time.Sleep(time.Millisecond * 150) // 模拟消费时间
}
}
func main() {
var wg sync.WaitGroup
queue := make(chan int, 10) // 创建一个带缓冲的通道
// 启动多个生产者
numProducers := 3
for i := 1; i <= numProducers; i++ {
wg.Add(1)
go producer(i, queue, &wg)
}
// 启动多个消费者
numConsumers := 2
for i := 1; i <= numConsumers; i++ {
wg.Add(1)
go consumer(i, queue, &wg)
}
// 等待所有生产者完成
wg.Wait()
close(queue) // 关闭通道以通知消费者没有更多消息
// 再次等待所有消费者完成
var consumerWg sync.WaitGroup
for i := 1; i <= numConsumers; i++ {
consumerWg.Add(1)
go func() {
defer consumerWg.Done()
for msg := range queue {
fmt.Printf("Final Consumer %d: consumed %d\n", i, msg)
time.Sleep(time.Millisecond * 150)
}
}()
}
consumerWg.Wait()
}
大文件排序
分块排序:
- 使用将大文件分块读取到内存,每次读取
chunk_size
行。 - 对每个小块进行排序,并将已排序的小块写入临时文件。
- 将每个小块文件的路径存储在
chunks
列表中。
归并排序:
- 打开所有小块文件,并初始化一个最小堆,将每个文件的第一行加入堆中。
- 逐步从堆中取出最小元素,并将其写入输出文件。
- 从相应的小块文件中读取下一行数据,如果该文件未读完,将下一行数据加入堆中;否则,关闭该文件。
- 重复以上过程直到堆为空,表示所有数据已排序并写入输出文件。
清理临时文件:
- 合并完成后,删除所有临时小块文件。
基本排序,哪些是稳定的
稳定的排序算法:冒泡排序、插入排序、归并排序、基数排序、计数排序。
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
Http get跟head
HTTP GET
功能
- 请求资源:
GET
请求用于从服务器请求指定的资源。服务器返回资源的内容和相关的头部信息。 - 常见用途:获取网页内容、下载文件、获取 API 数据等。
特点
- 包含响应体:
GET
请求的响应通常包含资源的完整内容,即响应体。 - 安全性和幂等性:
GET
请求是安全的(不会改变服务器的状态),也是幂等的(多次相同请求结果相同)。
示例
GET /index.html HTTP/1.1
Host: www.example.com
HTTP HEAD
功能
- 请求头部信息:
HEAD
请求与GET
请求类似,但服务器只返回头部信息,不返回响应体。 - 常见用途:检查资源是否存在、获取资源的元数据(如内容类型、长度、最后修改时间等)。
特点
- 无响应体:
HEAD
请求的响应不包含资源的内容,只有头部信息。 - 安全性和幂等性:
HEAD
请求是安全的,也是幂等的,通常用于在不下载整个资源的情况下获取资源的信息。
示例
HEAD /index.html HTTP/1.1
Host: www.example.com
Http 401,403
状态码 | 含义 | 主要原因 | 重试机会 |
---|---|---|---|
401 | 未授权 | 客户端未通过身份验证或未提供有效的凭据 | 可以,通过提供正确的身份验证信息 |
403 | 禁止访问 | 客户端已通过身份验证,但没有权限访问资源 | 不可以,权限问题无法通过重试解决 |
Http keep-alive
在 HTTP/1.0 中,每个请求/响应对都是通过单独的 TCP 连接来处理的,这意味着每次请求都需要开销较大的连接建立和关闭过程。HTTP/1.1 引入了持久连接(persistent connections),即 Keep-Alive
,允许在同一个连接上处理多个请求和响应。
GET /index.html HTTP/1.1
Host: www.example.com
Connection: keep-alive
Keep-Alive: timeout=10, max=100
timeout=10
表示如果在10秒内没有新的请求,连接将关闭;max=100
表示最多可以在该连接上发送100个请求。
Http能不能一次连接多次请求,不等后端返回
- 并行请求: 虽然单个 TCP 连接只能处理一个请求的响应周期,但客户端可以通过创建多个 TCP 连接来实现并行请求。通常,浏览器和现代 HTTP 客户端会使用多个并发的 TCP 连接来同时发起多个请求,以减少请求之间的等待时间。
- HTTP/2 多路复用: HTTP/2 协议支持在同一个 TCP 连接上进行多个并发的请求和响应。这种技术称为多路复用(Multiplexing),它允许多个 HTTP 请求和响应同时在同一个连接上进行传输,提高了性能和效率。
- 长连接(Keep-Alive): HTTP/1.1 引入了持久连接(Keep-Alive),允许在同一个 TCP 连接上发送多个 HTTP 请求和响应。在此模式下,客户端在发送完一个请求后不关闭连接,可以继续发送其他请求,服务器也可以在响应后保持连接打开以发送后续响应。这种方式虽然不能真正并行发送多个请求,但可以减少连接的建立和关闭开销,提升效率。
time-wait的作用
确保连接的正确关闭: TIME_WAIT
状态允许所有可能的延迟数据包被接收和处理,避免新的连接收到旧连接的数据包。
防止重复的数据包: 由于TCP连接关闭时,可能还有一些数据包在网络中传输。进入TIME_WAIT
状态可以确保这些数据包有足够的时间被处理,避免影响后续连接。
提供时间处理丢失的FIN包: 在关闭连接的过程中,双方需要确认FIN包。如果ACK包丢失了,发送FIN的一方需要在TIME_WAIT
期间重新发送FIN包,确保连接能正常关闭。
避免端口重用冲突: TIME_WAIT
状态确保在特定时间内,不能重用同一端口,以防止与旧连接的冲突。
数据库如何建索引
1. 基本索引类型
-
B-树索引:
- 最常见的索引类型,适用于大多数情况。
- 在MySQL中使用
CREATE INDEX
语句创建B-树索引。
CREATE INDEX index_name ON table_name(column_name);
-
唯一索引:
- 确保索引列中的所有值都是唯一的。
CREATE UNIQUE INDEX index_name ON table_name(column_name);
-
全文索引:
- 用于全文搜索,适用于大文本字段的快速搜索。
- MySQL中可以对
TEXT
类型的字段使用全文索引。
CREATE FULLTEXT INDEX index_name ON table_name(column_name);
-
哈希索引:
- 基于哈希表的索引,适用于等值查询。
- 一些数据库如MySQL的Memory引擎支持哈希索引。
CREATE INDEX index_name USING HASH ON table_name(column_name);
2. 创建复合索引
复合索引是基于多个列创建的索引,适用于涉及多个列的查询。
CREATE INDEX index_name ON table_name(column1, column2);
孤儿进程,僵尸进程
孤儿进程 (Orphan Process)
孤儿进程是指父进程已经终止,但子进程仍在运行的进程。操作系统会自动将孤儿进程的父进程重新分配给系统的init进程(在Unix和Linux系统中,PID为1的进程),从而确保这些孤儿进程不会没有父进程。
僵尸进程 (Zombie Process)
僵尸进程是指一个已经终止的子进程,但它的退出状态信息仍然保存在系统中,尚未被父进程读取。这个进程占用了一个进程ID(PID),但没有实际的进程资源。僵尸进程通常由父进程在调用wait()
或waitpid()
函数之前子进程终止时产生。
git文件版本,使用顺序,merge跟rebase
Merge(合并)
git merge
用于将一个分支的更改合并到当前分支。合并操作会创建一个新的提交,记录分支合并的历史。
git checkout main
git merge new_feature
Rebase(变基)
git rebase
用于将一个分支上的提交应用到另一个分支的基础之上。它会重写提交历史,使之看起来像是在目标分支的基础上直接进行的更改。
git checkout new_feature
git rebase main
最后给大家推荐一个LinuxC/C++高级架构系统教程的学习资源与课程,可以帮助你有方向、更细致地学习C/C++后端开发,具体内容请见 https://xxetb.xetslk.com/s/1o04uB