2024年9月

一:背景

1. 讲故事

前段时间有位朋友找到我,说他的窗体程序在客户这边出现了卡死,让我帮忙看下怎么回事?dump也生成了,既然有dump了那就上 windbg 分析吧。

二:WinDbg 分析

1. 为什么会卡死

窗体程序的卡死,入口门槛很低,后续往下分析就不一定了,不管怎么说先用 !clrstack 看下主线程,输出如下:


0:000> !clrstack
OS Thread Id: 0x3118 (0)
        Child SP               IP Call Site
000000c478afd1d8 00007ffc284e9a84 [HelperMethodFrame_1OBJ: 000000c478afd1d8] System.Threading.WaitHandle.WaitOneNative(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
000000c478afd300 00007ffbf2cc19ac System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 243]
000000c478afd330 00007ffbf2cc197f System.Threading.WaitHandle.WaitOne(Int32, Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\waithandle.cs @ 194]
000000c478afd370 00007ffbf1421904 System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle)
000000c478afd3e0 00007ffbf0c8e2f4 System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
000000c478afd520 00007ffbf1425124 System.Windows.Forms.Control.Invoke(System.Delegate, System.Object[])
000000c478afd590 00007ffb995d6fe8 DevComponents.DotNetBar.StyleManager.OnColorTintChanged(System.Drawing.Color, System.Drawing.Color)
000000c478afd5f0 00007ffb995d69ff DevComponents.DotNetBar.StyleManager.set_ColorTint(System.Drawing.Color)
000000c478afd680 00007ffb995d694c DevComponents.DotNetBar.StyleManager.set_ManagerColorTint(System.Drawing.Color)
...
000000c478afd6b0 00007ffb995d50f9 xxx.MarkInspectPadControl.InitializeComponent()

有经验的朋友看到上面的卦象相信就知道咋事情了,即有工作线程创建了用户控件导致的,而且这个控件貌似和 DevComponents 有关,接下来的常规套路就是挖一下 WindowsFormsSynchronizationContext 对象看看到底是哪一个线程创建的,使用 !dso 即可。


0:000> !dso
OS Thread Id: 0x3118 (0)
RSP/REG          Object           Name
000000C478AFCF98 000002093b9143c0 System.Windows.Forms.WindowsFormsSynchronizationContext
...
0:000> !do poi(20939c91588)
Name:        System.Threading.Thread
MethodTable: 00007ffbf2769580
EEClass:     00007ffbf288c658
Size:        96(0x60) bytes
00007ffbf276aaf8  4001934       4c         System.Int32  1 instance                1 m_ManagedThreadId

按照剧本的话 WindowsFormsSynchronizationContext 应该会有2个,但这里只有1个,这一个还是主线程的同步上下文,这就完犊子了。。。完全不按照剧本走,这也是真实dump分析的复杂性,那到底是谁创建的呢? 天要绝人之路吗?

2. 出路在哪里

所有东西的落地都在汇编里,而汇编又在方法里,所以突破口就是寻找线程栈中的方法,接下来到
System.Windows.Forms.Control.MarshaledInvoke
方法里看一看可有什么大货,简化后如下:


private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
{
    bool flag = false;
    if (SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, Handle), out var _) == SafeNativeMethods.GetCurrentThreadId() && synchronous)
    {
        flag = true;
    }
    ThreadMethodEntry threadMethodEntry = new ThreadMethodEntry(caller, this, method, args, synchronous, executionContext);
    lock (threadCallbackList)
    {
        if (threadCallbackMessage == 0)
        {
            threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
        }
        threadCallbackList.Enqueue(threadMethodEntry);
    }
    if (flag)
    {
        InvokeMarshaledCallbacks();
    }
    else
    {
        UnsafeNativeMethods.PostMessage(new HandleRef(this, Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
    }
    if (synchronous)
    {
        if (!threadMethodEntry.IsCompleted)
        {
            WaitForWaitHandle(threadMethodEntry.AsyncWaitHandle);
        }
        return threadMethodEntry.retVal;
    }
    return threadMethodEntry;
}

从卦中的代码来看,这个
SafeNativeMethods.GetWindowThreadProcessId
方法是关键,它可以拿到这个窗口创建的
processid

threadid
,接下来观察下简化后的汇编代码。


0:000> !U /d 00007ffbf0c8e2f4
preJIT generated code
System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control, System.Delegate, System.Object[], Boolean)
Begin 00007ffbf0c8dec0, size 4e9
00007ffb`f0c8dec0 55              push    rbp
00007ffb`f0c8dec1 4157            push    r15
00007ffb`f0c8dec3 4156            push    r14
00007ffb`f0c8dec5 4155            push    r13
00007ffb`f0c8dec7 4154            push    r12
00007ffb`f0c8dec9 57              push    rdi
00007ffb`f0c8deca 56              push    rsi
00007ffb`f0c8decb 53              push    rbx
00007ffb`f0c8decc 4881ecf8000000  sub     rsp,0F8h
00007ffb`f0c8ded3 488dac2430010000 lea     rbp,[rsp+130h]
...
00007ffb`f0c8dff0 488d55b0        lea     rdx,[rbp-50h]
00007ffb`f0c8dff4 ff151e1eddff    call    qword ptr [System_Windows_Forms_ni+0x8fe18 (00007ffb`f0a5fe18)] (System.Windows.Forms.SafeNativeMethods.GetWindowThreadProcessId(System.Runtime.InteropServices.HandleRef, Int32 ByRef), mdToken: 00000000060033c4)
00007ffb`f0c8dffa 448bf0          mov     r14d,eax

根据卦中的汇编以及x64调用协定,
lea rdx,[rbp-50h]
就是我们的 processid,同时
mov r14d,eax
中的 r14d 就是我们的 threadid,突破口已找到,接下来就是深挖了。

3. 如何挖出进程ID和线程ID

有一点要知道 000000c478afd520 和 MarshaledInvoke 方法的 rsp 隔了一个 0x8,同时方法中影响 rsp 的 push 和 sub 都要计算进去,这里就不赘述了,具体可以参考文章:
https://www.cnblogs.com/huangxincheng/p/17250240.html
简单计算后如下:


0:000> ? 000000c478afd520-0x8-(0n8*0n8)-0xF8+0x130
Evaluate expression: 843838379280 = 000000c4`78afd510
0:000> dp 000000c4`78afd510-0x50 L1
000000c4`78afd4c0  00000000`000029dc

0:000> r r14
r14=000000c478afcf14
0:000> dp 000000c478afcf14 L1
000000c4`78afcf14  00000000`00000080

从卦中可以看到
processid=29dc ,threadid=0x80
,这东西是何方神圣呢,我们用 ~ 来找它的真身吧。

0:000> ~
...
  18  Id: 29dc.80 Suspend: 0 Teb: 000000c4`7890d000 Unfrozen
...

0:018> k
 # Child-SP          RetAddr               Call Site
00 000000c4`7a2ffcc8 00007ffc`28028ba3     ntdll!NtWaitForSingleObject+0x14
01 000000c4`7a2ffcd0 00007ffb`fa651cf8     KERNELBASE!WaitForSingleObjectEx+0x93
02 000000c4`7a2ffd70 00007ffb`fa652a51     wpfgfx_v0400!CPartitionManager::GetWork+0x17b
03 000000c4`7a2ffdc0 00007ffb`fa67a2fb     wpfgfx_v0400!CPartitionThread::Run+0x21
04 000000c4`7a2ffdf0 00007ffc`2a037bd4     wpfgfx_v0400!CPartitionThread::ThreadMain+0x2b
05 000000c4`7a2ffe20 00007ffc`2a76ced1     kernel32!BaseThreadInitThunk+0x14
06 000000c4`7a2ffe50 00000000`00000000     ntdll!RtlUserThreadStart+0x21

现在有点傻傻分不清了,怎么 winform 里还有 wpf 的渲染线程,有可能是 DevComponents 这种第三方控件在底层引入的吧。到这里路子又被堵死了,接下来该往哪里走呢?三步一回头,继续看主线程上的方法代码吧。

4. 在源码中寻找答案

虽然在两条路上的突围都失败了,但可以明显的看到离真相真的越来越近,也收获到了大量的作战信息,通过上面的
set_ManagerColorTint
方法的反编译,参考如下:


private void InitializeComponent()
{
    this.styleManager1.ManagerColorTint = System.Drawing.Color.Black;
}

[Description("Indicates color current style is tinted with.")]
[Category("Appearance")]
public Color ManagerColorTint
{
    get
    {
        return ColorTint;
    }
    set
    {
        ColorTint = value;
    }
}

看到源码之后太无语了,其实就是一个简单的
颜色赋值
,根据前面的探索
styleManager1
是由渲染线程创建的,所以主线程对它的赋值自然是得不到渲染线程的反馈。

那这个问题该怎么办呢?大概是如下两种吧。

  1. 重点关注 styleManager1 控件,用排除法观察程序运行状况。
  2. 看文档是否用了错误的方式使用 styleManager1 控件。

三:总结

这次生产事故还是挺有意思的,为什么 WinForm 中可以存在
CPartitionThread
渲染线程,最后还祸在其身,给我几百例dump分析之旅中添加了一笔色彩!

图片名称

十,Spring Boot 的内容协商的详细剖析(附+Debug调试说明)

@


1. 基本介绍

根据客户端接收能力不同,SpringBoot返回不同媒体类型的数据。

比如:客户端 Http请求
Accept:application/xml
则返回xml数据,客户端 Http 请求
Accept:application/json
则返回json数据。

关于 内容协商的 类是:
AbstractJackson2HttpMessageConverter.java
这个类

在这里插入图片描述

在这里插入图片描述

2. 准备工作

导入相关的
jar
依赖。

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>springboot_jsonxml</artifactId>
    <version>1.0-SNAPSHOT</version>


    <!--    导入SpringBoot 父工程-规定写法-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <!--    导入web项目场景启动器:会自动导入和web开发相关的jar包所有依赖【库/jar】-->
    <!--    后面还会在说明spring-boot-starter-web 到底引入哪些相关依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

</project>

准备好,测试的 Bean 类,两个,分别为 Car,Monster 类,这里我使用了
lombok
插件自动生成 set和 get方法。关于这部分的内容大家可以移步至:✏️✏️✏️
六,Spring Boot 容器中 Lombok 插件的详细使用,简化配置,提高开发效率_lombok的getter和setter怎么用-CSDN博客

在这里插入图片描述

package com.rainbowsea.bean;


import lombok.Data;


@Data
public class Car {
    private String name;
    private Double price;

}

package com.rainbowsea.bean;


import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;


@Component
@Setter
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Monster {
    private Integer id;
    private String name;
    private Boolean isMarried;
    private Integer age;
    private Date birth;
    private Car car;
    private String[] skill;
    private List<String> hobby;
    private Map<String,Object> wife;
    private Set<Double> salaries;
    private Map<String,List<Car>> cars;

}

相关的 Controller 控制器,处理相关的请求路径映射

在这里插入图片描述

package com.rainbowsea.controller;


import com.rainbowsea.bean.Car;
import com.rainbowsea.bean.Monster;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

@Controller
public class ResponseController {


    // 返回 Monster 数据-要求以JSON格式返回
    @GetMapping("/get/monster")
    @ResponseBody
    public Monster getMonster() {

        // monster 对象是从DB数据库获取-这里老师模拟一个monster对象
        Monster monster = new Monster();
        monster.setId(100);
        monster.setName("奔波霸");
        monster.setAge(200);
        monster.setIsMarried(false);
        monster.setBirth(new Date());
        Car car = new Car();
        car.setName("奔驰");
        car.setPrice(222.2);
        monster.setCar(car);

        return monster;
    }

}

项目的/应用程序的启动场景

在这里插入图片描述

package com.rainbowsea;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication  // 标注项目的启动场景
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

运行程序,打开浏览器输入:
http://localhost:8080/get/monster
。运行测试

在这里插入图片描述

3. 内容协商的本质

从上述运行结果的返回来看,显示的是
JSON
格式的数据。

为什么显示的是 JSON格式的数据。显示返回的是什么样的格式的数据是由:

请求头当中的
Accept
属性的值所决定的。

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,
/
;q=0.8

在这里插入图片描述

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
xml 为 0.9 优先级更高
*/*(其它的数据类型,包括 json格式的字符) 为 0.8 稍微低一点。

而这里之所以显示的
JSON
格式的数据,是因为在我们pom.xml 文件中导入的 spring boot 依赖当中包含了 json 数据格式的
jar
依赖。

在这里插入图片描述

而没有 xml 的数据格式的
jar
依赖,自然就是优先为 json 你有的数据格式的jar依赖为准了。

我们可以 Debug 看看。

我们在
AbstractJackson2HttpMessageConverter.java
类当中的
writeInternal()
的方法当中打上
断点

在这里插入图片描述

在这里插入图片描述

直到走到这里,我们查看 generator的值:发现是 JSON。这就没错了,我们仅仅是spring boot 框架种自行配置了 json 数据格式的依赖,而没有其它的数据格式的依赖,自然就使用我们有的数据格式的了。

下面我们导入
xml
数据格式的依赖,再进行一个测试

在这里插入图片描述

<!--        引入 处理xml 的依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rainbowsea</groupId>
    <artifactId>springboot_jsonxml</artifactId>
    <version>1.0-SNAPSHOT</version>


    <!--    导入SpringBoot 父工程-规定写法-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <!--    导入web项目场景启动器:会自动导入和web开发相关的jar包所有依赖【库/jar】-->
    <!--    后面还会在说明spring-boot-starter-web 到底引入哪些相关依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--        引入 处理xml 的依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </dependency>

    </dependencies>

</project>

重新运行程序,打开浏览器输入:
http://localhost:8080/get/monster
。运行测试
在这里插入图片描述

这回返回的就是 xml 格式的数据了,因为我们这里配置了 json和 xml 两种格式的依赖,但是,xml数据格式的优先级为0.9比 json 的优先级更高,所以显示的是 xml 格式的数据类型。

同样我们也进行一个 Debug 断点调试。还是一样的。

在这里插入图片描述

在这里插入图片描述

我们前端显示的也是 xml 格式的数据。
在这里插入图片描述


4. 内容协商:注意事项和使用细节

  1. Postman 可以通过修改 Accept 的值,来返回不同的数据格式。感兴趣的大家可以自行去下载,试试。官网地址:
    https://www.postman.com/

在这里插入图片描述

  1. 对于浏览器,我们无法修改其 Accept的值,怎么办?解决方案:开启支持基于请求参数的内容协商功能

  2. 修改
    application.yaml
    (必须在类路径"resources"下才行),开启基于请求参数的内容协商功能。

在这里插入图片描述

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true # 开启基于请求参数的内容协商功能
      #?format=json

其实修改的就是:WebMvcProperties类当中的 内部类 Contentnegotiation的 favorParameter 的值。

在这里插入图片描述

在这里插入图片描述

配置好以后,重新启动程序,打开浏览器,注意需要在我们地址后面加上
?format=xxx
,xxx 就是你要转换显示为什么样格式的数据的值。比如:

  • ?format=json 就是在前端以 json格式的数据展示。
  • ?format=xml 就是在前端以 xml 格式的数据展示。

注意:参数format是规定好的,在开启请求参数的内容协商功能后,SpringBoot底层 ParameterContentNegotiationStrategy 会通过 format 来接收参数,然后返回对应的媒体类型/数据格式,当然format=xxx,这个xxx媒体类型/数据格式是SpringBoot可以
处理的才行,不能乱写。

也注意是英文的
?

在这里插入图片描述

在这里插入图片描述

其实这个
format
的值是:ParameterContentNegotiationStrategy 类当中的 parameterName 属性值。

在这里插入图片描述

这个我们也可以修改这个值,指定一个内容协商的参数名,就不再是默认的 format而是我们自己定义的一个参数名了。

还是在
application.yaml
文件当中配置,增加上一个属性。

这里我们指定**内容协商的参数名为,
rainbowsea

在这里插入图片描述

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true # 开启基于请求参数的内容协商功能
      #?format=json
      parameter-name: rainbowsea # 指定一个内容协商的参数名,就不再是默认的 format而是
      # ?rainbowsea=json ,默认的失效了

重新启动程序,浏览器运行测试:

在这里插入图片描述

需要注意的是: 我们自己指定一个内容协商的参数名,修改掉了默认的 format 的参数了,就不再是默认的 format而是,rainbowsea=json ,默认的(format 就不可以再用了,已经失效了)失效了。

在这里插入图片描述

默认的 format 已经失效了, 无论我们配置=json,还是 xml ,都返回的是 xml 格式类型的数据

。因为 xml 优先级是0.9 比较高的,所以返回的就是默认优先级高的 xml 的格式的了。

5. 总结:

  1. 内容协商就是:根据客户端接收能力不同,SpringBoot返回不同媒体类型的数据。

    比如:客户端 Http请求
    Accept:application/xml
    则返回xml数据,客户端 Http 请求
    Accept:application/json
    则返回json数据。

  2. 内容协商的返回值是由:

  3. 请求头当中的
    Accept
    属性的值所决定的。Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,
    /
    ;q=0.8

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
xml 为 0.9 优先级更高
*/*(其它的数据类型,包括 json格式的字符) 为 0.8 稍微低一点。
  1. 参数format是规定好的,在开启请求参数的内容协商功能后,SpringBoot底层 ParameterContentNegotiationStrategy 会通过 format 来接收参数,然后返回对应的媒体类型/数据格式,当然format=xxx,这个xxx媒体类型/数据格式是SpringBoot可以
    处理的才行,不能乱写。也注意是英文的
    ?
  2. 我们自己指定一个内容协商的参数名,修改掉了默认的 format 的参数了,就不再是默认的 format而是,rainbowsea=json ,默认的(format 就不可以再用了,已经失效了)失效了。

6. 最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”

在这里插入图片描述

商业模式

商业模式是帮助企业成功的“秘诀”,它通过整合企业内外部的多种要素,构建起一个全面、高效且具有独特竞争优势的运营体系。这一体系的目的是满足市场的需求,实现各利益相关者价值最大化,并确保企业的长期盈利能力。

商业模式的核心架构由三个紧密相连的环节构成:创造价值、传递价值和获取价值。

  • 创造价值:这一环节围绕客户的需求展开,提供具有吸引力的价值提案。
  • 传递价值:通过优化资源配置和实施相关策略,确保价值的有效传递。
  • 获取价值:通过精心设计的盈利机制,实现企业收益的持续增长。

这三个环节相互依赖,共同构成了商业模式的框架,它们协同工作,帮助企业在竞争激烈的市场中脱颖而出。

商业模式举例

因为互联网和数字化的发展,公司正在努力改变传统的商业模式,把生产、销售、交易、物流和支付等多个部分整合在一起,不受时间和空间的限制。

通过使用移动互联网、大数据、人工智能和云计算等新技术,公司正在改变消费者的购物习惯和生活方式。新零售的改变特别明显,线下实体门店向线上转型,而线上电商也在寻找扩大实体店市场机会。下面是一些典型的电商商业模式的例子:

  • B2C(商家对消费者):这是电商领域最为常见的模式,商家直接向消费者销售产品和服务。天猫、京东等平台是这一模式的典型代表。
  • B2B(商家对商家):在这一模式下,供应方和采购方通过电商平台完成交易,有效解决了供应链上游至中游的问题。1688等平台是这一模式的杰出代表。
  • C2C(消费者对消费者):在这种模式中,消费者可以直接与其他消费者交易。它强调产品的个性化和质量,提供类似于B2C的服务,但更注重服务。淘宝、微店等平台就是这种模式的例子。
  • C2M(消费者对工厂):这是一种消费者与制造商直接对接的模式,去除了中间环节,提供定制化的生产和消费,强调定制服务和增值服务。淘宝特价版、拼多多等平台采用了这一模式。
  • O2O(线上到线下):这一模式融合了线上信息获取和线下购买体验,是新零售商业模式的典型代表。许多传统企业正在积极探索O2O模式,以增强市场竞争力。

商业模式并非一成不变,它随着企业经营策略和市场环境的变化而变化。

商业模式画布

商业模式画布是一种广泛使用的规划商业模式的工具,由亚历山大·奥斯特瓦德(Alexander Osterwalder)首创。通过九宫格框架,企业能够将商业模式形象化,全面审视和分析其商业运作。

商业模式画布的核心要素:

  • 价值主张:企业向客户提供哪些独特的产品、服务或价值,并解决客户的哪些问题?价值主张是企业与竞争对手区分开的关键,通过创新性、性能、定制化、质量、设计、品牌、定价、成本效益、风险降低、便捷性和可用性等元素来提供价值。
  • 客户关系:企业打算与客户建立什么关系?可能的关系类型包括个性化服务、专属服务、自助服务、自动化服务、社区互动和共同创造等。
  • 客户细分:企业的目标客户群体是谁?通过识别和理解客户的不同需求及特征,企业可以对客户群体进行精准细分,如大众市场、小众市场、多边市场、区隔化市场等,更好地满足特定客户群体的需求。
  • 核心资源:企业为了支持商业活动,有哪些重要的资源?这些可能是实体资产、知识、员工、或者金融资产等。
  • 关键活动:企业需要进行哪些主要活动,来确保产品或服务的顺利运作?这些活动可能包括产品制造、问题解决、平台构建和服务网络搭建等。
  • 渠道通路:企业通过哪些途径将产品或服务传递给客户?渠道通路的构建涉及五个阶段:认知、评估、购买、交付和售后。渠道类型包括自有渠道(如实体店)、合作伙伴渠道(如分销商),同时还需考虑线上、线下和O2O等新零售渠道。
  • 合作伙伴:企业需要与哪些上游或下游的企业建立深度合作关系?合作伙伴关系可能包括战略联盟、竞争合作、新业务合作以及供应商与购买方关系。合作的实质在于资源共享和互利共赢。
  • 成本结构:企业在商业运作中是否充分考虑了成本因素?成本结构可以是成本驱动型或价值驱动型,需要考虑固定成本、变动成本、规模经济和范围经济等。
  • 收入来源:企业的主要收入途径是什么?收入的生成方式包括产品销售、使用费、订阅费、租赁费、授权费、交易费和广告费等。

下图所示为滴滴企业的商业模式画布:

价值流

价值流的相关概念包括:价值主张、价值流、价值流阶段。

价值主张

价值主张概念在商业模式画布部分已解释过。

价值主张处于商业画布的中心位置,当企业决定是否投资某个产品或服务前,首先需明确它为哪个客户群体服务?提供什么价值?以及目标客户群体能否承受产品或服务的价格?

价值流

价值流的定义:为客户创造结果的端到端活动的集合,客户可能是价值流的最终客户,也可能内部使用用户。

价值流更专注于特定的目标客户和价值主张,目标明确。同时,价值流更强调结果导向和价值增长。

通过价值流分析,我们可以很容易地看出哪些环节是增值的,哪些是不增值的。理论上,我们可以消除或减弱没有价值增长的环节,这可以避免流程过于繁重,效果不明显的情况。

价值流阶段

价值流可以进一步细分为不同的价值流阶段,其中的每个价值流阶段,都会贡献相应的价值增量,以确保客户所需整体价值的逐步实现和完整交付。价值流阶段有以下几个特征:

  • 每个流程阶段都有相应的“价值”。如果某个阶段不能增加或贡献到客户需要的价值,理论上可以放弃这个阶段。
  • 每个阶段都有进入条件。只有满足特定条件才能进入下一步。确认和保障这些条件,有利于阶段顺利实现目标。
  • 每个价值流阶段都有完成条件。设定明确的完成条件,可以快速检查是否已完成该阶段的任务,并开始下一个阶段。

我们以一个门店自提服务为例子:

  • 价值主张:让顾客享受优质的酒店服务
  • 价值流阶段:整个价值链由5个阶段组成,分别为商品浏览、下单支付、备货并通知、自提、售后。

业务能力

业务能力指的是企业开展其业务活动所必需的一系列核心技能和资源。这些能力是从业务的角度出发,为了达成特定的目标或结果,构建的特定技能或生产能力。

企业的业务能力与商业模式和价值流密切相关,因为它们直接影响了企业的业绩和价值创造。它们确保了企业战略的实施,并与客户旅程和市场环境保持一致。此外,业务能力也协调了业务需求和IT系统。

业务能力的范畴较为宏观,它有助于企业从多角度进行战略规划和业务发展。在TOGAF(开放组织架构框架)中,业务能力的实现涉及角色、流程、信息和工具的综合运用。

在其他企业管理理论中,业务能力同样被视为企业架构中的一个重要组成部分,它包括人员、组织机构、功能、流程、业务服务、数据信息、应用系统和基础设施等多个要素,并与企业的各种项目和解决方案紧密相关。

业务能力提供了一种独立于现有组织结构、业务流程、应用系统、产品/服务的业务视角,有助于企业从更高层次上理解和管理其业务。

在业务架构体系中,业务能力的关键在于系统化地表现企业的核心业务功能。

以电商业务为例,常见的业务能力包括店铺管理、商品管理、会员营销、订单处理、物流、支付结算、售后服务等方面的综合管理。这些管理活动汇集而成的能力,构成了企业高层业务能力,并且可以进一步细分为多个子业务能力。

例如,订单处理能力可以细分为平台订单管理、自营订单管理、第三方订单管理、订单来源追踪、订单分拆处理等子能力。

业务流程

业务流程是一系列逻辑上相关联的业务活动,它们组合起来以达成特定的业务结果。在业务架构的设计阶段,业务流程扮演着至关重要的角色,它不仅关系到企业资源的有效利用,也直接影响到企业IT架构中应用功能的设计和系统整合的具体需求。

业务流程是价值流概念的进一步展开,它将价值流中的概念细化为可操作的流程。

业务流程与业务能力的区别:

  • 业务能力:关注企业核心业务的能力和结果,不涉及具体的流程分解。
  • 业务流程:专注于流程本身,面向特定场景,通过活动组合解决具体问题,是企业日常运作的关键。
  • 业务流程涵盖关键业务活动,如销售、市场推广、生产、采购和客户服务等,以及执行这些活动的角色和他们之间的互动。同时,业务流程还需遵循行业规范、专业标准和企业内部规章。

业务流程可以进一步细化为不同层级,包括主流程和子流程。它们是连接不同业务部门的纽带,端到端流程往往跨越多个部门或业务能力领域,实现价值的增加。

以电商系统为例,用户交易流程是一个相对标准化的流程,通常包括以下环节:

  • 商品选择
    :用户浏览电商平台,选择想要购买的商品,并添加到购物车。
  • 购物车确认
    :用户查看购物车中的商品,可以修改数量或删除不想购买的商品。
  • 结算
    :用户选择“结算”选项,准备进行支付。在此步骤,用户还可以选择或添加配送地址。
  • 支付
    :用户选择支付方式(如信用卡、支付宝、微信支付等),输入必要的支付信息进行支付。
  • 订单确认
    :支付完成后,系统生成订单,确认购买的商品和支付详情,并通常通过邮件或短信形式发送订单确认信息给用户。
  • 物流处理
    :订单信息传递给仓库,开始打包和发货流程。
  • 发货与跟踪
    :商品发货后,用户可以通过订单系统跟踪物流状态。
  • 确认收货
    :用户收到货物,并确认收货。

业务流程可以进一步细化为不同层次,提供更具体的管理和执行指南。通过分层方法,企业能够确保业务流程设计与价值流的每个环节紧密相连,从而在不同层级上实现价值的最大化。通常包括以下几个层次:

  • 流程类别
    :大类别,如采购、销售、生产等。
  • 流程组
    :在同一类别下的相关流程集合,如订单处理流程组可能包括订单接收、订单确认、订单履行等。
  • 流程
    :具体的操作步骤,例如订单确认流程可能包括接收订单、审核订单、确认库存、生成配送单等步骤。
  • 子流程
    :流程中的更详细步骤,如审核订单可能细化为验证客户资料、检查支付状态等。
  • 任务
    :最基本的操作单元,具体到个人的具体工作,如输入客户订单数据、打印配送单等。

组织架构

组织架构是按照企业战略来设定和安排部门和岗位,形成稳定且科学的管理体系。这个体系保证企业能适应业务需求并支持企业发展。

组织架构对于业务架构至关重要。在梳理业务流程时,必须根据业务流程的运作规律和处理逻辑,在流程的各个节点上安排合适的人员,确保组织的灵活性和明确的责任分配。

同时,业务架构也需要考虑组织的业务需求和发展,对部门的岗位设置、人员配置、角色定义、权限分配、职责明确以及考核机制进行清晰的规划,保障业务流程中每个环节的顺利运作。

下图所示为一个中小连锁企业的组织架构图。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

前言

人工智能时代,人脸识别技术已成为安全验证、身份识别和用户交互的关键工具。

给大家推荐一款.NET 开源提供了强大的人脸识别 API,工具不仅易于集成,还具备高效处理能力。

本文将介绍一款如何利用这些API,为我们的项目添加智能识别的亮点。

项目介绍

GitHub 上拥有 1.2k 星标的 C# 面部识别 API 项目:FaceRecognitionDotNet。该项目功能强大,开箱即用,并支持跨平台。

它使用了 OpenCVSharp 和 face_recognition 开源库,并提供了 NuGet 包,方便集成到项目中。

项目是 face_recognition 的 C# 移植版本。 face_recognition 是一个基于 Python 的人脸识别库,它提供了简单易用的接口来进行人脸检测、人脸识别和人脸特征提取等功能。

这个库基于dlib和OpenCV开发,并且提供了一个高级的人脸识别接口,可以用于识别图像或视频中的人脸,并且可以识别出不同人物之间的相似度。

项目特点

  • 预测年龄
  • 情绪识别
  • 性别判断
  • 脸部标记
  • 眨眼检测

项目说明

支持跨平台,包括 Windows、Linux 和 macOS!

支持的API

项目展示

1、面部识别

2、年龄和性别

3、脸部标记

4、情绪识别

项目地址

文档:
https://taktak.jp/FaceRecognitionDotNet/

face_recognition:
https://github.com/ageitgey/face_recognition

Github:
https://github.com/takuya-takeuchi/FaceRecognitionDotNet

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言


第四讲
我们介绍了 main goroutine 是如何运行的。其中针对 main goroutine 介绍了调度函数 schedule 是怎么工作的,对于整个调度器的调度策略并没有介绍,这点是不完整的,这一讲会完善调度器的调度策略部分。

1. 调度时间点

runtime.schedule
实现了调度器的调度策略。那么对于调度时间点,查看哪些函数调用的
runtime.schedule
即可顺藤摸瓜理出调度器的调度时间点,如下图:

image

调度时间点不是本讲的重点,这里有兴趣的同学可以顺藤摸瓜,摸摸触发调度时间点的路径,这里就跳过了。

2. 调度策略

调度策略才是我们的重点,进到
runtime.schedule

// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
    mp := getg().m                  // 获取当前执行线程

top:
	pp := mp.p.ptr()                // 获取执行线程绑定的 P
	pp.preempt = false

    // Safety check: if we are spinning, the run queue should be empty.
	// Check this before calling checkTimers, as that might call
	// goready to put a ready goroutine on the local run queue.
    if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
		throw("schedule: spinning with local work")
	}

    gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available

    ...
    execute(gp, inheritTime)        // 执行找到的 goroutine
}

runtime.schedule
的重点在
findRunnable()

findRunnable()
函数很长,为避免影响可读性,这里对大部分流程做了注释,后面在有重点的加以介绍。进入
findRunnable()

// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from local or global queue, poll network.
// tryWakeP indicates that the returned goroutine is not normal (GC worker, trace
// reader) so the caller should try to wake a P.
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
	mp := getg().m                                      // 获取当前执行线程

top:
	pp := mp.p.ptr()                                    // 获取线程绑定的 P
	...
	
    // Check the global runnable queue once in a while to ensure fairness.
	// Otherwise two goroutines can completely occupy the local runqueue
	// by constantly respawning each other.
	if pp.schedtick%61 == 0 && sched.runqsize > 0 {
		lock(&sched.lock)
		gp := globrunqget(pp, 1)
		unlock(&sched.lock)
		if gp != nil {
			return gp, false, false
		}
	}

    // local runq
	if gp, inheritTime := runqget(pp); gp != nil {      // 从 P 的本地队列中获取 goroutine
		return gp, inheritTime, false
	}

    // global runq
	if sched.runqsize != 0 {                            // 如果本地队列获取不到就判断全局队列中有无 goroutine
		lock(&sched.lock)                               // 如果有的话,为全局变量加锁
		gp := globrunqget(pp, 0)                        // 从全局队列中拿 goroutine
		unlock(&sched.lock)                             // 为全局变量解锁
		if gp != nil {
			return gp, false, false
		}
	}

    // 如果全局队列中没有 goroutine 则从 network poller 中取 goroutine
    if netpollinited() && netpollWaiters.Load() > 0 && sched.lastpoll.Load() != 0 {
		...
	}

    // 如果 network poller 中也没有 goroutine,那么尝试从其它 P 中偷 goroutine
    // Spinning Ms: steal work from other Ps.
	//
	// Limit the number of spinning Ms to half the number of busy Ps.
	// This is necessary to prevent excessive CPU consumption when
	// GOMAXPROCS>>1 but the program parallelism is low.
    // 如果下面两个条件至少有一个满足,则进入偷 goroutine 逻辑
    // 条件 1: 当前线程是 spinning 自旋状态
    // 条件 2: 当前活跃的 P 要远大于自旋的线程,说明需要线程去分担活跃线程的压力,不要睡觉了
	if mp.spinning || 2*sched.nmspinning.Load() < gomaxprocs-sched.npidle.Load() {
        if !mp.spinning {                                       // 因为是两个条件至少满足一个即可,这里首先判断当前线程是不是自旋状态
			mp.becomeSpinning()                                 // 如果不是,更新线程的状态为自旋状态
		}

        gp, inheritTime, tnow, w, newWork := stealWork(now)     // 偷 goroutine
		if gp != nil {
			// Successfully stole.
			return gp, inheritTime, false                       // 如果 gp 不等于 nil,表示偷到了,返回偷到的 goroutine
		}
		if newWork {                
			// There may be new timer or GC work; restart to
			// discover.
			goto top                                            // 如果 gp 不等于 nil,且 network 为 true,则跳到 top 标签重新找 goroutine
		}

		now = tnow
		if w != 0 && (pollUntil == 0 || w < pollUntil) {
			// Earlier timer to wait for.
			pollUntil = w
		}
	}

    ...
    if sched.runqsize != 0 {                                    // 偷都没偷到,还要在找一遍全局队列,防止偷的过程中,全局队列又有 goroutine 了
		gp := globrunqget(pp, 0)
		unlock(&sched.lock)
		return gp, false, false
	}

    if !mp.spinning && sched.needspinning.Load() == 1 {         // 在判断一遍,如果 mp 不是自旋状态,且 sched.needspinning == 1 则更新 mp 为自旋,调用 top 重新找一遍 goroutine
		// See "Delicate dance" comment below.
		mp.becomeSpinning()
		unlock(&sched.lock)
		goto top
	}

    // 实在找不到 goroutine,表明当前线程多, goroutine 少,准备挂起线程
    // 首先,调用 releasep 取消线程和 P 的绑定
    if releasep() != pp {                                       
		throw("findrunnable: wrong p")
	}

    ...
    now = pidleput(pp, now)                                     // 将解绑的 P 放到全局空闲队列中
    unlock(&sched.lock)

    wasSpinning := mp.spinning                                  // 到这里 mp.spinning == true,线程处于自旋状态
	if mp.spinning {
		mp.spinning = false                                     // 设置 mp.spinning = false,这是要准备休眠了
		if sched.nmspinning.Add(-1) < 0 {                       // 将全局变量的自旋线程数减 1,因为当前线程准备休眠,不偷 goroutine 了
			throw("findrunnable: negative nmspinning")
		}
        ...
    }
    stopm()                                                     // 线程休眠,直到唤醒
	goto top                                                    // 能执行到这里,说明线程已经被唤醒了,继续找一遍 goroutine
}

看完线程的调度策略我都要被感动到了,何其的敬业,穷尽一切方式去找活干,找不到活,休眠之前还要在找一遍,真的是劳模啊。

大致流程是比较清楚的,我们把其中一些值得深挖的部分在单拎出来。

首先,从本地队列中找 goroutine,如果找不到则进入全局队列找,这里如果看
gp := globrunqget(pp, 0)
可能会觉得疑惑,从全局队列中拿 goroutine 为什么要把 P 传进去,我们看这个函数在做什么:

// Try get a batch of G's from the global runnable queue.
// sched.lock must be held.											// 注释说的挺清晰了,把全局队列的 goroutine 放到 P 的本地队列
func globrunqget(pp *p, max int32) *g {
	assertLockHeld(&sched.lock)										

	if sched.runqsize == 0 {
		return nil
	}

	n := sched.runqsize/gomaxprocs + 1								// 全局队列是线程共享的,这里要除 gomaxprocs 平摊到每个线程绑定的 P
	if n > sched.runqsize {
		n = sched.runqsize											// 执行到这里,说明 gomaxprocs == 1
	}
	if max > 0 && n > max {
		n = max
	}
	if n > int32(len(pp.runq))/2 {									
		n = int32(len(pp.runq)) / 2									// 如果 n 比本地队列长度的一半要长,则 n == len(P.runq)/2
	}

	sched.runqsize -= n												// 全局队列长度减 n,准备从全局队列中拿 n 个 goroutine 到 P 中

	gp := sched.runq.pop()											// 把全局队列队头的 goroutine 拿出来,这个 goroutine 是要返回的 goroutine
	n--																// 拿出了一个队头的 goroutine,这里 n 要减 1
	for ; n > 0; n-- {				
		gp1 := sched.runq.pop()										// 循环拿全局队列中的 goroutine 出来
		runqput(pp, gp1, false)										// 将拿出的 goroutine 放到全局队列中
	}
	return gp
}

调用
globrunqget
说明本地队列没有 goroutine 要从全局队列拿,那么就可以把全局队列中的 goroutine 放到 P 中,提高了全局队列 goroutine 的优先级。

如果全局队列也没找到 goroutine,在从
network poller
找,如果
network poller
也没找到,则准备进入自旋,从别的线程的 P 那里偷活干。我们看线程是怎么偷活的:

// stealWork attempts to steal a runnable goroutine or timer from any P.
//
// If newWork is true, new work may have been readied.
//
// If now is not 0 it is the current time. stealWork returns the passed time or
// the current time if now was passed as 0.
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {
	pp := getg().m.p.ptr()																// pp 是当前线程绑定的 P

	ranTimer := false

	const stealTries = 4																// 线程偷四次,每次都要随机循环一遍所有 P
	for i := 0; i < stealTries; i++ {
		stealTimersOrRunNextG := i == stealTries-1

		for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {			// 为保证偷的随机性,随机开始偷 P。随机开始,后面每个 P 都可以轮到
			...
			p2 := allp[enum.position()]													// 从 allp 中获取 P
			if pp == p2 {
				continue																// 如果获取的是当前线程绑定的 P,则继续循环下一个 P
			}
			...
			// Don't bother to attempt to steal if p2 is idle.
			if !idlepMask.read(enum.position()) {										// 判断拿到的 P 是不是 idle 状态,如果是,表明 P 还没有 goroutine,跳过它,偷下一家
				if gp := runqsteal(pp, p2, stealTimersOrRunNextG); gp != nil {			// P 不是 idle,调用 runqsteal 偷它!
					return gp, false, now, pollUntil, ranTimer
				}
			}
		}
	}

	// No goroutines found to steal. Regardless, running a timer may have
	// made some goroutine ready that we missed. Indicate the next timer to
	// wait for.
	return nil, false, now, pollUntil, ranTimer
}

线程随机的偷一个可偷的 P,偷 P 的实现在
runqsteal
,查看
runqsteal
怎么偷的:

// Steal half of elements from local runnable queue of p2
// and put onto local runnable queue of p.
// Returns one of the stolen elements (or nil if failed).						// 给宝宝饿坏了,直接偷一半的 goroutine 啊,够狠的!
func runqsteal(pp, p2 *p, stealRunNextG bool) *g {
	t := pp.runqtail															// t 指向当前 P 本地队列的队尾
	n := runqgrab(p2, &pp.runq, t, stealRunNextG)								// runqgrab 把 P2 本地队列的一半 goroutine 拿到 P 的 runq 队列中
	if n == 0 {
		return nil
	}
	n--
	gp := pp.runq[(t+n)%uint32(len(pp.runq))].ptr()								// 把偷到的本地队列队尾的 goroutine 拿出来
	if n == 0 {
		return gp																// 如果只偷到了这一个,则直接返回。有总比没有好
	}
	h := atomic.LoadAcq(&pp.runqhead) // load-acquire, synchronize with consumers
	if t-h+n >= uint32(len(pp.runq)) {
		throw("runqsteal: runq overflow")										// 如果 t-h+n >= len(p.runq) 表示偷多了...
	}
	atomic.StoreRel(&pp.runqtail, t+n) 											// 更新 P 的本地队列的队尾
	return gp
}

这个偷就是把“地主家”(P2)的余粮 (goroutine) 给它抢一半过来,没办法我也要吃饭啊。

如果连偷都没偷到(好吧,太惨了点...),那就准备休眠了,不干活了还不行嘛。不干活之前在去看看全局队列有没有 goroutine 了(口是心非的 M 人)。还是没活,好吧,准备休眠了。

准备休眠,首先解除和 P 的绑定:

func releasep() *p {
	gp := getg()

	if gp.m.p == 0 {
		throw("releasep: invalid arg")
	}
	pp := gp.m.p.ptr()
	if pp.m.ptr() != gp.m || pp.status != _Prunning {
		print("releasep: m=", gp.m, " m->p=", gp.m.p.ptr(), " p->m=", hex(pp.m), " p->status=", pp.status, "\n")
		throw("releasep: invalid p state")
	}
	...
	gp.m.p = 0
	pp.m = 0
	pp.status = _Pidle
	return pp
}

就是指针的解绑操作,代码很清晰,连注释都不用,我们也不讲了。

解绑之后,
pidleput
把空闲的 P 放到全局空闲队列中。

接着,更新线程的状态,从自旋更新为非自旋,调用
stopm
准备休眠:

// Stops execution of the current m until new work is available.
// Returns with acquired P.
func stopm() {
	gp := getg()							// 当前线程执行的 goroutine

	...

	lock(&sched.lock)
	mput(gp.m)								// 将线程放到全局空闲线程队列中
	unlock(&sched.lock)
	mPark()
	acquirep(gp.m.nextp.ptr())
	gp.m.nextp = 0
}

stopm
将线程放到全局空闲线程队列,接着调用
mPark
休眠线程:

// mPark causes a thread to park itself, returning once woken.
//
//go:nosplit
func mPark() {
	gp := getg()
	notesleep(&gp.m.park)					// notesleep 线程休眠
	noteclear(&gp.m.park)
}

func notesleep(n *note) {
	gp := getg()
	if gp != gp.m.g0 {
		throw("notesleep not on g0")
	}
	ns := int64(-1)
	if *cgo_yield != nil {
		// Sleep for an arbitrary-but-moderate interval to poll libc interceptors.
		ns = 10e6
	}
	for atomic.Load(key32(&n.key)) == 0 {					// 这里通过 n.key 判断线程是否唤醒,如果等于 0,表示未唤醒,线程继续休眠
		gp.m.blocked = true
		futexsleep(key32(&n.key), 0, ns)					// 调用 futex 休眠线程,线程会“阻塞”在这里,直到被唤醒
		if *cgo_yield != nil {
			asmcgocall(*cgo_yield, nil)
		}
		gp.m.blocked = false								// “唤醒”,设置线程的 blocked 标记为 false
	}
}

// One-time notifications.
func noteclear(n *note) {									
	n.key = 0												// 执行到 noteclear 说明,线程已经被唤醒了,这时候线程重置 n.key 标志位为 0
}

线程休眠是通过调用
futex
进入操作系统内核完成线程休眠的,关于
futex
的内容可以参考
这里

线程的 n.key 是休眠的标志位,当 n.key 不等于 0 时表示有线程在唤醒休眠线程,线程从休眠状态恢复到正常状态。唤醒休眠线程通过调用
notewakeup(&nmp.park)
函数实现:

func notewakeup(n *note) {
	old := atomic.Xchg(key32(&n.key), 1)
	if old != 0 {
		print("notewakeup - double wakeup (", old, ")\n")
		throw("notewakeup - double wakeup")
	}
	futexwakeup(key32(&n.key), 1)					// 调用 futexwakeup 唤醒休眠线程
}

首先,线程是怎么找到休眠线程的?线程通过全局空闲线程队列找到空闲的线程,并且将空闲线程的休眠标志位 m.park 传给
notewakeup
,最后调用
futexwakeup
唤醒休眠线程。

值得一提的是,唤醒的线程在唤醒之后还是会继续找可运行的 goroutine 直到找到:

func stopm() {
	...
	mPark()								// 如果 mPark 返回,表示线程被唤醒,开始正常工作
	acquirep(gp.m.nextp.ptr())			// 前面休眠前,线程已经和 P 解绑了。这里在给线程找一个 P 绑定
	gp.m.nextp = 0						// 线程已经绑定到 P 了,重置 nextp
}

基本这就是调度策略中很重要的一部分,线程如何找 goroutine。找到 goroutine 之后调用
gogo
执行该 goroutine。

3. 小结

本讲继续丰富了调度器的调度策略,下一讲,我们开始非 main goroutine 的介绍。