之前一个项目涉及到针对海量(千万级)实时变化数据的计算,由于对性能要求非常高,我们不得不将参与计算的数据存放到内存中,并通过检测数据存储的变化实时更新内存的数据。存量的数据几乎耗用了上百G的内存,再加上它们在每个时刻都在不断地变化,所以每时每刻都无数的对象被创建出来(添加+修改),同时无数现有的对象被“废弃”(删除+修改)。这种情况针对GC的压力可想而知,所以每当进行一次2代GC的时候,计算的耗时总会出现“抖动”。为了解决这类问题,几天前尝试着创建了一个名为NativeBuffering的框架。目前这个框架远未成熟,而且是一种“时间换空间”的解决方案,虽然彻底解决了内存分配的问题,但是以牺牲数据读取性能为代价的。这篇文章只是简单介绍一下NativeBuffering的设计原理和用法,并顺便收集一下大家的建议。[本文演示源代码从这里下载]

一、让对象映射一段连续的内存

二、Unmanaged类型

三、BufferedBinary类型

四、BufferedString类型

一、让对象映射一段连续的内存

针对需要高性能的互联网应用来说,GC针对性能的影响是不得不考虑的,减少GC影响最根本的解决方案就是“不需要GC”。如果一个对象占据的内存是“连续的”,并且承载该对象的字节数是可知的,那么我们就可以使用一个预先创建的字节数组来存储数据对象。我们进一步采用“对象池”的方式来管理这些字节数组,那么就能实现真正意义上的“零分配”,自然也就不会带来任何的GC压力。不仅如此,连续的内存布局还能充分地利用各级缓存,对提高性能来说是一个加分项。如果从序列化/发序列话角度来说,这样的实现直接省去了反序列化的过程。

但是我们知道在托管环境这一前提是不成立的,只有值类型的对象映射一片连续的内存。对于引用类型的对象来说,只有值类型的字段将自身的值存储在该对象所在的内存区域,对于引用类型的字段来说,存储的仅仅目标对象的地址而已,所以“让对象映射一段连续内存”是没法做到的。但是基元类型和结构体默认采用这样的内存布局,所以我们可以采用“非托管或者Unsafe”的方式将它们映射到我们构建的一段字节序列。对于一个只包含基元类型和结构体成员的“复合”类型来说,对应实例的所有数据成员可以存储到一段连续的字节序列中。

既然如此,我们就可以设计这样一种数据类型:它不在使用“字段”来定义其数据成员,而将所有的数据成员转换成一段字节序列。我们为每个成员定义一个属性将数据读出来,这相当于实现了“将对象映射为一段连续内存”的目标。以此类推,任何一个数据类型其实都可以通过这样的策略实现”连续内存布局“。

正如上面提到过的,这是一种典型的”时间换空间“的解决方案,所以NativeBuffering的一个目标就是尽可能地提高读取数据成员的性能,其中一个主要的途径就是Buffer存储的字节就是数据类型原生(Native)的表现形式。也就是说原生的数据类型采用怎样的内存布局,NativeBuffering就采用怎样的布局,这也是NativeBuffering名称的由来。在这一根本前提下,NativeBuffering针对单一数据的读取并没有性能损失,因为中间不存在任何Marshal的过程,针对影响读取性能的因素是需要额外计算待读取数据在Buffer中的偏移量。

也正是为了保证“与数据类型的Native形式保持一直”,NativeBuffering对于数据类型做了限制。总地来说,NativeBuffering只支持Unmanaged、BufferedBinary和BufferedString三种基本类型。NativeBuffering将定义的数据类型称为BufferedMessage,除了上述三种基本的数据类型,BufferedMessage的数据类型还可以是另一个BufferedMessage类型,以及基于这四种类型的集合和字典。下面的内容主要从“内存布局”的角度介绍上述三种基本的数据类型,同时通过实例演示其基本用法。

二、Unmanaged类型

