V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  lesismal  ›  全部回复第 47 页 / 共 53 页
回复总数  1058
1 ... 39  40  41  42  43  44  45  46  47  48 ... 53  
2021-05-13 17:35:55 +08:00
回复了 ng29 创建的主题 Go 编程语言 资讯一个 golang 并发的问题
@baiyi
send 的时候先检查 recvq,等待队列有 waiter 的话直接发给第一个 waiter
https://github.com/golang/go/blob/master/src/runtime/chan.go#L207
并标记 waiter 的那个 g 为可运行状态,顺着代码往下看就是了
https://github.com/golang/go/blob/master/src/runtime/chan.go#L320

这里需要着重说的一点是,标记可运行不是立刻就运行,而且就算立刻运行,也不能保证 chan op 之后的一段代码全部在单次调度运行中执行完,所以你调试 chan 内部的实现逻辑,其实解释不了这个现象,解释现象,我 #12 的应该说得差不多了

recv 的逻辑也类似,代码就不贴了
2021-05-13 16:53:53 +08:00
回复了 ng29 创建的主题 Go 编程语言 资讯一个 golang 并发的问题
@baiyi golang 的内存模型,无缓冲的 chan,比如两个 goroutine 分别 send 、recv 之间,可以保证这两个 A chan op 前段代码先于 B chan op 后段代码执行,但不能保证 A 和 B op 后段代码的执行顺序,因为 chan op 之后的代码随时也可能被调度

比如

goroutine A:

some code... // 1
chan <- v
some code... // 3
some code... // 4

goroutine B:
<-chan
some code... // 2
some code... // 5
some code... // 6

这里能保证的是 1 先于 2/5/6 执行,但是不能保证 3 和 4,因为 3 和 4 执行之前就可能被调度了
2021-05-13 16:48:14 +08:00
回复了 ng29 创建的主题 Go 编程语言 资讯一个 golang 并发的问题
@baiyi 你多跑几次例子试试,至少我这里中间可以遇到这样的日志:

| <-r recv|10001
| <-r recv|10001
|write <- who|10000
|write <- who|10000
| <-r recv|10001
| <-r recv|10001
|write <- who|10000 ///////////// 不是连续的两次,也不是第一次和最后一次
| <-r recv|10001 ///////////// 不是连续的两次,也不是第一次和最后一次
|write <- who|10000
|write <- who|10000
| <-r recv|10001
| <-r recv|10001
|write <- who|10000
|write <- who|10000
| <-r recv|10001
| <-r recv|10001
2021-05-13 11:16:33 +08:00
回复了 ng29 创建的主题 Go 编程语言 资讯一个 golang 并发的问题
@baiyi 这样解释应该是不对的。
“然后在循环中再次的操作,才会阻塞,阻塞时自己也会在相应的等待队列中。所以每次的 send 或 recv 操作,都会执行两次,输出的形式也是连着输出”
—— 你都说了,再次的操作会阻塞,比如 A 的再次操作阻塞了,然后就暂时没走到 print,需要等 B 触发 A 的运行后才能 print,也就是说,A 的两次之间,只有一次 print,然后 B 触发了之后才能再次 print,这两次中间阻塞过、并不是连着输出。

你看 #11 的,通常一开始的时候日志是这样的,第一句他就不是连续的两次:
|write <- who|10000 // 不是连续的两次,只有一次
| <-r recv|10001
| <-r recv|10001
|write <- who|10000
|write <- who|10000

再举个例子,简单点,我开 4 个协程

runtime.GOMAXPROCS(1)
ch := make(chan int)
go count(ch, 10000)
go count(ch, 10001)
go count(ch, 10002)
go count(ch, 10003)

然后某段日志里:

| <-r recv|10003
| <-r recv|10003
| <-r recv|10001 // 不是连续的两次,只有一次
|write <- who|10002 // 不是连续的两次而是三次
|write <- who|10002 // 不是连续的两次而是三次
|write <- who|10002 // 不是连续的两次而是三次
| <-r recv|10001
| <-r recv|10001
|write <- who|10002
|write <- who|10002

runtime 的调度不可能是这样简单对 chan 稳定的两次然后就调度,即使是因为楼主代码的例子场景这两个协成比较均衡导致基本是出现两次,但这也并不是 runtime 提供的保证

