context.Context

news2024/11/15 11:21:22

context.Context

  • 前言
  • 一、为什么要context
  • 二、context有什么用
    • 三、基本数据结构
      • 3.1、context包的整体工作机制
      • 3.2 基本接口和结构体
      • 3.3 API函数
      • 3.4 辅助函数
      • 3.5 context用法
      • 3.6 使用 context 传递数据的争议
  • 总结
  • 参考资料

前言

context是go语言的一个并发包,一个标准库,用于goroutine之间的退出通知。

一、为什么要context

Go中的goroutine之间没有父与子的关系,也就没有所谓子进程退出后的通知机制,多个goroutine被并行的调度,多个goroutine如何协作?

  • 利用select收敛 + 在输入端在绑定一个非业务chan。

这样处理某个goroutine是一个简单的方案,但这不是一个通用的解决方案,当并发结构非常复杂时,该方案就显得力不从心了。

实际编程中 goroutine 会拉起新的 goroutine ,新的 goroutine 又会拉起另一个新的 goroutine ,最终形成一个树状的结构,由于 goroutine 里并没有父子的概念, 这个树状的结构只是在程序员头脑中抽象出来的 ,程序的执行模型并没有维护这么一个树状结构 。怎么通知这个树状上的所有 goroutine 退出?仅依靠语法层面的支持显然比较难处理。

  • 为此 Go 1.7 提供了一个标准库 context 来解决这个问题。它提供两种功能: 退出通知元数据传递。 context库 的设计目的就是跟踪 goroutine 调用 , 在其内部维护一个调用树, 井在这些调用树中传递通知和元数据。

二、context有什么用

context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。两个目的 :

  1. 退出通知机制一一通知可以传递给整个 goroutine 调用树上的每一个 goroutine 。
  2. 传递数据一一数据可 以传递给整个 goroutine 调用树上的每一个 goroutine。

三、基本数据结构

3.1、context包的整体工作机制

第一个创建 Context 的 goroutine被称为 root 节点。 root 节点负责创建一个实现 Context 接口的具体对象, 并将该对象作为参数传递到其新拉起的 goroutine , 下游的 goroutine 可 以继续封装该对象,再传递到更下游的 goroutine 。Context 对象在传递的过程中最终形成一个树状的数据结构,这样通过位于 root 节点(树的根节点) 的 Context 对象就能遍历整个 Context 对象树 , 通知和消息就可以通过 root 节点传递出去 ,实现了上游 goroutine 对下游 goroutine 的消息传递。

3.2 基本接口和结构体

  1. Context 接口,Context 是一个基本接 口 , 所有的 Context 对象都要实现该接 口, context 的使用者在调用接口中都使用 Context 作为参数类型 。
type Context interface {
	//如果 Context 实现了起时控制,则该方法返回 ok true, deadline 为超时时间,否则 ok 为 false
	Deadline() (deadline time.Time, ok bool)
	// 后端被调的 goroutine 应该监听该方法返回的 chan ,以便及时释放资源
	Done() <-chan struct{}
	//Done 返回的 ch an 收到通知的时候,才可以访问 Err ()获知因为什么原因被取消
	Err() error
	// 可以 访问上游 goroutine 传递给下游 goroutine 的位
	Value(key interface{}) interface{}
}
  1. canceler 接口,canceler 接口是一个扩展接口,规定了取消通知的 Context 具体类型需要实现的接口 。
    context 包中的具体类型*cancelCtx 和*timerCtx 都实现了该接口。
// 一个 context 对象如采实现了 canceler 接口,则可以被取消
type canceler interface {
	// 创建 cancel 接 口实例的 g o routine 调 用 cancel 方法通知后续创建的 gorou tine 退出
	cancel(removeFromParent bool, err error)
	// Done 方法返回的 chan 需妥后端 goroutine 来监听 , 并及时退出
	Done() <-chan struct{}
}
  1. empty Context结构体,emptyCtx 实现了 Context 接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为 Context 对象树的根( root 节点)。因为 context 包的使用思路就是不停地调用context 包提供的包装函数来创建具有特殊功能的 Context 实例 ,每一个 Context 实例的创建都以上一个 Context 对象为参数, 最终形成一个树状的结构。