顾名思义,Unmanaged类型可以理解为不涉及托管对象引用的值类型(可以参与我们的文章《.NET的基元类型包括哪些?Unmanaged和Blittable类型又是什么?》),如下的类型属于Unmanaged 类型的范畴。由于这样的类型在托管和非托管环境的内存布局是完全一致的,所以可以使用静态类型Unsafe从指定的地址指针将值直接读取出来。

  • 14种基元类型+Decimal(decimal)

  • 枚举类型

  • 指针类型(比如int*, long*)

  • 只包含Unmanaged类型字段的结构体

我们创建一个简单的控制台程序演示NativeBuffering的基本用法。NativeBuffering除了提供同名的NuGet包外,还提供了一个名为NativeBuffering.Generator的NuGet包,后者以Source Generator的形式根据“原类型”生成对应的BufferedMessage类型,并生成用来计算字节数量和输出字节内容的代码。我们定义了如下这个Entity类作为“源类型”(上面标注了BufferedMessageSourceAttribute特性),由于我们还需要为该类型生成一些额外成员,所以必须将其定义成partial类。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
} public readonly record struct UnmanagedStruct(int X, double Y);

如上面的代码片段所示,Entity具有Foo和Bar两个数据成员,类型分别为long(Int64)和UnmanagedStruct ,它们都是Unmanaged类型。如果将这个Entity转换成对应的BufferedMessage,承载字节将具有如下的结构。任何一个BufferedMessage对象承载的字节都存储在一个预先创建的字节数组中。如果它具有N个成员(被称为字段),前N * 4个字节用来存储一个整数指向对应成员的起始位置(在字节数组中的索引),后续的字节依次存储每个数据成员。在读取某个成员的时候,先根据字段索引读取目标内容在缓冲区中的位置,然后根据类型读取对应的值。

有人可能说,既然值类型的长度都是固定的,完全可以按照下图(上)所示的方式直接以“平铺”的方式存储每个字段的值,然后根据数据类型确定具体字段的初始位置。实际上最初我也是这么设计的,但是如果考虑内存地址对齐下图(下),针对字段初始位置的计算就比较麻烦。内存对齐目前尚未实现,实现了之后相信对性能有较大的提升。

具有上述结构的字节不可能手工生成,所以我们采用了Source Generator的方式。安装的Source Generator(NativeBuffering.Generator)将会帮助我们生成如下图所示的两个.cs文件。

Entity.g.cs补上上了Entity这个partial类余下的部分。如下面的代码片段所示,自动生成的代码让这个类实现了IBufferedObjectSource接口,实现的CalculateSize用于计算生成的字节数,而具体的字节输出则实现在Writer方法中。

public partial class Entity : IBufferedObjectSource
{
public int CalculateSize()
{
var size = 0;
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Foo);
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Bar);
return size;
}
public void Write(BufferedObjectWriteContext context)
{
using var scope = new BufferedObjectWriteContextScope(context);
scope.WriteUnmanagedField(Foo);
scope.WriteUnmanagedField(Bar);
}
}

NativeBuffering.Generator还帮助我们自动生成对应的EntityBufferedMessage 类型。如下面的代码片段所示,为了尽可能节省内存,我们将其定义为只读的结构体,并实现了IReadOnlyBufferedObject<EntityBufferedMessage>
接口。EntityBufferedMessage是对一个NativeBuffer对象的封装,NativeBuffer是一个核心类型,用来表示从指定位置开始的一段缓冲区。它Bytes属性表示作为缓存区的字节数组,Start属性表示起始地址的指针。至于两个属性Foo和Bar返回的值,分别调用相应的方法从这个NativeBuffer对象中读取出来。

public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public System.Int64 Foo => Buffer.ReadUnmanagedField<System.Int64>(0);
public ref readonly UnmanagedStruct Bar => ref Buffer.ReadUnmanagedFieldAsRef<UnmanagedStruct>(1);
} public interface IReadOnlyBufferedObject<T> where T: IReadOnlyBufferedObject<T>
{
static abstract T Parse(NativeBuffer buffer);
} public unsafe readonly struct NativeBuffer
{
public byte[] Bytes { get; }
public void* Start { get; }
...
}

