Golang笔记_day01

一.  进程 线程 协程

1.概念

进程:进程基本上是一个正在执行的程序,它是操作系统中最小的资源分配单位。

线程:线程是进程的子集,也称为轻量级进程。一个进程可以有多个线程,这些线程由调度器独立管理。一个进程内的所有线程都是相互关联的。线程是操作系统中最小的调度单位。

协程:可以看作轻量级线程,他的内存占用少只要 2k,且上下文切换成本低,是一个独立执行的函数,由 go 语言启动,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。

2.对比

进程上下文切换开销:

1.地址空间

2.硬件上下文

线程上下文切换开销:

1.硬件上下文

2.同一进程下不切换地址空间

goroutine 切换开销:

1.用户态,不用象线程和进程一样多进行一次内核用户态切换

2.只需要保存/恢复三个寄存器的值,开销远远小于线程

其余优点:goroutine 的栈空间为 2k,线程为 2m,进程是 10m

由进程,线程,goroutine 的上下文切换可以明显看出是一个逐步减负的过程,这个过程可以结合它们的结构来理解,coverco 故而自带 goroutine 的 go 语言在高并发开发中有着得天独厚的优势。

3.编程思想

异步场景中需要对线程和协程进行人为的控制,就需要用到Context   sync.WaitGroup 以及channel等技术。

二.Context

1.概念

context 是 golang 中的经典工具,主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制. 除此之外,context 还兼有一定的数据存储能力. 本着知其然知其所以然的精神,本文和大家一起深入 context 源码一探究竟,较为细节地对其实现原理进行梳理.

2.原理

参考:https://zhuanlan.zhihu.com/p/597234214

3.代码案例

func WithCancle() {
    time1 := time.Now()
    wg.Add(1)
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
       ip, err := GetIp(ctx)  //一个新的context对象传入协程
       if err != nil {
          fmt.Println(err)
       }
       fmt.Println(ip)
       wg.Done()
    }()

    go func() {
       time.Sleep(2 * time.Second)
       cancel()
    }()
    wg.Wait()
    fmt.Println("Done", time.Since(time1))

}
func GetIp(ctx context.Context) (ip string, err error) {
    go func() {
       select {
       case <-ctx.Done():
          fmt.Println("协程取消", ctx.Err())
       }
       wg.Done()
       err = ctx.Err()
       return
    }()

    time.Sleep(4 * time.Second)
    ip = "172.0.9.1"
    return
}

三.指针

1.概念

每一个变量都会分配一块内存,数据保存在内存中,内存有一个地址,就像门牌号,通过这个地址就可以找到里面存储的数据。

指针就是保存这个内存地址的变量。

在 Go 语言中, 指针包括两个核心概念:

  • 类型指针,允许对这个指针类型的数据进行修改。传递数据使用指针,而无需拷贝数据。类型指针不能进行偏移和运算。
  • 切片,由指向起始元素的原始指针、元素数量和容量组成。

受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃

2.代码案例

//形参与局部变量等同
//函数传参:值传递,实参将自己的值拷贝一份给形参

func Point() {
	var x = 99
	y := &x
	fmt.Printf("x:%v, \n y:%v \n", x, y)

	a, b := 10, 20
	swap(a, b)
	fmt.Println("Point1 a:", a, "b:", b)
	swap2(&a, &b)
	fmt.Println("Point2 a:", a, "b:", b)
	swap3(&a, &b)
	fmt.Println("Point3 a:", a, "b:", b)
	swap4(&a, &b)
	fmt.Println("Point4 a:", a, "b:", b)
	swap5(a, b)
	fmt.Println("Point5 a:", a, "b:", b)
}
func swap(a, b int) {
	a, b = b, a
	fmt.Println("swap a:", a, "b:", b)
}

func swap2(a, b *int) {
	a, b = b, a                         //注意此处的a b 已经是内存地址了,地址值交换了没有意义,地址指向的值还是没有改变
	fmt.Println("swap2 a:", a, "b:", b) //swap2 a: 0xc0000aef28 b: 0xc0000aef20
}

