2024年4月

前言

前几天分享了一个
.NET MAUI开源免费的UI工具包 - Uranium
,然后技术群有不少同学问.NET MAUI是不是免费的?能做什么?今天特意写这篇文章来介绍一下.NET开源、免费(基于MIT License)的跨平台框架:MAUI。

.NET MAUI官方介绍

.NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML 创建本机移动和桌面应用。使用 .NET MAUI,可从单个共享代码库开发可在 Android、iOS、macOS 和 Windows 上运行的应用。

.NET MAUI 是一款开放源代码应用,是 Xamarin.Forms 的进化版,从移动场景扩展到了桌面场景,并从头重新生成了 UI 控件,以提高性能和可扩展性。如果以前使用过 Xamarin.Forms 来生成跨平台用户界面,那么你会注意到它与 .NET MAUI 有许多相似之处。但也有一些差异。通过使用 .NET MAUI,可使用单个项目创建多平台应用,但如果有必要,可以添加特定于平台的源代码和资源。.NET MAUI 的主要目的之一是使你能够在单个代码库中实现尽可能多的应用逻辑和 UI 布局。

.NET MAUI适合哪些用户

  • 基于 Visual Studio 中的单一共享代码库,使用 XAML 和 C# 编写跨平台应用。
  • 跨平台共享 UI 布局和设计。
  • 跨平台共享代码、测试和业务逻辑。

.NET MAUI支持的平台

  • Android 5.0 (API 21) 或更高版本。
  • iOS 11 或更高版本。
  • Mac Catalyst 的 macOS 10.15 或更高版本。
  • Windows 11 和 Windows 10 版本 1809 或更高版本,使用 Windows UI 库 (WinUI) 3。

.NET MAUI 如何运作

.NET MAUI 将 Android、iOS、macOS 和 Windows API 统一到单个 API 中,提供“编写一次就能在任何地方运行”的开发人员体验,同时还提供了对每个原生平台各个方面的深入访问。

.NET MAUI 提供了用于为移动和桌面应用生成 UI 的单一框架。下图显示了 .NET MAUI 应用体系结构的高级视图:

.NET MAUI项目源代码

.NET MAUI开发环境安装

注意:开发本机跨平台.NET MAUI应用需要 Visual Studio 2022 17.8 或更高版本,或者 Visual Studio 2022 for Mac 17.6。

安装.NET Multi-platform App UI 开发工作负荷:

.NET MAUI的优缺点

以下是个人见解,假如你有不一样的观点欢迎留言。

优点:

  • 跨平台性
    :使用.NET MAUI,开发人员可以编写一次代码,然后在多个平台上运行,包括iOS、Android、Windows 和 macOS。
  • 本地用户体验
    :.NET MAUI 提供了本地用户界面组件和控件,使开发人员能够创建具有原生外观和感觉的应用程序。
  • 单一项目
    :.NET MAUI 使用单一项目结构,使得在不同平台上开发和维护应用程序变得更加简单和高效。
  • 共享代码
    :开发人员可以在不同平台之间共享大部分代码,从而减少了开发时间和成本。
  • 现有技能
    :对于已经熟悉.NET 和 C# 的开发人员来说,学习和使用.NET MAUI 是相对容易的,因为它构建在熟悉的技术栈之上。

缺点:

  • 性能
    :尽管.NET MAUI 使用了许多性能优化技术,但跨平台开发仍可能导致性能上的一些损失,特别是对于需要高性能的应用程序而言。
  • 平台特定功能
    :某些平台特定的功能可能无法在.NET MAUI 中直接实现,开发人员可能需要编写特定平台的代码来满足这些需求。
  • 学习曲线
    :对于没有.NET 或 C#开发经验的开发者而言,学习.NET MAUI 可能需要一定的时间和精力。
  • 社区生态
    :虽然.NET社区很庞大,但与其他跨平台开发框架相比,.NET MAUI的社区支持可能相对较少,.NET MAUI社区成熟的解决方案也不多,对于开发者和公司而言这可能是他们技术选型时的一大考核点。