看下我 #12 楼的解释
2021-05-12 14:08:22 +08:00
回复了 ng29 创建的主题 Go 编程语言 资讯一个 golang 并发的问题
@no1xsyzy
"而且 chan 和 printf 都是原子的,golang 默认没有 buffer 。"
—— 跟 printf 有没有 buffer 也没关系,即使有 buffer,先调用 printf 的也是先入 buffer
2021-05-12 14:03:58 +08:00
回复了 ng29 创建的主题 Go 编程语言 资讯一个 golang 并发的问题
@no1xsyzy 这个解释是错误的

“上面都不看 runtime.GOMAXPROCS(1) 的吗?”
—— 跟 runtime.GOMAXPROCS(1) 没关系,尝试下 runtime.GOMAXPROCS(大于 1),也一样会出现两两一组的日志

“一句话解释:因为是非抢占式调度。”
—— golang 好像是 1.2 版中开始引入比较初级的抢占式调度,然后好像是 1.14 做得更彻底,即使 for{} 也能释放调度权了

这个代码的 chan 虽然是无缓冲的,但只能保证 chan 的 send 和 recv 单句代码的原子性,记得那句话吗——不要通过共享内存来通信,而应该通过通信来共享内存。这句话主要是指用 chan 来保证内存的一致性,因为传统的方法用锁、锁在复杂的业务场景更烧脑并且一不小心容易死锁

用 chan 保证内存的一致性,进一步就可以做到一些业务逻辑的串行化,通常用 chan 也主要是用来做内存读写和逻辑的串行化从而保证一致性,但这并不是承诺多个协程同一个 chan 前后代码段的所有代码执行顺序

楼主代码中的实验是用 printf 打印,而 printf 本身就可能触发调度权的出让,所以其实现象不是 chan 直接导致的,而是由于 printf 时的出让顺序导致的

举个例子(对应代码注释中的 1 、2 、3 、4 ):

func count(r chan int, who int) {
for {
if who%2 == 0 {
r <- who // 1
fmt.Printf("|write <- who|%d\n", who) // 2
} else {
<-r // 3
fmt.Printf("| <-r recv|%d\n", who) // 4
}
}
}

1 执行后立刻执行了 2,打印了 write,出让
3 执行,4 执行前出让
又执行了一组 1 、2,再次打印了 write,出让
4 继续执行,打印了一个 recv,出让
1 执行,2 执行前出让调度
3 执行,4 执行,再次打印了一个 recv
...
依次类推,每次 printf 前都可能出让

顺便宣传下自己两个项目,欢迎来玩玩
https://v2ex.com/t/755862
2021-05-07 19:29:15 +08:00
回复了 jiangshanmeta 创建的主题 求职 刷了 1400 道力扣的开发,跪求广州前端岗位
做 web 前端有点可惜了
2021-05-02 11:24:26 +08:00
回复了 rv54ntjwfm3ug8 创建的主题 Go 编程语言 Go 如何根据请求头限制请求频率?
请求头只是个 key,所以请求头跟问题本身无关。只问怎么限流就行了

多节点就 redis 之类的:
https://github.com/go-redis/redis_rate

单节点就进程内,官方扩展包:
https://github.com/golang/time
或者其他第三方实现挺多的、自己手撸也不难
2021-04-07 12:05:20 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@zkdfbb 基于标准库的知名框架都比较稳,功能和周边也都差不多,按 star 就 gin,按喜好就看自己了
2021-04-07 11:40:20 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@makdon 63 楼回答的好
@Lpl 现在是互夸了

enjoy coding, have fun ~
2021-04-07 11:37:02 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@makdon
原来 63 楼已经回复过了,66 楼多余了,缘分
2021-04-07 11:34:51 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@Lpl 恩恩,这种简单功能都不会性能瓶颈,主要还要考虑设计上的复杂度,chan 不是万能灵药,毕竟加了一层 chan,同步逻辑变成了异步逻辑,换成 chan 的实现也不比 mutex 来得简洁,并且也不如用 mutex 容易理解

锁是很基础的设施,不要怕用它
2021-04-07 11:27:08 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@GTim 不用以后深入研究,就今天吧

不能怪 fiber,fiber 是基于 fasthttp 的,以前看过 fasthttp 相关介绍大概意思是 fasthttp 为了性能,很多地方 pool 复用内存,虽然我没有读 fasthttp 源码,只是大概分析,但是大致原因应该是差不多的:

应用层获取 http 各种参数时是复用的[]byte unsafe 的方式强转成 string,类似 c/c++的浅拷贝,新的 string 和原来的[]byte 类型结构体的数据指针指向同一段内存,而在本次 handler 调用结束后,这段[]byte 就被放回了 pool 并且以后有新的地方使用时又被拿出来
比如楼主的 key 加入到 map 时字面值是 "a",按照 "a" 的 hash index 存到对应的 map 的 bucket 里,而这个 string "a" 的结构体内部指向的内存被放回 pool,其他地方再次从 pool get 到时就可能被复用的地方修改,比如刚好其他请求的这个 key 复用了 "a" 的同一段内存但是这些请求的 key 为"b","b" 加到 map 里的时候是按照 "b" 的 hash index 存到对应的 bucket 里的、不同的 hash index 则不碰撞、不会跟原来的那个 string "a"(当前字面值也是"b")比较,所以就产生了多个 key

实在不喜欢 fasthttp
但是 fiber 的接口 /API 设计看着比 gin 舒服,还是挺喜欢的
但是生产项目,我还是不打算用 fasthttp 系的
2021-04-06 21:04:36 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@zkdfbb 最好可以给一份可以复现的代码+测试用例,大家可以复现下看看
2021-04-06 20:09:13 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@Lpl 淡定点,淡定点
1. 每个路由预先存到这个简单的统计里,代码会很漂亮?而且这点计数功能用 mutex 性能也不是瓶颈
2. 照本宣科这个词不是为了贬低,而是想告诉你不要听别人说好就什么都用什么,要懂得从实际出发
3. 先看懂我回复的内容
另外,chan 和 mutex 你可以自己先 benchmark 试试,chan 的源码在 runtime/chan.go 里,本身就带有锁的逻辑,并且跨越了协程,如果你觉得会比 mutex 性能好,那可以试试看
2021-04-06 20:01:51 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@kcojkcnw 对。32 位也一样,pointer 是字长,并且这种非结构体成员变量是对齐的,除了老奔腾还是哪个版本年代之前的,i32 也是原子的
2021-04-06 19:07:14 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@Orlion 对,这份代码里问题最大的就这句,accessLog 原来的内存被赋为新值的过程不原子,所以可能 panic 、还可能 Incr 的 Lock 是原来的 Mux 、Unlock 是新的 Mux 所以死锁

我还是建议我 25 楼说的那样都改成 accessLog = &Counter{data: make(map[string]int)}
2021-04-06 19:03:45 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@makdon 过奖了,3q
2021-04-06 18:26:00 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@Lpl
我上一楼的回答的 1 中,即使 map 的 value 使用对象,然后 atomic 操作对象的 value 也不适合,因为最早 map 为空,拿不到对应的 value,如果判断空先存入,那还是要加锁,除非你的 key 数量和 key 值都是固定的、创建 map 时就初始化了 value
2021-04-06 18:23:12 +08:00
回复了 zkdfbb 创建的主题 Go 编程语言 map 的一个神奇的问题
@Lpl

1. atomic 做的原子操作是比加锁快吧

先看 3 的回答吧,因为锁不可避免,所以在这里根本不能使用 atomic

并且因为是按 map 的 key 进行统计,首先你得取这个 map key 对应的 value,而直接取 map[key] 的地址是不行的,你试试编译下:
m := map[string]int32{"a": 0}
atomic.AddInt32(&m["a"], 1)
会报错: "cannot take the address of m["a"]"


2. 用管道通过求摸建立多个协程来消费

相对比较复杂的并且需要保证临界区一致性的并发逻辑可以考虑用 chan 替代锁来避免锁的复杂度尤其死锁等情况,但是简单功能,就比如这种计数器,使用 chan 实现起来会比锁更麻烦并且性能稍微损失一点点,完全没必要,要从实际出发、不能照本宣科

3. 目的是为了每一个 key 都能并发安全,加细粒度的锁不用去加对象锁,concurrentmap 不就是这样做的吗

首先要解决获得这个 key 的 value,获得这个 key 本身就需要对 map 的锁
标准库的 sync.Map 一样内置锁来实现、只是应用层不需要自己使用锁罢了
其他的三方 concurrentmap 实现也并不能实现每个 key 粒度,而是为了减少 key 数量巨大时并发流的竞争,所以在标准库 map 之上再加一层 hash buckets,再每个 buckets 对应的结构上用一个锁,go 标准库自己的 map 本身就是个 hash 下面多个 buckets,三方 concurrentmap 相当于再加一层 hash 分开成多个锁来降低粒度为 bucket 级别减少竞争
1 ... 39  40  41  42  43  44  45  46  47  48 ... 53  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   933 人在线   最高记录 6543   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 24ms · UTC 20:21 · PVG 04:21 · LAX 13:21 · JFK 16:21
Developed with CodeLauncher
♥ Do have faith in what you're doing.