func swap3(a, b *int) {
	*a, *b = *b, *a
	fmt.Println("swap3 a:", *a, "b:", *b)
}

func swap4(a, b *int) {
	*a = 11
	*b = *a
	fmt.Println("swap4 a:", *a, "b:", *b)
}

func swap5(a, b int) {
	p := &a
	q := &b
	fmt.Println("swap5 p:", p, "q:", q) //swap5 p: 0xc0000aef30 q: 0xc0000aef38  此处的内存地址与swap2的内存地址已经不同了,说明函数传参:值传递,实参将自己的值拷贝一份给形参

}

四.Channel

1.概念

在 Go 语言中,Channel 是一种数据结构,用于在多个 goroutine 之间传递消息。它类似于管道的概念,支持两个基本操作:发送和接收。Channel 通过 <- 符号来发送和接收数据,确保 goroutine 之间能够安全地通信。

type hchan struct {
    qcount   uint           // 缓冲区中元素的数量
    dataqsiz uint           // 缓冲区的大小
    buf      unsafe.Pointer // 缓冲区指针
    elemsize uint16         // 每个元素的大小
    closed   uint32         // 是否已关闭
    sendx    uint           // 发送索引
    recvx    uint           // 接收索引
    recvq    waitq          // 等待接收的 goroutine 队列
    sendq    waitq          // 等待发送的 goroutine 队列
    lock     mutex          // 锁
}

  • qcount:缓冲区中当前元素的数量。
  • dataqsiz:缓冲区的大小,若为 0 则为无缓冲 Channel。
  • buf:指向缓冲区的指针,用于存储 Channel 中传递的数据。
  • elemsize:Channel 中元素的大小,用于内存对齐。
  • closed:标记 Channel 是否已关闭。
  • sendx 和 recvx:发送和接收操作的索引,指示在缓冲区中数据的读取和写入位置。
  • recvq 和 sendq:等待接收和发送的 goroutine 队列。当 Channel 无法立即进行传输时,goroutine 会被加入这些队列中等待被唤醒。
  • lock:保证对 Channel 操作的互斥。

2.Channel 的类型

Go 语言中的 Channel 有两种类型:无缓冲 Channel 和带缓冲 Channel。

      无缓冲 Channel:发送和接收操作必须同时准备好,才能进行数据的传输。这意味着发送操作会阻塞直到接收操作准备好,反之亦然。

      带缓冲 Channel:创建时可以指定缓冲区大小。发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区为空时才会阻塞。

3.channel的特性

(1) 线程安全:channel是线程安全的,多个协程可以同时读写一个channel,而不会发生数据竞争的问题。这是因为Go语言中的channel内部实现了锁机制,保证了多个协程之间对channel的访问是安全的。

(2) 阻塞式发送和接收:当一个协程向一个channel发送数据时,如果channel已经满了,发送操作会被阻塞,直到有其他协程从channel中取走了数据。同样地,当一个协程从一个channel中接收数据时,如果channel中没有数据可供接收,接收操作会被阻塞,直到有其他协程向channel中发送了数据。这种阻塞式的机制可以保证协程之间的同步和通信

(3) 顺序性:通过channel发送的数据是按照发送的顺序进行排列的

(4) 可以关闭:  close(ch)

(5) 缓冲区大小:  ch := make(chan int, 10)

4.使用 Channel 的注意事项

  1. 不要重复关闭 Channel:Channel 只能被关闭一次,重复关闭会引发 panic。
  2. 不要从 nil Channel 中收发数据:nil Channel 会导致永久阻塞。
  3. 关闭 Channel 后只可接收,不可发送:从已关闭的 Channel 接收不会阻塞,但向已关闭的 Channel 发送数据会引发 panic。

5.Channel 的应用场景

  1. goroutine 间通信:Channel 是 goroutine 之间传递数据的最佳方式。
  2. 任务调度:可以使用 Channel 实现任务的分发和处理,如工作池模型。
  3. 信号通知:Channel 可用于实现同步信号,例如通过一个 done Channel 通知其他 goroutine 某个任务已完成。