由于UnmanagedStruct 是一个自定义的结构体,我们知道值类型赋值采用“拷贝”的方式。如果这个结构体包含过多的成员,可能会因为拷贝的字节过多而带来性能问题,为此我直接返回这个结构体的引用。由于整个BufferedMessage 是只读的,所以返回的引用也是只读的。为了方便BufferedMessage对象的创建,我们为实现的IReadOnlyBufferedObject<EntityBufferedMessage>接口定义了一个静态方法Parse。如下的程序验证了EntityBufferedMessage 与原始Entity类的“等效性”。

using NativeBuffering;
using System.Diagnostics; var entity = new Entity
{
Foo = 123,
Bar = new UnmanangedStruct(789, 3.14)
}; var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context);
File.WriteAllBytes(".data", bytes); EntityBufferedMessage bufferedMessage;
BufferOwner? bufferOwner = null; try
{
using (var fs = new FileStream(".data", FileMode.Open))
{
var byteCount = (int)fs.Length;
bufferOwner = BufferPool.Rent(byteCount);
fs.Read(bufferOwner.Bytes, 0, byteCount);
} bufferedMessage = BufferedMessage.Create<EntityBufferedMessage>(ref bufferOwner);
Debug.Assert(bufferedMessage.Foo == 123);
Debug.Assert(bufferedMessage.Bar.X == 789);
Debug.Assert(bufferedMessage.Bar.Y == 3.14);
}
finally
{
bufferOwner?.Dispose();
}

整个演示程序分两个部分,第一个部分演示了如何将一个Entity对象转换成我们需要的字节,并持久化到一个文件中。第二部分演示如何读取字节并生成对应的EntityBufferedMessage,这里我们使用了“缓冲池”,所以针对EntityBufferedMessage的创建不会涉及内存分配。我们没有直接使用ArrayPool<byte>,因为数据成员根据指针读取,我们需要保证整个缓冲区不会因GC的“压缩”而移动位置,通过BufferPool实现的内存池将字节数组存储在POH中,位置永远不会改变。

三、BufferedBinary类型

BufferedBinary 是NativeBuffering支持的第二种基本类型,它表示一个长度确定的字节序列。和Unmanaged类型不同,这是一种长度可变的类型,所以我们使用前置的4字节以整数的形式表示字节长度。BufferedBinary 被定义成如下这样一个结构体,它同样实现了IReadOnlyBufferedObject<BufferedBinary>接口。我们可以调用AsSpan方法以ReadOnlySpan<byte>的形式字节序列。

public unsafe readonly struct BufferedBinary : IReadOnlyBufferedObject<BufferedBinary>
{
public BufferedBinary(NativeBuffer buffer) => Buffer = buffer;
public NativeBuffer Buffer { get; }
public int Length => Unsafe.Read<int>(Buffer.Start);
public ReadOnlySpan<byte> AsSpan() => new(Buffer.GetPointerByOffset(sizeof(int)), Length);
public static BufferedBinary Parse(NativeBuffer buffer) => new(buffer);
}

为了演示字节序列在NativeBuffering中的应用,我们为Entity类添加了如下这个字节数组类型的属性Baz。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
public byte[] Baz { get; set; }
}

新的Entity对应的BufferedMessage将具有如下的内存布局。

Entity类的定义一旦放生改变,NativeBuffering.Generator将自动修正生成的两个.cs文件的内容。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
public byte[] Baz { get; set; }
} public partial class Entity : IBufferedObjectSource
{
public int CalculateSize()
{
var size = 0;
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Foo);
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Bar);
size += NativeBuffering.Utilities.CalculateBinaryFieldSize(Baz);
return size;
}
public void Write(BufferedObjectWriteContext context)
{
using var scope = new BufferedObjectWriteContextScope(context);
scope.WriteUnmanagedField(Foo);
scope.WriteUnmanagedField(Bar);
scope.WriteBinaryField(Baz);
}
} public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public System.Int64 Foo => Buffer.ReadUnmanagedField<System.Int64>(0);
public ref UnmanagedStruct Bar => ref Buffer.ReadUnmanagedFieldAsRef<UnmanagedStruct>(1);
public BufferedBinary Baz => Buffer.ReadBufferedObjectField<BufferedBinary>(2);
}

在如下所示的演示程序中,通过Entity的Baz属性设置的字节数组,在生成的EntityBufferedMessage对象中,同样可以利用同名的属性读取出来。