type emptyCtx int
// 实现Context所有方法,就实现了Context
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

func (e *emptyCtx) String() string {
	switch e {
	case background:
		return "context.Background"
	case todo:
		return "context.TODO"
	}
	return "unknown empty Context"
}

package 定义了两个全局变量和两个封装函数,返回两个 emptyCtx 实例对象,实际使用时通过调用这两个封装函数来构造 Context 的 root 节点。

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}
  1. cancelCtx结构体,cancelCtx 是一个实现了 Context 接口的具体类型,同时实现了 conceler 接口。 conceler 具有退出通知方法。注意退出通知机制不但能通知自己,也能逐层通知其 children 节点。
// cancelCtx 可以被取消, cancelCtx 取消时会同时取消所有实现 canceler 接口的孩子节点
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}

func (c *cancelCtx) Value(key interface{}) interface{} {
	if key == &cancelCtxKey {
		return c
	}
	return c.Context.Value(key)
}

func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

type stringer interface {
	String() string
}

func contextName(c Context) string {
	if s, ok := c.(stringer); ok {
		return s.String()
	}
	return reflectlite.TypeOf(c).String()
}

func (c *cancelCtx) String() string {
	return contextName(c.Context) + ".WithCancel"
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d) // 显示通知自己
	}
	// 通知孩子
	// 循环调用 children 的 cancel 函数,由于 parent 已经取消,所以此时 child 调用cancel 传入的是 false
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}
  1. timeCtx,timerCtx 是一个实现了 Context 接口 的具体类型 ,内部封装了 cancelCtx 类型实例 ,同时有一个 deadline 变量,用来实现定时退出通知。
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

func (c *timerCtx) String() string {
	return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
		c.deadline.String() + " [" +
		time.Until(c.deadline).String() + "])"
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}
  1. valueCtx,valueCtx 是一个实现了 Context 接口的具体类型,内部封装了 Context 接口类型,同时封装了一个 k/v 的存储变量。 valueCtx 可用来传递通知信息。
type valueCtx struct {
	Context
	key, val interface{}
}

// stringify tries a bit to stringify v, without using fmt, since we don't
// want context depending on the unicode tables. This is only used by
// *valueCtx.String().
func stringify(v interface{}) string {
	switch s := v.(type) {
	case stringer:
		return s.String()
	case string:
		return s
	}
	return "<not Stringer>"
}

func (c *valueCtx) String() string {
	return contextName(c.Context) + ".WithValue(type " +
		reflectlite.TypeOf(c.key).String() +
		", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

3.3 API函数

  1. 构造root节点对象,用于后序With包装函数的实参。
func Background() Context 
func TODO() Context 
  1. With包装函数
// 带退出通知的Context
func WithCancel (parent Context) (ctx Context , cancel CancelFunc)
// 带超时通知的Context
func WithDeadline (parent Context , deadline time.Time ) (Context , CancelFunc)
func WithTimeout (parent Context , timeout time.Duration) (Context , 
CancelFunc )
// 带传递数据的Context
func WithValue(parent Context , key , val interface{}) Context

这些函数都有一个共同的特点-- parent 参数,其实这就是实现 Context 通知树的必备条件。在 goroutine 的调用链中, Context 的实例被逐层地包装并传递,每层又可以对传进来的 Context实例再封装自己所需的功能 ,整个调用树需要一个数据结构来维护,这个维护逻辑在这些包装函数内部实现。

3.4 辅助函数

前面描述的 With 开头的构造函数是给外部程序使用的 API 接口函数。 Context 具体对象的链条关系是在 With 函数的内 部维护的。现在分析一下 With 函数内部使用的通用函数。
func propagateCancel(parent Context , child canceler )有如下几个功能:

  1. 判断 parent 的方法 Done() 返回值是否是 nil ,如果是,则说明 parent 不是一个可取消的 Context 对象,也就无所谓取消构造树,说明 child 就是取消构造树的根。
  2. 如果 parent 的方法 Done() 返回值不是 nil ,则向上回溯自己的祖先是否是 cancerCtx 类型实例,如果是,则将 child 的子节点注册维护到那棵关系树里面。
  3. 如果向上回溯自己的祖先都不是 cancelCtx 类型实例,则说明整个链条的取消树是不连续的。此时只需监听 parent 和自己的取消信号即可。
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

func parentCancelCtx(parent Context )(*cancelCtx , bool ) : 判断 parent 中 是否封装*cancelCtx 的字段,或者接口里面存放的底层类型是否是*cancelCtx 类型。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	done := parent.Done()
	if done == closedchan || done == nil {
		return nil, false
	}
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	return p, true
}

