最新要闻
- 焦点快报!116-104!山东高速力克北京首钢,山东试训外援首秀轻取19分
- 63岁男子向熊猫投掷苹果惹众怒 后果来了:被终身禁入
- 宝镇
- 天玑9200+性价王者预定!Redmi K60 Ultra第三方保护壳现身|全球新视野
- 支持正版:英国为采购Office刚花了71亿|天天快看
- 全球热资讯!7月新农合再涨40元,一市明确每人590元,今年你还交吗?
- 长城站起来!哈弗枭龙系列第1万台下线:6月热销6098台暴增97%
- 复兴号正式在青藏铁路投入运行:海拔3000米 零下40度也能跑
- 曼高德谈《夺宝奇兵5》
- 天天快讯:传奇设计大师去世:“我从不设计丑陋东西”
- 焦点精选!第一部8.0分不值一提!美国年度科幻大片续集来了
- 协鑫宣布年产36万吨储能正极材料项目今日投产-世界热讯
- 张雪峰老师推荐的十大高薪专业引发热议:计算机类霸榜 文科中仅法语入围 天天速读
- 丰田章男工资曝光:9.99亿日元丰田汽车史上最高! 环球微头条
- 100%纯棉、亲肤舒适:布静观纯棉短袖13元、长袖16元
- 环球视讯!多地高温难耐 黑龙江却遭遇特大冰雹:砸碎几乎整栋楼玻璃
手机
![光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯](http://www.jxyuging.com/uploadfile/2022/0610/20220610012348753.jpg)
光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯
![搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注](http://hnimg.zgyouth.cc/2022/0610/20220610115002743.jpg)
搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注
- 光庭信息跌4.57% 2021上市超募11亿2022扣非降74% 时快讯
- 搜狐汽车全球快讯 | 大众汽车最新专利曝光:仪表支持拆卸 可用手机、平板替代-环球关注
- 视点!美国首位女总统即将诞生?拜登恐怕要提前下岗,美政坛迎来变局?
- 当前速递!用理想仪器实现更好的颗粒 德国新帕泰克亮相CPHI & PMEC China获好评
- 微粒贷怎么申请开通 开通方法如下
- 焦点简讯:心疼!这位40岁的云南缉毒警,已是满头白发
家电
【聚看点】go src - sync.Map
前言
在并发编程中,我们经常会遇到多个goroutine同时操作一个map的情况。如果在这种情况下直接使用普通的map,那么就可能会引发竞态条件,造成数据不一致或者更严重的问题。
(资料图片)
sync.Map
是Go语言中内置的一种并发安全的map,但是他的实现和用法与普通的map完全不同,这篇文章将详细介绍这些区别。
一、使用方法
创建sync.Map
非常简单,只需要声明即可:
var m sync.Map
使用Store
方法存储键值对:
m.Store("hello", "world")
使用Load
方法获取值:
value, ok := m.Load("hello")if ok { fmt.Println(value) // 输出:world}
使用Delete
方法删除键值对:
m.Delete("hello")
二、原理
sync.Map
的核心实现依赖于两个主要的数据结构:一个只读的read
字段,以及一个可写的dirty
字段。
读操作:
当我们进行读取操作(Load
)时,首先会尝试从read
字段读取数据,这个过程是完全无锁的。
如果read
中没有找到,那么会尝试加锁后从dirty
中读取。这个设计保证了在大部分读多写少的场景下,读操作是无锁的,大大提升了性能。
写操作:
写入操作(key不存在
)时,会直接在dirty
中进行写入,并将read
的amended
标记为true
,表示read
字段有待更新的数据。
然后再有新的读取操作到来时,如果amended
为true并且miss数量超过dirty长度
,则会从dirty
中拷贝数据到read
,并清除amended
标记。
总结:
在这个设计中,读操作在大部分情况下是无锁的,而写操作(key不存在时)则需要获取dirty
的锁,从而实现了对于读多写少场景的优化。
三、优点
sync.Map
在以下两种情况下表现得特别好:
- 键值对的数量相对稳定,读操作明显多于写操作的场景
- 多个goroutine并发读取不重叠的键集的场景
这是因为sync.Map
的设计将读取操作优化至极致,同时尽量减少在写入新键值对时的锁竞争。
四、缺点
然而,sync.Map
并不是银弹,也有一些局限:
sync.Map
没有像普通map那样的直观语法,必须使用特定的方法来操作键值对- 对于键值对数量快速增长、写操作频繁的场景,
sync.Map
的性能可能不如使用普通map加锁的方式 - 读操作无锁情况下,可能会出现时间竞态问题
五、实现
sync.Map
type Map struct {mu Mutex// read contains the portion of the map"s contents that are safe for// concurrent access (with or without mu held).read atomic.Pointer[readOnly]// dirty contains the portion of the map"s contents that require mu to be// held. To ensure that the dirty map can be promoted to the read map quickly,// it also includes all of the non-expunged entries in the read map.dirty map[any]*entry// misses counts the number of loads since the read map was last updated that// needed to lock mu to determine whether the key was present.misses int}
readonly
// readOnly is an immutable struct stored atomically in the Map.read field.type readOnly struct {m map[any]*entryamended bool // true if the dirty map contains some key not in m.}
entry
// An entry is a slot in the map corresponding to a particular key.type entry struct {// p points to the interface{} value stored for the entry.p atomic.Pointer[any]}
expunged
// expunged is an arbitrary pointer that marks entries which have been deleted// from the dirty map.var expunged = new(any)
状态机:
总结:
- 当key从read中删除时,会先被标记为nil,不会立马删除key
- 当重新初始化dirty时(将read.m克隆到dirty),如果key的值为nil,会设置为expunged,并不在dirty中创建这个key。
- 如果key为expunged,LoadOrStore/Swap/CompareAndDelete/CompareAndSwap都会不执行写操作并返回false。
Load
// Load returns the value stored in the map for a key, or nil if no// value is present.// The ok result indicates whether value was found in the map.func (m *Map) Load(key any) (value any, ok bool) {read := m.loadReadOnly()e, ok := read.m[key]if !ok && read.amended {m.mu.Lock()// Avoid reporting a spurious miss if m.dirty got promoted while we were// blocked on m.mu. (If further loads of the same key will not miss, it"s// not worth copying the dirty map for this key.)read = m.loadReadOnly()e, ok = read.m[key]if !ok && read.amended {e, ok = m.dirty[key]// Regardless of whether the entry was present, record a miss: this key// will take the slow path until the dirty map is promoted to the read// map.m.missLocked()}m.mu.Unlock()}if !ok {return nil, false}return e.load()}func (m *Map) loadReadOnly() readOnly {if p := m.read.Load(); p != nil {return *p}return readOnly{}}
总结:
- 如果查询的key在read中找到了,返回entry.load()
- 如果查询的key在read中未找到,并且read和dirty一致,返回nil, false
- key未找到,并且read与dirty不一致
- 加锁
- 重新查询read,类似上面1、2流程,如果未找到并且read和dirty不一致则继续
- 在dirty中查询
- misses加一
- 如果misses数大于dirty长度,将dirty同步到read,重置dirty和misses
- 释放锁
- 返回结果
LoadOrStore
// LoadOrStore returns the existing value for the key if present.// Otherwise, it stores and returns the given value.// The loaded result is true if the value was loaded, false if stored.func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {// Avoid locking if it"s a clean hit.read := m.loadReadOnly()if e, ok := read.m[key]; ok {actual, loaded, ok := e.tryLoadOrStore(value)if ok {return actual, loaded}}m.mu.Lock()read = m.loadReadOnly()if e, ok := read.m[key]; ok {if e.unexpungeLocked() {m.dirty[key] = e}actual, loaded, _ = e.tryLoadOrStore(value)} else if e, ok := m.dirty[key]; ok {actual, loaded, _ = e.tryLoadOrStore(value)m.missLocked()} else {if !read.amended {// We"re adding the first new key to the dirty map.// Make sure it is allocated and mark the read-only map as incomplete.m.dirtyLocked()m.read.Store(&readOnly{m: read.m, amended: true})}m.dirty[key] = newEntry(value)actual, loaded = value, false}m.mu.Unlock()return actual, loaded}// tryLoadOrStore atomically loads or stores a value if the entry is not// expunged.//// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and// returns with ok==false.func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {p := e.p.Load()if p == expunged {return nil, false, false}if p != nil {return *p, true, true}// Copy the interface after the first load to make this method more amenable// to escape analysis: if we hit the "load" path or the entry is expunged, we// shouldn"t bother heap-allocating.ic := ifor {if e.p.CompareAndSwap(nil, &ic) {return i, false, true}p = e.p.Load()if p == expunged {return nil, false, false}if p != nil {return *p, true, true}}}
总结:
- 如果key在read中找到了,并且不为expunged
- 如果为nil,则CAS新的值,并返回value, false
- 如果不为nil,则返回*p, true
- 如果key在read中不存在,或者为expunged
- 加锁
- 再次在read中查找,如果找到了
- 如果为expunged,结果为nil, false
- 如果为nil,则CAS新的值,结果为value, false
- 如果不为nil,结果为*p, true
- 如果在dirty中找到了,重复2的逻辑判断
- 在read和dirty中都没有,则创建一个新的entry
- 释放锁
- 返回结果
Delete/LoadAndDelete
// Delete deletes the value for a key.func (m *Map) Delete(key any) {m.LoadAndDelete(key)}
// LoadAndDelete deletes the value for a key, returning the previous value if any.// The loaded result reports whether the key was present.func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {read := m.loadReadOnly()e, ok := read.m[key]if !ok && read.amended {m.mu.Lock()read = m.loadReadOnly()e, ok = read.m[key]if !ok && read.amended {e, ok = m.dirty[key]delete(m.dirty, key)// Regardless of whether the entry was present, record a miss: this key// will take the slow path until the dirty map is promoted to the read// map.m.missLocked()}m.mu.Unlock()}if ok {return e.delete()}return nil, false}
总结:
- 如果要删除的key在read中存在,将它置为nil
- 如果要删除的key在read中未找到,并且read和dirty一致,说明key不存在,返回nil, false
- key未找到,并且read和dirty不一致
- 加锁
- 重新查询read,类似上面1、2流程,如果key未找到,并且read和dirty不一致继续
- 在dirty中查询并删除
- misses加一
- 如果misses数大于dirty长度,将dirty同步到read,重置dirty和misses
- 释放锁
- 如果key在dirty中也不存在,返回nil, false;反之,将它置为nil
Store/Swap
// Store sets the value for a key.func (m *Map) Store(key, value any) {_, _ = m.Swap(key, value)}// Swap swaps the value for a key and returns the previous value if any.// The loaded result reports whether the key was present.func (m *Map) Swap(key, value any) (previous any, loaded bool) {read := m.loadReadOnly()if e, ok := read.m[key]; ok {if v, ok := e.trySwap(&value); ok {if v == nil {return nil, false}return *v, true}}m.mu.Lock()read = m.loadReadOnly()if e, ok := read.m[key]; ok {if e.unexpungeLocked() {// The entry was previously expunged, which implies that there is a// non-nil dirty map and this entry is not in it.m.dirty[key] = e}if v := e.swapLocked(&value); v != nil {loaded = trueprevious = *v}} else if e, ok := m.dirty[key]; ok {if v := e.swapLocked(&value); v != nil {loaded = trueprevious = *v}} else {if !read.amended {// We"re adding the first new key to the dirty map.// Make sure it is allocated and mark the read-only map as incomplete.m.dirtyLocked()m.read.Store(&readOnly{m: read.m, amended: true})}m.dirty[key] = newEntry(value)}m.mu.Unlock()return previous, loaded}// trySwap swaps a value if the entry has not been expunged.//// If the entry is expunged, trySwap returns false and leaves the entry// unchanged.func (e *entry) trySwap(i *any) (*any, bool) {for {p := e.p.Load()if p == expunged {return nil, false}if e.p.CompareAndSwap(p, i) {return p, true}}}
总结:
- 如果key在read中找到了,并且不为expunged,则试图CAS并返回结果
- key在read中未找到,或者为expunged
- 加锁
- 在read中查询
- 如果查到了,试图unexpunge
- 如果需要unexpunge,会将entry置(CAS)为nil,并在dirty中插入key
- 执行entry的Swap
- 如果查到了,试图unexpunge
- 在ditry中查询
- 如果查到了,执行entry的Swap
- 都没查到,则检查dirty是否存在,初始化dirty,并在dirty增加新的entry
- 释放锁
- 返回结果
CompareAndSwap
// CompareAndSwap swaps the old and new values for key// if the value stored in the map is equal to old.// The old value must be of a comparable type.func (m *Map) CompareAndSwap(key, old, new any) bool {read := m.loadReadOnly()if e, ok := read.m[key]; ok {return e.tryCompareAndSwap(old, new)} else if !read.amended {return false // No existing value for key.}m.mu.Lock()defer m.mu.Unlock()read = m.loadReadOnly()swapped := falseif e, ok := read.m[key]; ok {swapped = e.tryCompareAndSwap(old, new)} else if e, ok := m.dirty[key]; ok {swapped = e.tryCompareAndSwap(old, new)// We needed to lock mu in order to load the entry for key,// and the operation didn"t change the set of keys in the map// (so it would be made more efficient by promoting the dirty// map to read-only).// Count it as a miss so that we will eventually switch to the// more efficient steady state.m.missLocked()}return swapped}// tryCompareAndSwap compare the entry with the given old value and swaps// it with a new value if the entry is equal to the old value, and the entry// has not been expunged.//// If the entry is expunged, tryCompareAndSwap returns false and leaves// the entry unchanged.func (e *entry) tryCompareAndSwap(old, new any) bool {p := e.p.Load()if p == nil || p == expunged || *p != old {return false}// Copy the interface after the first load to make this method more amenable// to escape analysis: if the comparison fails from the start, we shouldn"t// bother heap-allocating an interface value to store.nc := newfor {if e.p.CompareAndSwap(p, &nc) {return true}p = e.p.Load()if p == nil || p == expunged || *p != old {return false}}}
总结:
- 如果key在read中找到了
- 如果为nil/expunged/不等于old,则返回false
- 试图进行entry的CAS,成功返回true,失败继续1、2流程
- 如果key在read中未找到,并且read与dirty没有不一致,返回false
- 如果key在read中未找到,并且read与dirty不一致
- 加锁
- 如果key在read中找到
- 如果仍然为nil/expunged/不等于old,结果为false
- 试图进行entry的CAS,CAS成功,则结果为true;否则继续1、2流程
- 如果key在dirty中找到
- 如果不等于old,结果为false
- 试图进行entry的CAS,CAS成功,则结果为true;否则继续1、2流程
- misses加一
- 如果misses数大于dirty长度,将dirty同步到read,重置dirty和misses
- 释放锁
- 返回结果
CompareAndDelete
// CompareAndDelete deletes the entry for key if its value is equal to old.// The old value must be of a comparable type.//// If there is no current value for key in the map, CompareAndDelete// returns false (even if the old value is the nil interface value).func (m *Map) CompareAndDelete(key, old any) (deleted bool) {read := m.loadReadOnly()e, ok := read.m[key]if !ok && read.amended {m.mu.Lock()read = m.loadReadOnly()e, ok = read.m[key]if !ok && read.amended {e, ok = m.dirty[key]// Don"t delete key from m.dirty: we still need to do the “compare” part// of the operation. The entry will eventually be expunged when the// dirty map is promoted to the read map.//// Regardless of whether the entry was present, record a miss: this key// will take the slow path until the dirty map is promoted to the read// map.m.missLocked()}m.mu.Unlock()}for ok {p := e.p.Load()if p == nil || p == expunged || *p != old {return false}if e.p.CompareAndSwap(p, nil) {return true}}return false}
总结:
- 如果key在read中找到了
- 如果为nil/expunged/不等于old,则返回false
- 试图进行entry的CAS,成功返回true,失败继续1、2流程
- 如果key在read中未找到,并且read和dirty没有不一致,返回false
- 如果key在read中未找到,并且read和dirty不一致
- 加锁
- 从read中查找
- 如果找到key
- 为nil/expunged/不等于old,结果为false
- 试图进行CAS,成功结果为true,失败继续尝试
- 如果没找到,并且没有不一致,结果为false
- 如果找到key
- 如果read和dirty有不一致
- 从dirty中查找
- 如果找到key
- 不等于old,结果为false
- 试图CAS,成功结果为true,失败继续尝试
- 没找到key,结果为false
- 释放锁
- 返回结果
结论
总的来说,sync.Map
是Go标准库提供的一个非常有用的工具,它可以帮助我们简化并发编程,并且在一些特定的场景下能提供良好的性能。
但在使用的时候,我们需要根据具体的应用场景和需求来选择使用sync.Map
还是其他的并发原语。
关键词:
-
-
-
-
【聚看点】go src - sync.Map
环球观天下!Web安全-渗透测试-权限提升01
焦点快报!116-104!山东高速力克北京首钢,山东试训外援首秀轻取19分
63岁男子向熊猫投掷苹果惹众怒 后果来了:被终身禁入
宝镇
天玑9200+性价王者预定!Redmi K60 Ultra第三方保护壳现身|全球新视野
支持正版:英国为采购Office刚花了71亿|天天快看
全球热资讯!7月新农合再涨40元,一市明确每人590元,今年你还交吗?
长城站起来!哈弗枭龙系列第1万台下线:6月热销6098台暴增97%
复兴号正式在青藏铁路投入运行:海拔3000米 零下40度也能跑
曼高德谈《夺宝奇兵5》
天天快讯:传奇设计大师去世:“我从不设计丑陋东西”
焦点精选!第一部8.0分不值一提!美国年度科幻大片续集来了
协鑫宣布年产36万吨储能正极材料项目今日投产-世界热讯
张雪峰老师推荐的十大高薪专业引发热议:计算机类霸榜 文科中仅法语入围 天天速读
丰田章男工资曝光:9.99亿日元丰田汽车史上最高! 环球微头条
RPM安装的Oracle19c 修改init.ora进行修复以及最简单开机启动Oracle的方法 世界百事通
markdown终极指南 观点
100%纯棉、亲肤舒适:布静观纯棉短袖13元、长袖16元
环球视讯!多地高温难耐 黑龙江却遭遇特大冰雹:砸碎几乎整栋楼玻璃
环球新消息丨安卓性能标杆!一加Ace 2 Pro来了:5200mAh+骁龙8 Gen2
米体:汉达可能降薪续约留在国米,下赛季担任二号门将 全球微动态
mosquitto的基本参数使用
全球热议:探究直播app源码技术:视频上传功能
当前资讯!02修剪标准&&方法
环球滚动:ThinkPHP6.0 链式SQL语句
【世界速看料】见证历史!苹果成全球首家市值3万亿美元公司 意味着什么?
男生旅行3年收集50余个城市水土:覆盖30个省份 比打卡拍照更有意义
云南男子外出干活遇超大菌子 网友羡慕:菌之大一锅炖不下_当前聚焦
网友吐槽一条街三四十个井盖 官方回应:雨污分流、将铺上沥青
当前热议!五年中考三年模拟八上物理答案
全球观热点:MinIO-对象存储简单使用
JS必学的11个工具方法(避免重复造轮子) 世界快看
IDA的使用2_全球新视野
荷兰DUV光刻机一律管制出口?一文看懂|环球快播
埃安6月销量45013辆创历史新高 今年已卖出比亚迪一个月销量
特斯拉加入价格乱战!买这两款车最高优惠4.5万元
国内高端手机市场除了苹果iPhone 还能有谁?! 焦点要闻
2023年全国铁路暑期运输启动:满血复活 暴涨超70%
焦点热议:使用Gitee或GitHub托管Maven仓库JAR包的便捷方法
灭霸演员是谁_ 芝士回答
世界短讯!心爱的小乌龟死了:主人把它做成《七龙珠》龟仙人手办!
担心的事情发生了!泰国女游客卷入电动步道腿被夹断
火车站保洁阿姨不慎弄脏乘客衣服跪地道歉 公司回应:已和解 世界观焦点
蔚来智能系统Banyan 2.0正式发布:超120项功能 号称全场景领先
天天快播:一键打开动态日历 锁定2023下半年这些大事!
本田飞度彻底无语!新款马自达昂克赛拉官降3万:8.99万起_今头条
小米下一代旗舰还用1英寸主摄!1.33英寸还得等|快报
天天头条:今天起全国汽车实施国6排放标准6b阶段!对老车年检有何影响?官方解答
Web安全-渗透测试-waf绕过02
每日快讯!微服务设计:集成
vue中封装服务器地址/接口与设置请求头
天津关于调整2023年住房公积金缴存额的通知政策解读
环球讯息:你敢吃吗?知了肉初上市400元一斤:号称高蛋白
不用求助“拍瓜师”!西瓜甜不甜:看这里就知道
QQ空间《抢车位》游戏改名了 还把特斯拉Model X名字拼错_每日快看
中国包揽全球液晶电视面板TOP5 牢牢掌控话语权 日韩份额仅剩1成-全球热闻
男子围观火灾拍视频结果着火的是自家 还讨论是谁家倒霉:网友直呼尴尬 当前看点
关于Linux-Kernel-Live-patching-的效果演示-kpatch auto-配置-今日聚焦
世界视讯!win10投影仪连接电脑后画面不显示怎么办
无惧高通华为压制!苹果5G也站起来了:跟诺基亚签许可_天天实时
焦点日报:中国豪华车市场格局改写!理想汽车6月交付32575辆 首次突破三万辆:要超过BBA
五菱再出神车!缤果6月爆卖1.9万台:上市三月销量已超6万
关于lvm磁盘管理-单个磁盘分区PV的扩容 全球聚看点
一天吃透Redis面试八股文
如何使用libavfilter库给输入文件input.yuv添加视频滤镜?
Linux主流架构运维工作简单剖析 今日热搜
视讯!年货再出新花样 透视玩家现原形
14岁女孩500元卖头发被商贩剪坏 网友吐槽太坑:应先通女孩父母 每日动态
努比亚官宣新旗舰:镜头超越一英寸 7月见|全球聚看点
天天视点!别喝工业勾兑啤酒了:熊猫精酿12°P啤酒2.6元新低 好喝不上头
环球快消息!随笔[七律]
【见·闻】巴西精品咖啡市场从业者非常看好中国市场发展前景-百事通
撤销日本福岛核污染地区食品进口限制?欧盟回应很合理 美国早撤销 世界通讯
即时看!交付12万台电池零起火!极氪汽车6月交付10620辆 极氪001蝉联30万元以上纯电车型销冠
大额券手慢无:361°板鞋/运动鞋/休闲鞋74元抄底(多款式)
联合电子X-Pin电机批量生产 X-Pin绕组技术比对解读
燃烧烈爱无删减在线_燃烧烈爱-全球热推荐
当前要闻:电话号码过户要预存1万元话费?中国移动回应来了
我国CR450动车组研制取得阶段性成果:时速453km性能指标良好-焦点讯息
观天下!大猩猩性别歧视 英国小报遭头版制裁
2023年6月随笔暨半年总结 世界今亮点
cad如何调比例尺寸_cad比例怎么调整原图纸比例 环球热点
复读14年的高考钉子户认清事实:考了594分没能力上清华 没浪费教育资源
泰坦号潜艇失事前水下短信通信记录曝光:报警前8小时就已出现问题
简讯:如何做屋顶的防水层呢(如何做屋顶的防水层)
ASP.NET Core Web API之Token验证 天天快看
讯息:# 02. 数据分组整合之unique+groupby
天天快播:IDApython的学习
胖手指戴哪种戒指图片_胖手指
环球微动态丨山东大学拟聘用2名硕士为寓管:一人毕业于哈工大 另一人是海归
跑单王1年送25000单外卖:从外卖小白到外卖王者只用了一个月时间
Intel 13代酷睿最低端U300 CPU首次现身:1个大核、4个小核|速讯
当前看点!因为四个字 Kindle彻底告别中国!说真的 我有点难过
AMD锐龙5 5600X3D处理器确认:美国独享
起亚k2怎么样专家点评(起亚k2怎么样)-热消息
大话墨香online(大话墨香)
决算是什么意思通俗易懂_决算是什么意思_全球观点
特斯拉CEO马斯克被曝患有抑郁症 其服用氯胺酮以此“治疗”
演员马丽迎来了自己41岁生日 晒出美丽照片为自己庆生