using NativeBuffering;
using System.Diagnostics; var entity = new Entity
{
Foo = 123,
Bar = new UnmanangedStruct(789, 3.14),
Baz = new byte[] { 1, 2, 3 }
}; var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context);
File.WriteAllBytes(".data", bytes); EntityBufferedMessage bufferedMessage;
BufferOwner? bufferOwner = null; try
{
using (var fs = new FileStream(".data", FileMode.Open))
{
var byteCount = (int)fs.Length;
bufferOwner = BufferPool.Rent(byteCount);
fs.Read(bufferOwner.Bytes, 0, byteCount);
} bufferedMessage = BufferedMessage.Create<EntityBufferedMessage>(ref bufferOwner);
Debug.Assert(bufferedMessage.Foo == 123);
Debug.Assert(bufferedMessage.Bar.X == 789);
Debug.Assert(bufferedMessage.Bar.Y == 3.14); Debug.Assert(bufferedMessage.Baz.Length == 3);
var byteSpan = bufferedMessage.Baz.AsSpan();
Debug.Assert(byteSpan[0] == 1);
Debug.Assert(byteSpan[1] == 2);
Debug.Assert(byteSpan[2] == 3);
}
finally
{
bufferOwner?.Dispose();
}

四、BufferedString类型

字符串同样是一个“长度可变”数据类型。如果将一个字符串转换成一个一段连续的字节呢?可能很多人会说,那还不容易,将其编码不久可以了吗?确实没错,但是如何将编码转换成字符串呢?解码吗?不要忘了我们的目标是“创建一个完全无内存分配”的数据类型。当我们解码字节将其“还原”一个字符串时,实际上CLR会创建一个String类型(引用类型)的实例,并将指定的字节转换成标准的字符字节(采用UTF-16编码)并将其拷贝到实例所在的内存区域。

要达到我们“无分配”的目标,字符串转换的字节序列必须与这个String实例在内存中的内容完全一致。此时你不了解字符串对象在.NET中的内存布局,可以参阅我的另一篇文章《你知道.NET的字符串在内存中是如何存储的吗?》。总的来说,一个字符串实例由ObjHeader+TypeHandle+Length+Encoded Characters4部分组成。我们还需要知道整个字节序列的长度,所以我们还需要前置的4个字节。

字符串在NativeBuffering通过如下这个名为BufferedString的结构体表示,它同样实现了IReadOnlyBufferedObject<BufferedString>接口。BufferedString可以通过AsString方法转换成String类型,该方法不会带来任何的内存分配。AsString方法用在针对String的隐式类型转换操作符上,所以在任何使用到String类型的地方都可以直接使用BufferedString类型。

public unsafe readonly struct BufferedString : IReadOnlyBufferedObject<BufferedString>
{
private readonly void* _start;
public BufferedString(NativeBuffer buffer) => _start = buffer.Start;
public BufferedString(void* start)=> _start = start;
public static BufferedString Parse(NativeBuffer buffer) => new(buffer);
public static BufferedString Parse(void* start) => new(start);
public static int CalculateSize(void* start) => Unsafe.Read<int>(start);
public string AsString()
{
string v = default!;
Unsafe.Write(Unsafe.AsPointer(ref v), new IntPtr(Unsafe.Add<byte>(_start, sizeof(int) + IntPtr.Size)));
return v;
}
public static implicit operator string(BufferedString value) => value.AsString();
public override string ToString() => AsString();
}

为了演示字符串在NativeBuffering中的应用,我们为Entity添加了字符串类型的Qux属性。

[BufferedMessageSource]
public partial class Entity
{
public long Foo { get; set; }
public UnmanagedStruct Bar { get; set; }
public byte[] Baz { get; set; }
public string Qux { get; set; }
}

对于新的Entity类型,它对应的BufferedMessage封装的字节序列将变成如下的结构。

在Entity添加的Qux属性,也将同步体现在生成的两个.cs文件中。