func removeChild(parent Context , child canceler ) : 如果 parent 封装*cancelCtx类型字段,或者接口里面存放的底层类型是*cancelCtx 类型 ,则将其构造树上的 child 节点删除。

func removeChild(parent Context, child canceler) {
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}

3.5 context用法

package main

import (
	"context"
	"fmt"
	"time"
)

// context 基本用法

// 自定义context类型
type otherContext struct {
	context.Context
}

// 主函数
func main() {
	// 利用withCancel函数构建可取消的context
	ctxA, cancel := context.WithCancel(context.Background())
	// work 模拟运行,并检测前端的推出通知
	go work(ctxA, "work1")
	// 利用withDeadline函数构建超时通知的context
	ctxB, _ := context.WithDeadline(ctxA, time.Now().Add(3*time.Second))
	// work 模拟运行
	go work(ctxB, "work2")
	// 利用withValue函数构建可传递数据的context
	oc := otherContext{ctxB}
	go work(oc, "work3")
	ctxC := context.WithValue(oc, "key", "andes pass from main")
	// workWithValue 模拟运行
	go workWithValue(ctxC, "work3")
	// 故意停顿10s,让work2 和 work3 超时退出
	time.Sleep(time.Second * 10)
	// 让work1 cancel
	cancel()
	// 停顿5秒,让work1打印信息
	time.Sleep(time.Second * 5)
	fmt.Println("main stop")
}

// 获取Context传递的数据,等待前端的退出通知。
func workWithValue(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			val := ctx.Value("key").(string)
			fmt.Printf("%s is running and value = %s\n", name, val)
			time.Sleep(time.Second * 100)
		}
	}
}

// 检测通知 & do something
func work(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			fmt.Printf("%s is running\n", name)
			time.Sleep(4 * time.Second)
		}
	}
}

在这里插入图片描述

3.6 使用 context 传递数据的争议

  1. 该不该使用 context 传递数据
    首先要清楚使用 context 包主要是解决 goroutine 的通知退出,传递数据是其一个额外功能。可以使用它传递一些元信息 ,总之使用 context 传递的信息不能影响正常的业务流程,程序不要期待在 context 中传递一些必需的参数等,没有这些参数,程序也应该能正常工作。
  2. 在 context 中传递数据的坏处
  • 传递的都是 interface{} 类型的值,编译器不能进行严格的类型校验。
  • 从 interface{} 到具体类型需要使用类型断言和接口查闹,有一定的运行期开销和性能损失。
  • 值在传递过程中有可能被后续的服务覆盖,且不易被发现。
  • 传递信息不简明,较晦涩;不能通过代码或文档一眼看到传递的是什么,不利于后续维护。
  1. context 应该传递什么数据
  • 日志信息。
  • 调试信息
  • 不影响业务主逻辑的可选数据。
    context 包提供的核心的功能是多个 goroutine 之间的退出通知机制,传递数据只是一个辅助功能,应谨慎使用 context 传递数据。

总结

1)context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。

参考资料

[1] Go 语言核心编程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/376559.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

平台总线开发(id和设备树匹配)