.NET Multi-Platform App UI文档

微软官方提供了详细的使用文档,文档中有详细的介绍和上手实践教程,大家可以放心食用。

https://learn.microsoft.com/zh-cn/dotnet/maui/?view=net-maui-8.0

Awesome .NET MAUI

https://github.com/jsuarezruiz/awesome-dotnet-maui

DotNetGuide技术社区交流群

  • DotNetGuide技术社区是一个面向.NET开发者的开源技术社区,旨在为开发者们提供全面的C#/.NET/.NET Core相关学习资料、技术分享和咨询、项目框架推荐、求职和招聘资讯、以及解决问题的平台。
  • 在DotNetGuide技术社区中,开发者们可以分享自己的技术文章、项目经验、学习心得、遇到的疑难技术问题以及解决方案,并且还有机会结识志同道合的开发者。
  • 我们致力于构建一个积极向上、和谐友善的.NET技术交流平台。无论您是初学者还是有丰富经验的开发者,我们都希望能为您提供更多的价值和成长机会。





欢迎加入DotNetGuide技术社区微信交流群

使用归一化盒过滤器对图像进行平滑处理

前言

在OpenCV中提供了一些函数将不同的线性滤波器应用于平滑图像:

  1. Normalized Box Filter 归一化盒过滤器
  2. Gaussian Filter 高斯滤波器
  3. Median Filter 中值滤波器
  4. Bilateral Filter 双边过滤器

其中归一化盒过滤器是最简单的,我们就从归一化盒过滤器开始我们的图像平滑之旅吧。

Normalized Box Filter原理介绍

为了执行平滑操作,我们将对图像应用滤镜。最常见的滤波器类型是线性滤波器,其中输出像素的值(即 g(i,j) )被确定为输入像素值的加权和(即 f(i+k,j+l) ):

image-20240420145604112

h(k,l) 称为核,无非是滤波器的系数。

Normalized Box Filter,也被称为归一化的盒形滤波器,是一种简单的图像滤波器,主要用于实现图像的平滑效果。

盒形滤波器的工作原理是将每个像素的值替换为其邻域内所有像素值的平均值。这种操作可以有效地消除图像中的噪声,但是它也会使图像变得模糊,因为它没有考虑像素之间的距离。

内核如下:

image-20240420145812584

动手做一个3*3核的查看效果

OpenCV中提供了函数,本来我们只要用那个函数就行了,但是自己动手会让自己对原理理解的更清楚,等自己真的理解了原理,再直接使用OpenCV提供的函数也不迟。

在维基百科上有3*3核的伪代码:

Box blur (image)
{
    set newImage to image;

    For x /*row*/, y/*column*/ on newImage do:
    {
        // Kernel would not fit!
        If x < 1 or y < 1 or x + 1 == width or y + 1 == height then:
            Continue;
        // Set P to the average of 9 pixels:
           X X X
           X P X
           X X X
        // Calculate average.
        Sum = image[x - 1, y + 1] + // Top left
              image[x + 0, y + 1] + // Top center
              image[x + 1, y + 1] + // Top right
              image[x - 1, y + 0] + // Mid left
              image[x + 0, y + 0] + // Current pixel
              image[x + 1, y + 0] + // Mid right
              image[x - 1, y - 1] + // Low left
              image[x + 0, y - 1] + // Low center
              image[x + 1, y - 1];  // Low right

        newImage[x, y] = Sum / 9;
    }

    Return newImage;
}