public partial class Entity : IBufferedObjectSource
{
public int CalculateSize()
{
var size = 0;
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Foo);
size += NativeBuffering.Utilities.CalculateUnmanagedFieldSize(Bar);
size += NativeBuffering.Utilities.CalculateBinaryFieldSize(Baz);
size += NativeBuffering.Utilities.CalculateStringFieldSize(Qux);
return size;
}
public void Write(BufferedObjectWriteContext context)
{
using var scope = new BufferedObjectWriteContextScope(context);
scope.WriteUnmanagedField(Foo);
scope.WriteUnmanagedField(Bar);
scope.WriteBinaryField(Baz);
scope.WriteStringField(Qux);
}
} public unsafe readonly struct EntityBufferedMessage : IReadOnlyBufferedObject<EntityBufferedMessage>
{
public NativeBuffer Buffer { get; }
public EntityBufferedMessage(NativeBuffer buffer) => Buffer = buffer;
public static EntityBufferedMessage Parse(NativeBuffer buffer) => new EntityBufferedMessage(buffer);
public System.Int64 Foo => Buffer.ReadUnmanagedField<System.Int64>(0);
public ref UnmanagedStruct Bar => ref Buffer.ReadUnmanagedFieldAsRef<UnmanagedStruct>(1);
public BufferedBinary Baz => Buffer.ReadBufferedObjectField<BufferedBinary>(2);
public BufferedString Qux => Buffer.ReadBufferedObjectField<BufferedString>(3);
}

我们同样在演示程序中添加了针对字符串数据成员的验证。

using NativeBuffering;
using System.Diagnostics; var entity = new Entity
{
Foo = 123,
Bar = new UnmanangedStruct(789, 3.14),
Baz = new byte[] { 1, 2, 3 },
Qux = "Hello, World!"
}; var bytes = new byte[entity.CalculateSize()];
var context = new BufferedObjectWriteContext(bytes);
entity.Write(context);
File.WriteAllBytes(".data", bytes); EntityBufferedMessage bufferedMessage;
BufferOwner? bufferOwner = null; try
{
using (var fs = new FileStream(".data", FileMode.Open))
{
var byteCount = (int)fs.Length;
bufferOwner = BufferPool.Rent(byteCount);
fs.Read(bufferOwner.Bytes, 0, byteCount);
} bufferedMessage = BufferedMessage.Create<EntityBufferedMessage>(ref bufferOwner);
Debug.Assert(bufferedMessage.Foo == 123);
Debug.Assert(bufferedMessage.Bar.X == 789);
Debug.Assert(bufferedMessage.Bar.Y == 3.14);
Debug.Assert(bufferedMessage.Baz.Length == 3); var byteSpan = bufferedMessage.Baz.AsSpan();
Debug.Assert(byteSpan[0] == 1);
Debug.Assert(byteSpan[1] == 2);
Debug.Assert(byteSpan[2] == 3); Debug.Assert(bufferedMessage.Qux == "Hello, World!");
}
finally
{
bufferOwner?.Dispose();
}

NativeBuferring&mdash;&mdash;一种零分配的数据类型[上篇]的更多相关文章

  1. 深入剖析Linux IO原理和几种零拷贝机制的实现

    深入剖析Linux IO原理和几种零拷贝机制的实现 来源 https://zhuanlan.zhihu.com/p/83398714 零壹技术栈      公众号[零壹技术栈] 前言 零拷贝(Zero ...

  2. [转]详述DHCP服务器的三种IP分配方式

    DHCP就是动态主机配置协议(Dynamic Host Configuration Protocol),它的目的就是为了减轻TCP/IP网络的规划.管理和维护的负担,解决IP地址空间缺乏问题.这种网络 ...

  3. Nginx upstream的5种权重分配方式【转】

    原文地址:Nginx upstream的5种权重分配方式 1.轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除. 2.weight指定轮询几率,weig ...

  4. C++三种内存分配方式

    从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量,static变量.静态分配的区域的生命期是整个软件运行期,就是说从软件运行开始到软件终止退出.只 ...

  5. Nginx upstream的5种权重分配方式

    .轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,后端服务器down掉,能自动剔除 .weight 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况. upstre ...

  6. Nginx upstream的5种权重分配方式分享

    Nginx负载均衡的分发方式有4种: 1.轮询,默认采取此方式,Nginx会按照请求时间的先后顺序进行轮询分发,若某台Web Server宕机,Nginx自动将其摘掉. 2.weight,权重,即轮询 ...

  7. Nginx upstream的5种权重分配方式(转)

    出处:http://www.cnblogs.com/funsion/p/4003499.html?utm_source=tuicool 1.轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器, ...

  8. 算发帖&mdash;&mdash;俄罗斯方块覆盖问题一共有多少个解

    问题的提出:如下图,用13块俄罗斯方块覆盖8*8的正方形.   那么一共可以有多少个解呢?(若通过旋转.翻转一个解而得到的新解,则两个解视为同一个解)   首先,求解的问题,已经在上一篇帖子里完成 算 ...

  9. Maven&mdash;&mdash;软件开发中一个神奇的项目管理工具

    由于本人是从c++转入从事JAVA工作的 所以很多东西要从头学起,相信有很多跟我一样的人吧,那么我们一起来学习. 今天我们一起来认识下Maven这个工具,很多人可能会问题了,为什么说是工具呢?不是写代 ...

  10. 渗透测试流程&mdash;&mdash;渗透测试的9个步骤(转)

    目录 明确目标 分析风险,获得授权 信息收集 漏洞探测(手动&自动) 漏洞验证 信息分析 利用漏洞,获取数据 信息整理 形成报告 1.明确目标 1)确定范围:测试的范围,如:IP.域名.内外网 ...