目录 一、ID匹配之框架代码 二、ID匹配之led驱动​​​​​​​ 三、设备树匹配 四、设备树匹配之led驱动 五、一个编写驱动用的宏 一、ID匹配之框架代码 id匹配&#xff08;可想象成八字匹配&#xff09;&#xff1a;一个驱动可以对应多个设备 ------优先级次低 注意事项…

河南农信社数字化转型实践方案

农信机构立足地方“三农”&#xff0c;普遍规模较小&#xff0c;高地域集中度在县域和农村地区&#xff0c;容易受到地方农村经济脆弱性的影响。 农信机构由于信贷项目要求多&#xff0c;单笔贷款业务批复的额度相对较小&#xff0c;在信用档案登记环节&#xff0c;造成业务量…

SQL Server主流版本生命周期管理

SQL Server 生命周期 每个版本的 SQL Server 都有至少 10 年的支持期限&#xff0c;其中包括五年的主要支持和五年的扩展支持&#xff1a; “主要支持” 包括功能、性能、可伸缩性和安全更新。“扩展支持” 仅包含安全更新。 “终止支持” &#xff08;有时也称为生命周期结束…

udp套接字编程(超详细带你逐步实现)

我自己在学习UDP服务器的时候&#xff0c;有着太多的不解&#xff0c;我不明白一个udp服务器是如何设计出来的。我在网上找了很多的资料&#xff0c;不过绝大多数都是把代码往哪里一放&#xff0c;具体的设计流程完全不提&#xff0c;这让我看了之后一头雾水。或许对于刚刚开始…

scikit-learn实现近邻算法分类的示例

scikit-learn库 scikit-learn已经封装好很多数据挖掘的算法 现介绍数据挖掘框架的搭建方法 转换器&#xff08;Transformer&#xff09;用于数据预处理&#xff0c;数据转换流水线&#xff08;Pipeline&#xff09;组合数据挖掘流程&#xff0c;方便再次使用&#xff08;封装…

SVN配置使用(钩子配置、updata忽略指定文件)

参考链接&#xff1a; svn(svnsync)实时同步备份及问题解答 SVN常用命令之update SVN钩子就是一个脚本&#xff0c;在SVN更新前、后、或者变化前后等等状态&#xff0c;触发的脚本。据此可以有多种用途&#xff0c;如&#xff1a;1、在版本提交前要求必须对更新进行说明&#…

20分钟6个示例4个动图教你学会Async Hooks

序幕 async_hooks模块提供了一个全新的功能世界,但作为 Node.js 爱好者,我最感兴趣的是,它可以让您轻松了解我们在应用程序中经常执行的一些任务的幕后情况。 在本文中,我将尝试借助async_hooks模块来演示和解释一个典型的异步资源的生命周期。 Async Hooks API 简介 as…

【源码解析】SpringBoot自动装配的实现原理

什么是SpringBoot的自动装配 SpringBoot在启动的时候会扫描外部jar包中的META-INF/spring.factories文件&#xff0c;将文件中配置的类信息按照条件装配到Spring容器中。 实现原理 核心注解SpringBootApplication Target({ElementType.TYPE}) Retention(RetentionPolicy.R…

时间序列分析 | BiLSTM双向长短期记忆神经网络时间序列预测(Matlab完整程序)

时间序列分析 | BiLSTM双向长短期记忆神经网络时间序列预测(Matlab完整程序) 目录 时间序列分析 | BiLSTM双向长短期记忆神经网络时间序列预测(Matlab完整程序)预测结果评价指标基本介绍完整程序参考资料预测结果 评价指标 训练集数据的R2为:0.99302 测试集数据的R2为&…

企业数字化(技术中台、数据中台、工业互联网平台)建设方案

【版权声明】本资料来源网络&#xff0c;知识分享&#xff0c;仅供个人学习&#xff0c;请勿商用。【侵删致歉】如有侵权请联系小编&#xff0c;将在收到信息后第一时间删除&#xff01;完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 1.1 技术架构 1.1.1 技术架构总…

pandas提取excel数据形成三元组,采用neo4j数据库构建小型知识图谱