我们现在试着去用C#写一下:

 public Mat BoxFilter(Mat src)
 {
     Mat mat = src.Clone();
     for(int i = 1; i < mat.Width -1;i++)
     {
         for(int j = 1; j < mat.Height -1; j++)
         {                 
             int sumBlue = 0;
             int sumGreen = 0;
             int sumRed = 0;

             for (int n = -1; n <= 1; n++)
             {
                 for(int m = -1; m <= 1; m++)
                 {
                     Vec3b pixel = mat.At<Vec3b>(j+m, i+n);
                     sumBlue += pixel[0];
                     sumGreen += pixel[1];
                     sumRed += pixel[2];
                 }
             }

             int Blue = sumBlue / 9;
             int Green = sumGreen / 9;
             int Red = sumRed / 9;

             Vec3b pixel2 = new Vec3b();
             pixel2[0] = (byte)Blue;
             pixel2[1] = (byte)Green;
             pixel2[2] = (byte)Red;

             // 将修改后的像素值写回图像
             mat.Set(j, i, pixel2);

         }
     }
     return mat;
 }

现在看看我们写的与OpenCV中提供的效果对比:

 // 加载图片
 using (Mat src = new Mat("测试图片, ImreadModes.Color))
 {
     // 显示图片
     Cv2.ImShow("原图", src);
     Cv2.WaitKey(0);

     Mat dst = new Mat();
      
     dst = BoxFilter(src);

     // 显示图片
     Cv2.ImShow("MyBlur", dst);
     Cv2.WaitKey(0);

     Mat dst2 = new Mat();
     Cv2.Blur(src, dst2, new OpenCvSharp.Size(3, 3));
     
     // 显示图片
     Cv2.ImShow("OpenCVBlur", dst2);
     Cv2.WaitKey(0);

 }

效果如下所示:

image-20240420150729235

会发现实现了差不多的效果。

进行扩展适应其他大小的内核

现在我们学会了3*3大小的核的写法,能不能再进行扩展使之满足其他大小的核呢?

如果是5*5这样的我们也是可以进行扩展的。

用一个3*3的核去扫图像中的每一个像素,如果我们的锚点是在中心点,那么第一行第一列与最后一行最后一列上的像素就没法扫到。

什么是锚点?

在计算机图形学和图像处理中,"锚点"通常指的是一个参考点,用于确定图像或图形对象的位置、旋转和缩放。

那么现在如果是5*5的核去扫图像中的每一点,如果锚点是中心点的话,就是前两行前两列后两行后两列扫不到了。

根据这个规律我们就可以进行改写:

 public Mat BoxFilter2(Mat src,OpenCvSharp.Size size)
 {
     Mat mat = src.Clone();
     int x = (size.Width - 1) / 2;

     for (int i = x; i < mat.Width - x; i++)
     {
         for (int j = x; j < mat.Height - x; j++)
         {
             int sumBlue = 0;
             int sumGreen = 0;
             int sumRed = 0;

             for (int n = -x; n <= x; n++)
             {
                 for (int m = -x; m <= x; m++)
                 {
                     Vec3b pixel = mat.At<Vec3b>(j + m, i + n);
                     sumBlue += pixel[0];
                     sumGreen += pixel[1];
                     sumRed += pixel[2];
                 }
             }

             int Blue = sumBlue / (size.Width * size.Height);
             int Green = sumGreen / (size.Width * size.Height);
             int Red = sumRed / (size.Width * size.Height);

             Vec3b pixel2 = new Vec3b();
             pixel2[0] = (byte)Blue;
             pixel2[1] = (byte)Green;
             pixel2[2] = (byte)Red;

             // 将修改后的像素值写回图像
             mat.Set(j, i, pixel2);

         }
     }
     return mat;
 }

现在再看一下效果:

// 加载图片
 using (Mat src = new Mat("测试图片, ImreadModes.Color))
 {
     // 显示图片
     Cv2.ImShow("原图", src);
     Cv2.WaitKey(0);

     Mat dst = new Mat();
      
     dst = BoxFilter2(src, new OpenCvSharp.Size(5, 5));

     // 显示图片
     Cv2.ImShow("MyBlur", dst);
     Cv2.WaitKey(0);

     Mat dst2 = new Mat();
     Cv2.Blur(src, dst2, new OpenCvSharp.Size(3, 3));
     
     // 显示图片
     Cv2.ImShow("OpenCVBlur", dst2);
     Cv2.WaitKey(0);

 }

实现的效果如下所示:

image-20240420151948186

OpenCV是非常强大的,它考虑了很多情况,如何核是5 * 7的呢?我们这样写就又要修改了。如何锚点不是中心点呢?我们这样写就不行了。我们这样写的目的只是为了让我们更熟悉一下原理,后面需要用到的时候直接使用OpenCV提供的函数就好了。

使用OpenCV函数

经过前面自己的练习,对于原理也更加了解了,以后就可以直接使用OpenCV提供的函数了。

先来介绍一下OpenCvSharp中的
Cv2.Blur
函数:

 public static void Blur(InputArray src, OutputArray dst, Size ksize, Point? anchor = null, BorderTypes borderType = BorderTypes.Reflect101)
参数名 类型 含义
src InputArray 输入的图像
dst OutputArray 与输入图像同样大小同样类型的输出图像
ksize Size 平滑内核大小
anchor Point 锚点,默认在内核中心
borderType BorderTypes 用于外推图像外部像素的边界模式

平常使用时一般这样使用就可以了:

 Cv2.Blur(src, dst, new OpenCvSharp.Size(5, 5));

再来介绍一下OpenCvSharp中的
Cv2.BoxFilter
函数:

 public static void BoxFilter(InputArray src, OutputArray dst, MatType ddepth, Size ksize, Point? anchor = null, bool normalize = true, BorderTypes borderType = BorderTypes.Reflect101)
参数名 类型 含义
src InputArray 输入的图像
dst OutputArray 与输入图像同样大小同样类型的输出图像
ddepth MatType 输出图像的深度 ,
-1
表示输出图像和输入图像有相同的深度。
ksize Size 平滑内核大小
anchor Point 锚点,默认在内核中心
normalize bool 表示是否对滤波器核进行归一化
borderType BorderTypes 用于外推图像外部像素的边界模式

平常使用时一般这样使用就可以了:

 Cv2.BoxFilter(src, dst, -1,new OpenCvSharp.Size(5, 5));

总结

本文向大家介绍了
Normalized Box Filter 归一化盒过滤器
的基本原理,以及在OpenCVSharp中如何使用,希望对你有所帮助。

参考

1、
OpenCV: Smoothing Images

2、
框模糊 - 维基百科,自由的百科全书 --- Box blur - Wikipedia

接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。


《深度解读《深度探索C++对象模型》之C++对象的内存布局》
这篇文章中已经详细分析过C++的对象在经过封装后,在各种情况下的内存布局以及增加的成本。本文将进一步分析C++对象在封装后,数据成员的存取的实现手段及访问的效率。在这里先抛出一个问题,然后带着问题来一步一步分析,如下面的代码:

class Point {};
Point p;
Point *pp = &p;
p.x = 0;
pp->x = 0;

上面的代码中,对数据成员
x
的存取成本是什么?通过对象
p
来存取成员x和通过对象的指针
pp
来存取成员x的效率存在差异吗?要搞清楚这个问题,得看具体的
Point
类的定义以及成员x的声明方式。
Point
类可能是一个独立的类(也就是没有从其他类继承而来),也可能是一个单一继承或者多重继承而来的类,甚至也有可能它的继承父类中有一个是虚拟基类(virtual base class),成员x的声明可能是静态的或者是非静态的。下面的几节将根据不同的情况来一一分析。

类对象的数据成员的存取效率分析系列篇幅比较长,所以根据不同的类的定义划分为几种情形来分析,这篇先来分析静态数据成员的情况。

静态数据成员在编译器里的实现

在前面的文章中说过,类中的静态数据成员是跟类相关的,而非跟具体的对象有关,它存储在对象之外,具体的存储位置是在程序中的数据段中。它其实跟一个全局变量没什么区别,在编译期间编译器就已经确定好了它的存储位置,所以能够确定它的地址。看一下下面的代码:

#include <cstdio>

int global_val = 1;

class Base {
public:
    int b1;
    static int s1;
};
int Base::s1 = 1;

int main() {
    static int static_var = 1;
    int local_var = 1;
    Base b;
    printf("&global_val = %p\n", &global_val);
    printf("&static_var = %p\n", &static_var);
    printf("&local_var = %p\n", &local_var);
    printf("&b.b1 = %p\n", &b.b1);
    printf("&b.s1 = %p\n", &b.s1);

    return 0;
}

程序输出的结果:

&global_val = 0x102d74000
&static_var = 0x102d74008
&local_var = 0x16d0933f8
&b.b1 = 0x16d0933f4
&b.s1 = 0x102d74004

可以看到全局变量
global_val
和局部静态变量
static_var
以及类中的静态数据成员
s1
的地址是顺序且紧密排列在一起的,而且跟其他的两个局部变量的地址相差较大,说明这几个都是一起存储在程序的数据段中的。类中的非静态数据成员
b1
跟局部变量
local_var
一样,是存放在栈中的。

可以进一步看看生成的汇编代码,看一下是怎么存取静态数据成员的,下面节选部分的汇编代码:

main:                            # @main
    # 略...
    lea     rdi, [rip + .L.str]
    lea     rsi, [rip + global_val]
    mov     al, 0
    call    printf@PLT
    lea     rdi, [rip + .L.str.1]
    lea     rsi, [rip + main::static_var]
    mov     al, 0
    call    printf@PLT
  	# 略...
    lea     rdi, [rip + .L.str.4]
    lea     rsi, [rip + Base::s1]
    mov     al, 0
    call    printf@PLT
    # 略...
    ret
global_val:
    .long   1        # 0x1

Base::s1:
    .long   1        # 0x1

main::static_var:
    .long   1        # 0x1

从汇编代码中看到,
global_val

Base::s1

main::static_var
是定义在数据段中的,在代码中直接使用它们的地址,如:

lea rsi, [rip + Base::s1]

则是将
Base::s1
的地址加载到rsi寄存器中,作为参数传递给
printf
函数。这也证明了它跟全局变量,普通的静态变量是没有区别的。
结论就是,类中的静态数据成员的存取方式是直接通过一个具体的地址来访问的,跟全局变量毫无区别,所以效率上也跟访问一个全局变量一样。

通过不同方式存取静态数据成员的效率差异

访问类的静态数据成员可以通过类名来访问,如
Base::s1
,也可以通过对象来访问,如
b.s1
,甚至是通过指针来访问,如
pb->s1
。那么这几种访问方式有什么差别?或者说是否有效率上的损失?其实这几种访问方式本质上没有任何差别,编译器会转换成如
Base::s1
一样的方式,后面的两种方式只是语法上的方便而已,看一下汇编代码就一目了然。把上面的例子多余的代码删除掉,只留下
Base
类,然后
main
函数中增加几行打印,如下:

Base b;
Base *pb = &b;
printf("&Base::s1 = %p\n", &Base::s1);
printf("&b.s1 = %p\n", &b.s1);
printf("&pb->s1 = %p\n", &pb->s1);

输出的结果当然是同一个地址了,下面是节选的汇编代码:

lea     rdi, [rip + .L.str]
lea     rsi, [rip + Base::s1]
mov     al, 0
call    printf@PLT
lea     rdi, [rip + .L.str.1]
lea     rsi, [rip + Base::s1]
mov     al, 0
call    printf@PLT
lea     rdi, [rip + .L.str.2]
lea     rsi, [rip + Base::s1]
mov     al, 0
call    printf@PLT

可以看到C++中的几行不同的访问方式在汇编代码中都转换为同样的代码:

lea rsi, [rip + Base::s1]

继承而来的静态数据成员的存取分析

我们已经知道类中的静态数据成员是跟对象无关的,所有的对象都共享同一个静态数据成员。但是如果继承而来的静态数据成员又是怎样的呢?假如定义一个
Derived
类,它是
Base
类的派生类,那么静态数据成员
s1
的情况又是如何?其实无论继承多少次,静态数据成员都只有一份,无论是
Derived
类还是
Base
类,它们都共享同一个静态数据成员
s1
,可以通过下面的例子来验证一下:

#include <cstdio>

class Base {
public:
    int b1;
    static int s1;
};
int Base::s1 = 1;

class Derived: public Base {};

int main() {
    Derived d;
    printf("&d.s1 = %p\n", &d.s1);
    printf("d.s1 = %d\n", d.s1);
    d.s1 = 2;

    Base b;
    printf("&b.s1 = %p\n", &b.s1);
    printf("b.s1 = %d\n", b.s1);

    return 0;
}

程序输出的结果:

&d.s1 = 0x10028c000
d.s1 = 1
&b.s1 = 0x10028c000
b.s1 = 2

可以看到通过
Derived
类的对象
d

Base
类的对象
b
访问到的都是同一个地址,通过对象
d
修改
s1
后,通过对象
b
可以看到修改后的值。

如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享并关注,以便在内容更新时直接向您推送。

gRPC入门学习之旅(六)

3.6、创建gRPC的桌面应用客户端

1. 在Visual Studio 2022菜单栏上选择“文件—》新建—》项目”。

或者在Visual Studio 2022的解决方案资源管理器中,使用鼠标右键单击“解决方案‘Demo.GrpcService’”,在弹出的快捷菜单中选择“添加—》新建项目”。如下图。

2. Visual Studio 2022弹出的“创建新项目”的对话框中做如下选择。如下图。

  • 在最左边的下拉框中,选择 “C# ,如下图中1处
  • 在中间的下拉框中,选择 “所有平台”,如下图2处。
  • 在最右边的下拉框中,选择“桌面”,如下图3处。
  • 在下图中4处,选择“WPF应用程序”模板,点击“下一步”按钮。

4.在弹出的“配置新项目”的对话框中,在“项目名称”输入框中,输入“Demo.Grpc.Client”。然后使用鼠标点击“下一步”按钮。

5. 在弹出的“其他信息”的对话框,在“框架”下拉框中,选择“NET 7.0(标准期限支持)”。其他值选择默认值即可。然后使用鼠标点击“创建”按钮。项目创建成功,如下图。

6.在解决方案资源管理器中——>在项目WpfGridDemo.NET7中的依赖项上鼠标右键单击——>弹出一个菜单,选中“管理NuGet程序包”,如下图。

7. 在打开的NuGet包管理界面的中选择“浏览”标签页,在搜索框中输入“google”,找到最新版本google.protobuf,点击安装。如下图。

8.  Visual Studio 2022 开始安装Google.Protobuf,会弹出安装确认界面,点击“OK”按钮。

9. 在打开的NuGet包管理界面的中“浏览”标签页的搜索框中输入“grpc”,然后依次安装以下三个包。

Grpc.Net.Client

Grpc.Tools

Grpc.Net.ClientFactory

10. 在以上四个包全部安装完成之后,NuGet包管理界面的中选择“已安装”标签页,会看到刚才安装的四个包,如下图。

大家好,我是程序员陶朱公。

前言

前两天解决了一个线上
消息队列堆积
事故,在这里做一个复盘与总结,希望我解决问题的方式、方法、手段对你将来遇到类似的事情有一定的帮助与启发。

背景

前两天,有业务方反馈,他们日常处理的工单数据变少了,希望开发同学去排查一下原因。

这里简单的画下我们系统的一个逻辑架构图:

逻辑架构图

在消费者应用前,有一个专门拉取邮件的服务,拉取邮件收件箱的邮件后,会将邮件存储在本地数据库作为原始邮件进行保存,保存成功后,会将保存下来的messageId,发送到MQ中去,供下游消费者应用消费。

像之前功能正常,消费者应用会从MQ中解析拿到MessageId,然后
解析
这封邮件,生成对应的工单。但一旦消费者应用由于各种原因消费不动了之后,就会造成队列消息的堆积。

分析

既然说到了堆积,我们就需要了解一下,堆积的整体情况:

消息队列堆积图

可能有小伙伴觉得说,这个对接的数字不是很大呀,业务方这么敏感吗。这里我想简单表达一下,敏感度这个事情主要还是看具体业务,case by case来分析。像我们的业务,相关操作人员每天都要及时处理邮箱里客户反馈的内容,需要及时与客户暂开沟通,而且每一天对这个工单的处理量,也是他们的考核指标,如果由于系统原因,导致他们不能及时处理本该生成的工单,影响面很大。

OK既然是消息对接,如果这方面实战经验比较欠缺,但八股文背的苦瓜烂熟的小伙伴,第一意识可能从脑海中会蹦出来一个认知,是不是消费者应用自身性能问题,比如CPU100%啊、FULLGC啊等造成消费者线程没有资源进行调度、处理。

这个认知肯定是没错的,我第一时间,也是照这这个思路去看了一下线上监控大图,观察后发现,上述关心的一些列指标在线上都是正常不过的,CPU负载不高,内存够用,没有FULLGC等情况发生。

那还能是什么原因造成堆积了呢?如果是你,此刻,你会怎么继续排查呢,欢迎大家把各自的想法或思路,在评论区输出参与讨论。

这里分享一下我的排查思路。

其实也是很容易就联想到的即一定是固定数量的
消费者线程
不明所以被Hang死了,而且是出不来的那种,导致没办法继续处理队列里的数据。

消费者线程配置参数

上图是我们配置的消费者线程参数,用了两个线程来消费队列里的数据。如果两个线程都Hang死了,那绝B是会造成队列堆积的。

所以,我们有必要去堆栈里面,看看这两个消费者线程的整体情况。

怎么看呢?

有很多手段,比如jstack 线程ID或借助一些开源工具,比如阿里的Arthas。可以用Arthas列出所有线程

然后找出你的消费者线程ID,最后执行thread id就可以查看线程的堆栈情况。

当然如果你们的PASS平台或一些云应用,查看对接的线程,会更方便,像阿里云平台上就可以直接查看:

点击堆栈查看按钮后,我们按下里面的内容长啥样:

消费者线程堆栈内部情况

我截图的内容,正式消费者线程内部情况的表现。很明显,我框的类的方法有问题,导致了线程一直Hang在了那里出不来。后来定位到了相关代码,发现内部其实做了一件事情,就是将一个URL地址,一个在线的PDF地址,然后在pdfToPng方法内部,会流的形式解析生成本地图片进行保存,遇到一些特别大的PDF源文件,就长时间出不来了。

而且我也看了原来的代码,这代码URL链接出,居然都没设置超时时间这一个选项,如果遇到长时间的这种IO操作,那消费者线程不Hang死才怪。

解决方案

既然知道了问题的症结,那就不难处理,就像上图所示,在pdfTopng方法内部,加上超时时间设置,后来用几天时间观察了一下线上的数据,发现还是有效的。

结论

今天跟大家分享了一下,我前端时间遇到的一个线上消息队列堆积的事故,也跟大家详细阐述了,我遇到问题一步一步是如何分析、排查问题的,最后也给出了我修复问题的解决方案,希望大家喜欢。

写到最后


感谢您一路陪伴着我,探索编程的奇妙世界。如果您对
程序员职场进阶窍门、编程技巧和计算机原理
等充满兴趣,那么不要错过未来我为大家奉上的精彩内容!点击
关注
,让您的程序员之旅更加丰富多彩,我们一同成长,一同前行!