随机推荐

  1. 【Vue2】NavigationDuplicated: Avoided redundant navigation to current location:xxxxx

    翻译过来就是,导航重复:避免了到当前位置的冗余导航. 简单来说就是重复跳转了相同路径 原因 触发这种情况是因为vue-router中引入了primise,当传递了多次重复的参数就会抛出异常,而这种问题 ...

  2. 2023-04-11:给你下标从 0 开始、长度为 n 的字符串 pattern , 它包含两种字符,‘I‘ 表示 上升 ,‘D‘ 表示 下降 。 你需要构造一个下标从 0 开始长度为 n + 1 的

    2023-04-11:给你下标从 0 开始.长度为 n 的字符串 pattern , 它包含两种字符,'I' 表示 上升 ,'D' 表示 下降 . 你需要构造一个下标从 0 开始长度为 n + 1 的 ...

  3. 2023-03-19:使用Go语言和FFmpeg库实现pcm编码为mp3。

    2023-03-19:使用Go语言和FFmpeg库实现pcm编码为mp3. 答案2023-03-19: 本文将介绍如何使用Go语言和FFmpeg库实现PCM音频文件编码为MP3格式.我们将使用moon ...

  4. 2020-08-22:I/O多路复用中select/poll/epoll的区别?

    福哥答案2020-08-22: select,poll,epoll 都是 操作系统实现 IO 多路复用的机制. 我们知道,I/O 多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是 ...

  5. 2021-02-13:字符串str最少添加多少个字符变成回文串?

    福哥答案2020-02-13: 假设字符串str是"abcde12344321",在str后添加"edcba"即可变成回文串.需要添加5个字符. 解法:包含最后 ...

  6. reverse逆转,即反向排序

    reverse逆转,即反向排序 print(Student.objects.all().exclude(nickname='A').reverse()

  7. ubuntu为navicat创建快捷方式

    一.前言 最近在ubuntu上安装了navicat,但是发现不能将其固定在启动栏阿!!!不能每次都用terminal运行吧!于是在上网查,有一说一,网上很多文章写的方法都不能实现(不排除是ubuntu ...

  8. React Native项目设置路径别名

    没有设置路径别名之前代码是这样的: import { px2dp } from '../../utils/screenKits'; 路径相当冗长,看着就头疼.增加了路径别名之后,变成这样 import ...

  9. 通过nc获取靶机的反弹Shell [靶机实战]

    1.环境 Kali:172.30.1.3/24 靶机(Funbox9):172.30.1.129/24 2.信息收集 通过nmap扫描此主机,我们需要获取到开放的端口以及服务的Banner 1 nma ...

  10. 代码随想录算法训练营Day23 二叉树

    代码随想录算法训练营 代码随想录算法训练营Day23 二叉树|669. 修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树 总结篇 669. 修剪二叉搜索树 题目链接 ...