前言 代码来自github项目 neo4j-python-pandas-py2neo-v3&#xff0c;项目作者为Skyelbin。我记录一下运行该项目的一些过程文字以及遇到的问题和解决办法。 一、提取excel中的数据转换为DataFrame三元组格式 from dataToNeo4jClass.DataToNeo4jClass import DataToNeo4j imp…

Unity WebGL打包后运行

打包后出现以下&#xff1a;工程中没有StreamingAssets文件夹的&#xff0c;则打包后不会有下图StreamingAssets的文件夹 这3个文件夹都是项目资源&#xff0c;只有index.html才是打开Web运行的页面 序列 主流浏览器 WebGL支持 1 Microsoft Edge Edge16或更高版本 2 火狐…

基于超像素的多视觉特征图像分割算法研究

0.引言 背景&#xff1a; 经典聚类算法&#xff1a;Kmeans、FCM 现有问题&#xff1a; 1&#xff09;现有算法大都是基于单一的视觉特征而设计的&#xff0c;eg&#xff1a;基于颜色特征的分割。 2&#xff09;没有考虑像素周围的空间信息&#xff1b;分割结果&#xff1a;多噪…

Spring-boot @ConfigurationProperties(prefix = “/xx“) 实际作用以及实现思路

ConfigurationProperties 可以通过配置该注解到我们自己创建的类上达成取代 Value 的作用 最终目的都是读取到配置文件中的值而已。 ConfigurationProperties(prefix “/xx”) 其中 prefix 指定了配置文件中的配置值得开头值 示例 配置文件中自定义值 application.yml 中…

3-10 图文并茂解释TCP/IP 3次握手4次断开

文章目录前言TCP报文首部TCP数据传输3次握手4次断开前言 TCP/IP传输协议&#xff0c;即传输控制/网络协议&#xff0c;也叫作网络通讯协议。它是在网络的使用中的最基本的通信协议。TCP/IP传输协议对互联网中各部分进行通信的标准和方法进行了规定。并且&#xff0c;TCP/IP传输…

Vue基本指令:v-on,v-if,v-mode

目录 Vue基本指令 1、v-text指令&#xff1a;设置标签的文本值 2、v-html指令&#xff1a;设置标签的innerHTML 3、v-on指令&#xff1a;为元素绑定事件 4、v-on:传参 5、v-show指令&#xff1a;根据真假切换元素的显示状态 6、v-if指令&#xff1a;根据表达式的真假切换…

前端学习---

1.简单且必须掌握的 1.MVVM是什么 将MVC中的V变为了MVVM&#xff0c;实现了双向绑定。其中VM就是vue的作用&#xff0c;这样页面的动态化可以通过vue来操作&#xff0c;而不是页面直接与后端操作&#xff0c;实现了前后端的分离 2.为什么vue采用异步渲染 &#xff1f; 调…

【云原生】k8s集群资源监控平台搭建—20230227

1. 监控指标 集群监控 ①节点资源利用率 ②节点数 ③运行pods pod监控 ①容器指标 ②应用程序 2. 监控平台 1. Prometheus 开源的&#xff1b;集成了很多功能&#xff0c;比如监控、报警、数据库等&#xff1b;以HTTP协议周期性的抓取被监控组件的状态以及相应的数据&…

HarmonyOS Connect “Device Partner”专场FAQ来啦

Device Partner平台是面向AIoT产业链伙伴的一站式服务平台&#xff0c;伙伴可以通过平台获取最新的产品、服务与解决方案&#xff0c;实现智能硬件产品的开发、认证、量产和推广等全生命周期的管理&#xff0c;加入HarmonyOS Connect生态&#xff0c;共同提升消费者的智慧生活体…

就业天花板?33% 程序员月薪达到 5 万元以上

2023年&#xff0c;随着互联网产业的蓬勃发展&#xff0c;程序员作为一个自带“高薪多金”标签的热门群体&#xff0c;被越来越多的人所关注。 在过去充满未知的一年中&#xff0c;他们的职场现状发生了一定的改变。那么&#xff0c;程序员岗位的整体薪资水平、婚恋现状、职业…