wenmo8 发布的文章

## 系列文章目录 ##

01现代计算机视觉入门之:什么是图片(6700字/25图)

02现代计算机视觉入门之:什么是视频

03现代计算机视觉入门之:什么是图片特征编码

04现代计算机视觉入门之:什么是图片分类

05现代计算机视觉入门之:什么是目标检测

06现代计算机视觉入门之:什么是图像分割

07现代计算机视觉入门之:什么是图片生成

08现代计算机视觉入门之:什么硬件加速

访问完整序列文章(更新中)

本文中的图片主要指光栅图片,而非矢量图片。光栅图片也是我们日常最常见的图片类型,智能手机拍照、监控摄像机、屏幕截图、社交网络图片等等都是光栅图片,它主要由成千上万个像素组成,也是计算机视觉(包括传统和现代)领域中图像算法处理的主要图片类型。

像素和分辨率

图片是一个有限二维平面,平面由纵横交叉的像素组成,类似一个二维表格,有明确的像素宽度(
W
)和像素高度(
H
),每个像素有自己的颜色,如白色、黑色、红色等等,密密麻麻的像素拼接成图片中各种各样的物体,它们有不同的形状、不同颜色。

图片中的像素由
bit
位来表示,如果每个像素由
1

bit
位表示的话,那么像素只能有
2
种颜色(比如
0
表示黑色,
1
表示白色),传统图像算法中有很多二值图每个像素由
1

bit
位表示,只有黑白两种颜色。同样的,如果每个像素由
2

bit
位表示的话,那么像素可以有
4
种颜色(
00

01

10

11
),以此类推。由此可见,
bit
位数越大,像素颜色种类越多。

图片的像素宽度
W*
像素高度
H
等于该图片包含的总像素数量,一般我们我们常说的
500
万像素、
1200
万像素就是用像素宽度
W*
像素高度
H
得到的结果,这个也称为图片分辨率。常见的分辨率有以下几种(各种场合定义可能有差别):

1

标清。
720
×
576
,约
40
万像素

2
、准高清(
720P
)。
1280
×
720
,约
90
万像素

3
、全高清(
1080P
)。
1920
×
1080
,约
200
万像素

4

2K

2048
×
1080
,约
200
万像素

5

4K

3840
×
2160
,约
800
万像素

6

8K

7680
×
4320
,约
3200
万像素

一般情况下,分辨率越大的图片,清晰度也越高。可以设想一下,用
1000
个像素拼成的目标肯定要比用
100
个像素拼成的目标细节更丰富、画面更清晰。注意这里的“一般情况”,图片清晰度跟成像设备也有很大关系,分辨率越高图片越清晰只是粗略说法。

(以下为图片格式版本)

原文地址:
Makefile文件中,两个$的变量变量$$Xxx 与一个$的变量 $Xxx的区别

Makefile 中的变量引用


Makefile
中,
$
符号用于变量替换,但它的使用方式有一些细微的区别:

单个
$
符号(
$Xxx

  • 用途
    :用于引用
    Makefile
    中定义的变量。

  • 用法

    $
    后面可以跟变量名,通常用括号或花括号括起来以明确变量名的边界。

  • 示例

    VAR = value
    all:
        echo $(VAR)
    

    在这个例子中,
    $(VAR)
    会被替换为
    value


$
符号(
$$Xxx

  • 用途
    :用于在 Makefile 中传递给 shell 的命令中引用 shell 变量。

  • 用法

    $$
    会被 make 解析为单个
    $
    ,从而在 shell 中使用。

  • 示例

    all:
        echo $$PATH
    

    在这个例子中,
    $$PATH
    会被 make 解析为
    $PATH
    ,从而在 shell 中输出环境变量 PATH 的值。

具体区别

  • $Xxx
    :用于 Makefile 变量替换。make 会在执行命令之前将其替换为变量的值。
  • $$Xxx
    :用于传递给 shell 的命令中引用 shell 变量。make 会将
    $$
    转换为
    $
    ,从而在 shell 中使用。

例子

假设你有一个 Makefile 如下:

VAR = Hello

all:
    # 使用 Makefile 变量
    echo $(VAR)
    # 使用 shell 变量
    echo $$USER
  • echo $(VAR)
    :make 会将
    $(VAR)
    替换为
    Hello
    ,然后执行
    echo Hello
  • echo $$USER
    :make 会将
    $$USER
    转换为
    $USER
    ,然后 shell 会替换
    $USER
    为当前用户的用户名。

总结

  • 使用
    $
    来引用 Makefile 中的变量。
  • 使用
    $$
    来引用 shell 中的变量,以确保在传递给 shell 时保留单个
    $
    符号。

前言

在 Kafka 中,消息偏移量是什么?是文件中的索引吗?又是如何通过偏移量快速定位消息的?本文将深入探讨这些问题,帮助你更好地理解 Kafka 的偏移量机制。

Kafka 的偏移量是什么?

Kafka 中的
偏移量
实际上是每条消息的
序号
。它为每条消息提供了一个唯一的标识。通过偏移量,消费者可以准确地找到并读取特定的消息。

偏移量在 Topic 中是唯一的吗?

答案是否定的,偏移量
仅在每个分区内是唯一的
。一个 Topic 可能有多个分区,每个分区的消息都有一个递增的偏移量。因此,Kafka 不需要对所有分区的消息偏移量进行全局同步。每个分区独立管理自己的偏移量,这不仅减少了复杂性,还降低了性能开销。

消息的偏移量由谁决定?客户端还是服务端?

偏移量是由
服务端
决定的。客户端无法了解其他客户端的偏移量情况,如果由客户端决定,则需要额外的同步成本来实现全局偏移量管理。所以,消息的偏移量是由 Kafka 服务端来维护的。

单调递增的序号是否会达到最大值?

理论上,
不会
。Kafka 使用 64 位长整型(
long
)作为偏移量,其最大值为
2^63 - 1
,即
9223372036854775807
。在正常的消息生产速率下,偏移量的增长将持续数百年,远远不需要担心达到最大值。

举个例子:假设每个分区每秒钟写入 1 万条消息,那么偏移量达到上限大约需要
29,200 年
。如此长的时间,几乎可以忽略不计。

Kafka 如何根据消息偏移量定位到文件中的位置?

既然我们知道 Kafka 的消息存储在文件中,那么问题就来了:如何根据“
序号
”定位到文件中的具体位置呢?

答案是
索引文件
。Kafka 为每个分区维护一个
索引文件
,该文件记录了每个消息的偏移量及其对应的存储位置。通过索引文件,Kafka 可以非常快速地定位到某个偏移量对应的消息存储位置。

实际上,索引文件通常记录的是消息的起始位置,而每条消息的结束位置可以通过消息头来确定。

联想:Socket 通道的数据处理

这种设计让我联想到
Socket 通道
中的数据处理。Socket 通道也是连续的字节流,服务端根据
消息头
来解析报文的起始和结束位置,从而确定数据的边界。服务端可以连续地从字节流中提取完整的报文进行处理。

相同之处:

  • 都是通过消息头来解析数据边界。

不同之处:

  • Socket 通道中的数据是即时消费的,而 Kafka 的数据存储在磁盘中,等待被消费。
  • Kafka 消费者并不是从头开始消费,而是从特定的偏移量位置开始消费,这需要额外的定位操作,即通过索引文件来定位消息的起始位置。

Kafka 如何高效读取多条消息?

假设我们要读取一批消息,比如 100 条。显然,Kafka 不会每次都查找索引文件并定位单独的消息偏移量。实际情况是,Kafka 会根据
起始消息的位置

最后一条消息的位置
,一次性将这一段消息批量读取出来。通过这种方式,Kafka 避免了多次查询索引文件,从而显著提高了读取效率。

一个分区的日志段文件有多个,具体如何实现?

Kafka 的日志段文件有最大值(log.segment.bytes)和最大存活时间(log.segment.ms)限制。

当文件达到最大大小或超过最大存活时间时,Kafka 会关闭旧文件并开启新文件。

那么,是每个分区只有一个索引文件吗?

答案是否定的。实际上,
每个日志段文件
(以
.log
为后缀)都有一个对应的
索引文件
(以
.index
为后缀)。

注意:
关闭日志段仅意味着它不再接受新的消息写入,但已经存储的数据仍然可以被消费者访问。

日志段在被删除之前都能被访问,至于何时删除详见保留配置 log.retention.*

如何知道要查找哪个文件?

每个日志段文件的文件名就是该段的
起始偏移量
。通过文件名,你可以知道消息存储在哪个文件中。接着,Kafka 通过该文件的索引文件来找到消息的具体位置。

  • 00000000000000000000.log(偏移量从 0 到 9999)
  • 00000000000000000000.index
  • 00000000000000010000.log(偏移量从 10000 到 19999)
  • 00000000000000010000.index

既然索引可以直接定位,Kafka为何还要限制日志段文件的大小?

因为内存映射。
Kafka 使用内存映射文件(Memory Mapped Files)来高效地处理日志的读取和写入。内存映射文件将文件映射到内存中,这样 Kafka 可以直接在内存中读取或写入数据,而无需频繁进行 I/O 操作,从而大大提高了性能。

但是,操作系统对内存映射文件的大小是有限制的,因此 Kafka 会限制单个日志段文件的大小。具体来说:

  • 操作系统的内存映射限制:
    每个操作系统对于内存映射的文件大小都有一定的限制。单个文件过大可能会超出这一限制,从而影响性能,甚至导致程序崩溃。

  • 内存占用问题:
    内存映射会将文件的部分内容加载到系统内存中,文件过大时,可能会导致系统内存占用过多,从而影响其他进程或系统的稳定性。尤其是在高负载环境下,操作系统可能无法为过大的文件提供足够的内存资源。

题外话:

1.正因如此,Kafka 通常建议将其单独部署在独立的服务器上,避免与其他应用争夺内存资源。这样能够确保 Kafka 的内存映射操作更加高效,并减少内存资源的竞争。

2.为了吞吐量,Kafka不会主动刷盘,刷盘依赖操作系统。刷新时间可能在几秒到几分钟之间。

写在前面

springboot
遵从约定大于配置的原则,极大程度的解决了配置繁琐的问题。在此基础上,又提供了spi机制,用
spring.factories
可以完成一个小组件的自动装配功能。

在一般业务场景,可能是不需要关心一个bean是如何被注册进spring容器的,只需要把需要注册进容器的bean声明为
@Component
即可,因为spring会自动扫描到这个Bean完成初始化并加载到spring上下文容器。

但是,如果加载Bean的过程中
部分Bean和Bean之间存在依赖关系
,也就是说
Bean A
的加载需要等待
Bean B
加载完成之后才能进行;或者你正在开发某个中间件需要完成自动装配时,你会声明自己的Configuration类,但是可能你面对的是好几个有互相依赖的Bean,如果不加以控制,这时候可能会报找不到依赖的错误。

而Spring框架在没有明确指定加载顺序的情况下是无法按照业务逻辑预期的顺序进行Bean加载,所以需要Spring框架提供能让开发人员显示地指定Bean加载顺序的能力。

几个误区

在正式说如何控制加载顺序之前,先说2个误区:

  • 在标注了
    @Configuration
    的类中,写在前面的@Bean一定会被先注册吗?

这个不存在的,spring在xml的时代,也不存在写在前面一定会被先加载的逻辑。因为xml不是渐进的加载,而是全部parse好,再进行依赖分析和注册。到了springboot中,只是省去了xml被parse成spring内部对象的这一过程,但是加载方式并没有大的改变。

  • 利用
    @Order
    这个标注就一定能进行加载顺序的控制吗?

严格的说,不是所有的Bean都可以通过
@Order
这个标注进行顺序的控制。因为把
@Order
这个标注加在普通的方法上或者类上是没有影响的,


@Order
能控制哪些bean的加载顺序呢?官方解释:

{@code @Order} defines the sort order for an annotated component. Since Spring 4.0, annotation-based ordering is supported for many kinds of components in Spring, even for collection injection where the order values of the target components are taken into account (either from their target class or from their {@code @Bean} method). While such order values may influence priorities at injection points, please be aware that they do not influence singleton startup order which is an orthogonal concern determined by dependency relationships and {@code @DependsOn} declarations (influencing a runtime-determined dependency graph).

最开始
@Order
注解用于切面的优先级指定;在 4.0 之后对它的功能进行了增强,支持集合的注入时,指定集合中 bean 的顺序,并且特别指出了,它对于单实例的 bean 之间的顺序,没有任何影响。目前用的比较多的有以下3点:

  • 控制AOP的类的加载顺序,也就是被
    @Aspect
    标注的类
  • 控制
    ApplicationListener
    实现类的加载顺序
  • 控制
    CommandLineRunner
    实现类的加载顺序

使用详情请看后文

如何控制

@Conditional 条件注解家族

  • @ConditionalOnClass:当类路径下存在指定的类时,配置类才会生效。
@Configuration
// 当类路径下存在指定的类时,配置类才会生效。
@ConditionalOnClass(name = "com.example.SomeClass")
public class MyConfiguration {
	// ...
}
  • @ConditionalOnMissingClass:当类路径下不存在指定的类时,配置类才会生效。
  • @ConditionalOnBean:当容器中存在指定的Bean时,配置类才会生效。
  • @ConditionalOnMissingBean:当容器中不存在指定的Bean时,配置类才会生效。

@DependsOn

@DependsOn
注解可以用来控制bean的创建顺序,该注解用于声明当前bean依赖于另外一个bean。所依赖的bean会被容器确保在当前bean实例化之前被实例化。

@DependsOn
的使用:

  • 直接或者间接标注在带有
    @Component
    注解的类上面;
  • 直接或者间接标注在带有
    @Bean
    注解的方法上面;
  • 使用
    @DependsOn
    注解到类层面仅仅在使用 component-scanning 方式时才有效,如果带有
    @DependsOn
    注解的类通过XML方式使用,该注解会被忽略,
    <bean depends-on="..."/>
    这种方式会生效。

示例:

@Configuration
public class BeanOrderConfiguration {
 
    @Bean
    @DependsOn("beanB")
    public BeanA beanA(){
        System.out.println("bean A init");
        return new BeanA();
    }
 
    @Bean
    public BeanB beanB(){
        System.out.println("bean B init");
        return new BeanB();
    }
 
    @Bean
    @DependsOn({"beanD","beanE"})
    public BeanC beanC(){
        System.out.println("bean C init");
        return new BeanC();
    }
 
    @Bean
    @DependsOn("beanE")
    public BeanD beanD(){
        System.out.println("bean D init");
        return new BeanD();
    }
 
    @Bean
    public BeanE beanE(){
        System.out.println("bean E init");
        return new BeanE();
    }
}

以上代码bean的加载顺序为:

bean B init
bean A init
bean E init
bean D init
bean C init

参数注入


@Bean
标注的方法上,如果传入了参数,springboot会自动会为这个参数在spring上下文里寻找这个类型的引用。并先初始化这个类的实例。

利用此特性,我们也可以控制bean的加载顺序。

示例:

@Bean
public BeanA beanA(BeanB demoB){
  System.out.println("bean A init");
  return new BeanA();
}
 
 
@Bean
public BeanB beanB(){
  System.out.println("bean B init");
  return new BeanB();
}

以上结果,beanB先于beanA被初始化加载。

需要注意的是,springboot会按类型去寻找。如果这个类型有多个实例被注册到spring上下文,那就需要加上
@Qualifier("Bean的名称")
来指定

利用bean的生命周期中的扩展点

在spring体系中,从容器到Bean实例化&初始化都是有生命周期的,并且提供了很多的扩展点,允许在这些步骤时进行逻辑的扩展。

这些可扩展点的加载顺序由spring自己控制,大多数是无法进行干预的。可以利用这一点,扩展spring的扩展点。在相应的扩展点加入自己的业务初始化代码。从来达到顺序的控制。

具体关于spring容器中大部分的可扩展点的分析,之前已经写了一篇文章详细介绍了:
Spring&SpringBoot中所有的扩展点

实现
Ordered/PriorityOrdered
接口/注解

在Spring中提供了如下的方法来进行Bean加载顺序的控制:

  • 实现
    Ordered/PriorityOrdered
    接口,重写order方法
  • 使用
    @Order/@Priority
    注解,
    @Order
    注解可以用于方法级别,而
    @Priority
    注解则不行;

针对
自定义的Bean
而言,上述的方式都可以实现Bean加载顺序的控制。无论是实现接口的方式还是使用注解的方式,
值设置的越小则优先级越高
,而通过实现PriorityOrdered接口或者使用@Priority注解的Bean时其加载优先级会
高于
实现Ordered接口或者使用@Order注解的Bean。

需要注意的是,使用上述方式只会改变实现同一接口Bean加载到集合
(比如List、Set等)
中的顺序(或者说优先级),但是这种方式并
不会影响到Spring应用上下文启动时不同Bean的初始化顺序
(startup order)。

  • 错误案例:以下案例代码是无法指定配置顺序的
@Component
@Order(1)
public class BeanA {
    // BeanA的定义
}

@Component
@Order(2)
public class BeanB {
    // BeanB的定义
}
  • 正确使用案例:

首先定义两个 Bean 实现同一个接口,并添加上@Order注解。

public interface IBean {
}

@Order(2)
@Component
public class AnoBean1 implements IBean {

    private String name = "ano order bean 1";

    public AnoBean1() {
        System.out.println(name);
    }
}

@Order(1)
@Component
public class AnoBean2 implements IBean {

    private String name = "ano order bean 2";

    public AnoBean2() {
        System.out.println(name);
    }
}

然后在一个测试 bean 中,注入
IBean
的列表,我们需要测试这个列表中的 Bean 的顺序是否和定义的
@Order
规则一致

@Component
public class AnoTestBean {

    public AnoTestBean(List<IBean> anoBeanList) {
        for (IBean bean : anoBeanList) {
            System.out.println("in ano testBean: " + bean.getClass().getName());
        }
    }
}

@AutoConfigureOrder

这个注解用来指定配置文件的加载顺序。但是在实际测试中发现,以下这样使用是不生效的:

@Configuration
@AutoConfigureOrder(2)
public class BeanOrderConfiguration1 {
    @Bean
    public BeanA beanA(){
        System.out.println("bean A init");
        return new BeanA();
    }
}
 
 
@Configuration
@AutoConfigureOrder(1)
public class BeanOrderConfiguration2 {
    @Bean
    public BeanB beanB(){
        System.out.println("bean B init");
        return new BeanB();
    }
}

无论你2个数字填多少,都不会改变其加载顺序结果。那这个
@AutoConfigureOrder
到底是如何使用的呢?

@AutoConfigureOrder适用于外部依赖的包中 AutoConfig 的顺序,而不能用来指定本包内的顺序。能被你工程内部scan到的包,都是内部的Configuration,而spring引入外部的Configuration,都是通过spring特有的spi文件:
spring.factories

换句话说,
@AutoConfigureOrder
能改变
spring.factories
中的
@Configuration
的顺序。

具体使用方式:

@Configuration
@AutoConfigureOrder(10)
public class BeanOrderConfiguration1 {
    @Bean
    public BeanA beanA(){
        System.out.println("bean A init");
        return new BeanA();
    }
}
 
@Configuration
@AutoConfigureOrder(1)
public class BeanOrderConfiguration2 {
    @Bean
    public BeanB beanB(){
        System.out.println("bean B init");
        return new BeanB();
    }
}

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.demo.BeanOrderConfiguration1,\
  com.example.demo.BeanOrderConfiguration2

总结

其实在工作中,我相信很多人碰到过复杂的依赖关系的bean加载,把这种不确定性交给spring去做,还不如我们自己去控制,这样在阅读代码的时候 ,也能轻易看出bean之间的依赖先后顺序。

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

一:背景

1. 讲故事

前面二篇我们聊到了
Thread.Sleep

Task.Result
场景下的线程注入逻辑,在线程饥饿的情况下注入速度都不是很理想,那怎么办呢?有没有更快的注入速度,这篇作为 动态注入 的终结篇,我个人总结如下两种方法,当然可能有更多的路子,知道的朋友可以在下面留言。

二:提高注入速度的两种方法

1. 降低GateThread的延迟时间

上一篇跟大家聊过 Result 默认情况下GateThread每秒会注入4个,底层逻辑是由
Blocking.MaxDelayMs=250ms
变量控制的,言外之意就是能不能减少这个变量的值呢?当然可以的,这里我们改成 100ms,参考代码如下:


        static void Main(string[] args)
        {
            AppContext.SetData("System.Threading.ThreadPool.Blocking.MaxDelayMs", 100);

            for (int i = 0; i < 10000; i++)
            {
                ThreadPool.QueueUserWorkItem((idx) =>
                {
                    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");

                    try
                    {
                        var client = new HttpClient();
                        var content = client.GetStringAsync("https://youtube.com").Result;
                        Console.WriteLine(content.Length);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }, i);
            }

            Console.ReadLine();
        }

现在我们还是用上一篇的方法在如下三个方法
HasBlockingAdjustmentDelayElapsed,PerformBlockingAdjustment,CreateWorkerThread
上埋日志断点,埋好之后运行程序观察。

从卦中的输出结果看,注入速度明显快了很多,判断阈值也从 250ms 变成了 100ms,每秒能注入
7~8
个线程,所以这是一个简单粗暴的提速方法。

2. 提高 MinThreads 的阈值

看过上两篇的朋友应该知道,我用过
喷涌而出
四个字来形容前 12个线程,这里的12是因为我的机器是 12 核,言外之意就是为什么要设置12呢?我能不能给它提升到 120,1200甚至更高的 12000 呢?这样线程的注入速度不是更快吗?有了这个想法赶紧上一段代码,参考如下:


        static void Main(string[] args)
        {
            ThreadPool.SetMinThreads(10000, 10);

            for (int i = 0; i < 10000; i++)
            {
                ThreadPool.QueueUserWorkItem((idx) =>
                {
                    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");

                    Thread.Sleep(int.MaxValue);
                }, i);
            }

            Console.ReadLine();
        }

从卦中看,直接秒了这个 10000 个任务,但不要忘了你的程序此时有1w个线程,如果是32bit程序大概率因为虚拟地址不足直接崩了,如果是 64bit 可能也会导致非常可观的内存占用。

有些人可能对底层逻辑感兴趣,我特意花了点时间绘了一张图来描述底层的运转逻辑。

之所以能快速的产生新线程,核心判断条件是
numProcessingWork <= counts.NumThreadsGoal
,我们设置的 MinThread=10000 最后给到了 NumThreadsGoal 字段,所以现有线程数不超过 10000 的话,就会不断的调用
CreateWorkThread
产生新的工作线程。

接下来我们再聊一下 SetMinThreads 这里面的坑吧,如果你将刚才的
ThreadPool.SetMinThreads(10000, 10);
改成
ThreadPool.SetMinThreads(10000, 10000);
的话,将不会有任何效果,截图如下:

为什么会出现这样的情况呢?这得从源码上找答案,参考代码如下:


        public class PortableThreadPool
        {
            private short _minThreads;
            private short _maxThreads;
            private short _legacy_maxIOCompletionThreads;
            private const short DefaultMaxWorkerThreadCount = MaxPossibleThreadCount;
            private const short MaxPossibleThreadCount = short.MaxValue;

            private PortableThreadPool()
            {
                _minThreads = HasForcedMinThreads ? ForcedMinWorkerThreads : (short)Environment.ProcessorCount;
                _maxThreads = HasForcedMaxThreads ? ForcedMaxWorkerThreads : DefaultMaxWorkerThreadCount;
                _legacy_maxIOCompletionThreads = 1000;
            }
        }

        public bool SetMinThreads(int workerThreads, int ioCompletionThreads)
        {
            if (workerThreads < 0 || ioCompletionThreads < 0)
            {
                return false;
            }
            bool flag = false;
            bool flag2 = false;
            this._threadAdjustmentLock.Acquire();
            if (workerThreads > (int)this._maxThreads)
            {
                return false;
            }
            if (ioCompletionThreads > (int)this._legacy_maxIOCompletionThreads)
            {
                return false;
            }
        }

从卦中代码可以看到 ioCompletionThreads 默认最大值为 1000,如果你设置的值大于 1000 的话,那前面的 workerThreads 等于白设置了。。。这就很无语了。。。 如果参数有误,你完全可以抛出一个异常来告诉我,,,而不是偷偷的掩埋错误信息,导致程序出现了我意想不到的行为。。。

为了凑篇幅,我再说一个有意思的参数 DebugBreakOnWorkerStarvation,它可以用来捕获
线程饥饿
的第一现场,底层逻辑是C#团队在代码里埋了一个钩子,参考如下:


        private static void GateThreadStart()
        {
            bool debuggerBreakOnWorkStarvation = AppContextConfigHelper.GetBooleanConfig("System.Threading.ThreadPool.DebugBreakOnWorkerStarvation", false);

            while (counts.NumProcessingWork < threadPoolInstance._maxThreads && counts.NumProcessingWork >= counts.NumThreadsGoal)
            {
                if (debuggerBreakOnWorkStarvation)
                {
                    Debugger.Break();
                }
            }
        }

这个
Debugger.Break();
发出的 int 3 信号,我们可以用 VS,DnSpy,WinDbg 这样的调试器去捕获,参考代码如下:


        static void Main(string[] args)
        {
            AppContext.SetSwitch("System.Threading.ThreadPool.DebugBreakOnWorkerStarvation", true);

            for (int i = 0; i < 10000; i++)
            {
                ThreadPool.QueueUserWorkItem((idx) =>
                {
                    Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")} -> {idx}: 这是耗时任务");

                    Thread.Sleep(int.MaxValue);
                }, i);
            }

            Console.ReadLine();
        }

三:总结

我们聊到了两种提升线程注入的方法,尤其是第二种让人意难平,面对上游洪水猛兽般的对线程池进行DDOS攻击,下游的线程不顾一切,倾家荡产的去承接,这是一种明知不可为而为之的悲壮之举。
图片名称