6.代码案例

func Channel1() {
	//定义channel
	ch := make(chan int)
	//1 将数据发送到channel
	ch <- 11
	//接收发送的数据
	aa := <-ch
	fmt.Println(aa)
	//关闭channel
	close(ch)
}
func Channel2() {
	rand.Seed(time.Now().UnixNano())
	a, b := longTimeRequest(), longTimeRequest()
	fmt.Println(sumSquares(<-a, <-b))
	/*
		sumSquares函数的两个参数获取是同时的. 两个channel receive 会阻塞,直到channel send操作执行. 参数获取时间大概需要3s而不是6s.
	*/
}

func longTimeRequest() <-chan int32 {
	r := make(chan int32)
	go func() {
		// 模拟任务花费时间
		time.Sleep(time.Second * 3)
		r <- rand.Int31n(100)
	}()
	return r
}

func sumSquares(a, b int32) int32 {
	return a*a + b*b
}
// channel for range
func ChannleRange() {
	//管道传值
	wg.Add(2)
	ch := make(chan int, 10)
	go func() {
		defer func() {
			close(ch)
			wg.Done()
		}()
		for i := 0; i < 10; i++ {
			fmt.Println("ch <-", i)
			ch <- i
		}
		fmt.Println("add ok!!")
	}()

	go func() {
		for {
			if v, ok := <-ch; ok { //判断 channel 是否被关闭
				fmt.Println(v, ":=<-ch")
			} else {
				fmt.Println("ch is close")
				wg.Done()
				return //注意这里的处理,当监听到ch已经关闭,就要告知上层进程本线程已经结束,wg.Done()并不能直接终止线程,所以后面还需要用return 返回
			}
		}
	}()

	wg.Wait()
	fmt.Println("ch finish")
}
func SelectChannel() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(1 * time.Second)
		ch1 <- 1
	}()

	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 2
	}()
	for i := 0; i < 2; i++ {
		select {
		case data, ok := <-ch1:
			if ok {
				fmt.Println("从 ch1 接收到数据:", data)
			} else {
				fmt.Println("通道已被关闭")
			}
		case data, ok := <-ch2:
			if ok {
				fmt.Println("从 ch2接收到数据: ", data)
			} else {
				fmt.Println("通道已被关闭")
			}
		}
	}

	select {
	case data, ok := <-ch1:
		if ok {
			fmt.Println("从 ch1 接收到数据:", data)
		} else {
			fmt.Println("通道已被关闭")
		}
	case data, ok := <-ch2:
		if ok {
			fmt.Println("从 ch2接收到数据: ", data)
		} else {
			fmt.Println("通道已被关闭")
		}
	default:
		fmt.Println("没有接收到数据,走 default 分支")
	}
}

func OverTimeChannel() {
	ch := make(chan int)
	go func() {
		time.Sleep(5 * time.Second)
		ch <- 11
	}()

	select {
	case data, ok := <-ch:
		if ok {
			fmt.Println("接收到数据:", data)
		} else {
			fmt.Println("通道被关闭")
		}
	case <-time.After(3 * time.Second):
		fmt.Println("超时了!")
	}
	//注意 这里的case 不是监听同一个channel 的不同情况,而是不同渠道的不同处理方式
}

func GoroutineChannel() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			ch1 <- i
			time.Sleep(time.Second)
		}
	}()

	go func() {
		for j := 0; j < 10; j++ {
			ch2 <- j
			time.Sleep(time.Second)
		}
	}()

	for i := 0; i < 20; i++ {
		select {
		case data := <-ch1:
			fmt.Println("data from ch1:", data)
		case data := <-ch2:
			fmt.Println("data from ch2:", data)
			//default: //使用 default 实现非阻塞读写,不要default流程就会阻塞一直监听channel数据发送
			//	fmt.Println("no data ready")
			//	time.Sleep(time.Second)
		}
	}
	fmt.Println("finish")
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值