第5章 编写异步代码

第5章 编写异步代码

5.1 异步函数简介

C# 5 引入了异步函数的概念。异步函数可以指某个由 async 修饰符修饰的方法或者匿名函数,它可以对 await 表达式使用 await 运算符。

5.2 对异步模式的思考

5.2.1 关于异步执行本质的思考

await 在 C#中的任务本质上是请求编译器为我们创建 续延 。尽管构想简单,却能显著增强代码可读性,让开发人员更从容。

在真实的异步模型中,续延并没有传递给异步操作,而是由异步操作发起并返回了一个令牌(即 Task​ 或 Task<TResult>​),该令牌可供续延使用。

C#5 的异步方法典型的执行流程如下:

  1. 执行某些操作;
  2. 启动一个异步操作,并记录其返回的令牌;
  3. 执行某些其他操作(通常在异步操作完成前不能进行后续操作,对应这一步应该为空);
  4. (利用令牌)等待异步操作完成;
  5. 执行其他操作;
  6. 完成执行。

Summary

等待异步操作其实是在表达:现在代码不能往下执行了,等待操作完成后再继续执行。那么如何才能不阻塞线程呢?答案很简单,那就是立即返回,之后继续异步地执行自身。如果想让调用方知道异步方法何时完成,则需要传递一个令牌给调用方,这样调用方就可以选择阻塞于该令牌上,或者(更有可能)将该令牌用于另一个续延。通常最终都会得到一批相互调用的异步方法,感觉就像进入了某段代码的一种“异步模式”。语言规范中并没有要求如此实现,但事实上对于调用异步操作的代码,其行为也和异步操作相一致,于是无形中就形成这样一条调用链。

5.2.2 同步上下文

异步函数使用 SynchronizationContext​ 类确保代码能够返回正确的线程中 , SynchronizationContext​ 类诞生于 .NET 2.0,用于像 BackgroundWorker​ 这样的组件中。SynchronizationContext​ 类负责在正确的线程中执行委托。该类中的 Post(异步)和 Send(同步)消息机制类似于 Windows Forms 的 Control.BeginInvoke() ​ 方法和 Control.Invoke() ​ 方法。

5.2.3 异步方法模型

对于异步编程,只有两点新语法:async 修饰符和 await 运算符。 async 用于修饰异步方法的声明, await 用于消费异步操作。

接下来分 3 节探讨异步方法,这 3 个阶段逐层递进。

  1. 声明 async 方法。
  2. 使用 await 运算符等待异步操作执行完成。
  3. 方法执行完成后返回值。

5.3 async 方法声明

除了新增了 async 关键字,async 方法声明的语法与其他方法声明没有区别。async 关键字可以放在 返回类型前的任意 位置。例如以下声明 均合 法:

public static async Task<int> FooAsync() { ... }
public async static Task<int> FooAsync() { ... }
async public Task<int> FooAsync() { ... }
public async virtual Task<int> FooAsync() { ... }

实际上,在异步设计之初,async 关键字不是必须的。为了代码的可维护性,设计团队引入了该关键字。而在生成的 IL 代码中,async 修饰符是被省略的。我们可以把现有普通方法(方法签名合适)改成 async 方法,或者反向操作。这种改动是源码和二进制 兼容 的。

async 属于方法的实现细节,因此 不能 声明抽象方法或者接口中的方法为 async。不过这些方法的返回值完全可以 是 Task<TResult>​ 类型的,它们的具体实现可以使用 async/await,也可以只是普通方法。

5.3.1 async 方法的返回类型

async 方法和调用它的方法之间通过返回值进行交互。在 C#5 中,异步函数的返回值仅限于以下 3 个类型:

  • void
  • Task ​:表示没有返回值的操作
  • Task<TResult> ​:表示返回值为 TResult​ 类型的操作

Info

C#7 新增了 ValueTask​,将在5.8.1 99.9% 的情况:ValueTask进行讲解

async 方法之所以可以返回 void 类型,是为了与 事件处理器 兼容。例如以下 UI 按钮点击的事件处理器:

private async void LoadStockPrice(object sender, EventArgs e)
{string ticker = tickerInput.Text;decimal price = await stockPriceService.FetchPriceAsync(ticker);priceDisplay.Text = price.ToString("c");
}

Warn

返回 void 类型的异步方法最好只用于事件订阅中。

5.3.2 async 方法的参数

async 方法的参数不能由 out 或者 ref 修饰。这是因为 outref 参数是用于与调用方交换信息的,有时 async 方法在控制流返回到调用方时,操作可能还未开始执行,因此引用参数可能尚未赋值。

此外, 指针 类型也不能用作 async 方法的参数。

5.4 await 表达式

await 所搭配的表达式有一个条件限制:它必须是可等待的。

5.4.1 可等待模式

“可等待模式”是基于模式实现的(而非类似于 using 语句+IDisposable​ 接口的形式)。假设有一个返回类型为 T​ 的表达式需要使用 await,编译器会执行以下检查步骤:

  1. T​ 必须具备一个无参数的 GetAwaiter() ​ 实例方法,或者存在 T​ 的扩展方法,该方法以类型 T​ 作为唯一参数。 GetAwaiter() ​ 方法的返回类型被称为 awaiter​ 类型。

  2. awaiter​ 类型必须实现 System.Runtime.INotifyCompletion ​ 接口,该接口中只有一个方法: void OnCompleted(Action) ​。

  3. awaiter​ 类型必须具有一个可读的实例属性 IsCompleted ​,其类型为 bool​。

  4. awaiter​ 类型必须具有一个非泛型、无参数的实例方法 GetResult() ​。

    该方法若返回 void ​,await 表达式会被视为无结果表达式;否则 await 表达式的返回类型与该方法保持一致。

  5. 上述成员不必为 public,但是这些成员需要能被调用 await 的 async 方法访问到。

    因此存在这样的可能性:对于某个类型,在某些代码中可以使用 await,但在其他代码中不可行,不过这种情况十分罕见。

如果 T​ 满足所有上述条件,就可以使用 await 运算符了。

Info

扩展方法的历史重要性

GetAwaiter()​ 之所以也可以是扩展方法,主要是由历史原因而不是现实原因决定的。C#5 是与 .NET 4.5 同期发布的,正是在这一版本中,C# 将 GetAwaiter()​ 方法引入了 Task​ 和 Task<TResult>​ 中。如果 GetAwaiter()​ 必须是根红苗正的实例方法,开发人员就不得不继续使用 .NET 4.0;而一旦支持扩展方法,就可以通过提供了这些扩展方法的 NuGet 包来实现 Task 和 Task<TResult>​ 的异步化。这样也能让社区不用测试 .NET 4.5 预览版,便能测试 C#5 编译器。

如今 framework 中的代码,早已具备相应的 GetAwaiter()​ 方法,因此以后几乎不再需要通过扩展方法来为某个类型添加可等待属性了。

5.4.2 await 表达式的限制条件

await 表达式的使用有如下限制:

  • 只能用于 async 方法或异步匿名函数中(见5.7 异步匿名函数);

  • await 运算符不能用于 非安全(unsafe) 上下文中;

    如下代码演示了 await 和 unsafe 使用时的限制:

    static async Task DelayWithResultOfUnsafeCode(string text)
    {int total = 0;unsafe                                    // async 方法中可以有非安全的上下文{fixed (char* textPointer = text){char* p = textPointer;while (*p != 0){total += *p;p++;}}}Console.WriteLine("Delaying for " + total + "ms");await Task.Delay(total);                // 但是 await 表达式不能位于非安全的上下文中Console.WriteLine("Delay complete");
    }
    
  • await 无法在锁中使用;

    如果确实需要锁,应使用 SemaphoreSlim​ 的 WaitAsync()​ 方法代替。

Info

还有一些上下文无法使用 await,在 C#6 解禁了:

  • 所有带有 catch​ 块的 try​ 块。
  • 所有 catch​ 块。
  • 所有 finally​ 块。

5.6 返回值的封装

5.6.2 await 表达式的运算

执行 await 表达式时,有两种可能:

  • 异步操作已经完成

    • 操作失败并捕获了表示失败的异常:会 抛出异常
    • 执行成功:获取 操作结果

    上述操作不涉及上下文切换和添加延续

  • 异步操作仍在进行

    此时 await 会异步等待操作完成(此时方法已经停止执行),之后在某个合适的上下文中继续执行其余代码(给异步操作附加一个延续)。

关于执行顺序,有如下几个重点。

  • async 方法在 await 已完成的 task 时不返回,此时方法还是按照 同步 方式执行。
  • 当 async 方法 await 延迟 task 时,async 方法会 立即返回
  • async 方法返回的 task 只有在 方法完成 后才完成。

image

按照上述规则,思考如下代码的输出:

static void Main()
{Task task = DemoCompletedAsync();Console.WriteLine("Method returned");task.Wait();Console.WriteLine("Task completed");
}
static async Task DemoCompletedAsync()
{Console.WriteLine("Before first await");await Task.FromResult(10);Console.WriteLine("Between awaits");await Task.Delay(1000);Console.WriteLine("After second await");
}

5.6.2.1 从异步方法中返回的含义

从异步方法返回意味着两种可能:

  • 目前是执行中遇到的第一个 await 表达式,最初的调用方 在调用栈中。

    最后得到的一般是返回给调用方的 Task​ 或者 Task<TResult>​,即将返回的 task 必须是 未完成的

  • 已处于等待某个操作完成期间,因此正处于某个被调起的 续延 之中。此时的调用栈与方法刚开始执行时的调用栈 大不相同

    回调方是谁取决于 当前上下文 。在 Windows Forms UI 中,如果在 UI 线程中调用某个 async 方法,且没有主动切换出 UI 线程,整个方法将在 UI 线程中执行

Notice

在真正执行到第一个异步 await 表达式之前,方法是完全同步执行的。

5.6.3 可等待模式成员的使用

5.4.1 可等待模式将可等待模式描述为一种 需要实现的类型 ,可以对该类型的表达式使用 await 运算符。下面把异步行为模式的几块拼图合到一起,用可等待模式替代原先比较宽泛的描述:

image

5.6.4 异常拆封

async/await 的基础架构会尽量让处理异步失败接近于处理同步失败。如果把失败看作一种特殊形式的结果,异常和 返回值 的处理就很相似了。

GetResult()​ 方法不仅负责返回结果,也负责生成异步操作抛出的异常并将其返回给调用方。下面以 Task​ 和 Task<TResult>​ 为例展开分析:

  • 当某个操作失败时,任务的 Status​ 变成 Faulted ​(并且 IsFaulted ​ 的值为 true)。
  • Exception​ 属性返回一个 AggregateException ​,它包含导致任务失败的所有(可能多个)异常。如果任务没有 faulted,则该属性值为 null )。
  • 如果任务最后的状态为 Faulted​,Wait()​ 方法会抛出一个 AggregateException ​。
  • Task<TResult>​(也在等待完成)的 Result​ 属性可能抛出一个 AggregateException ​。

Tips

如果通过 CancellationTokenSource​ 和 CancellationToken​ 取消任务,Wati()​ 方法和 Result​ 属性会抛出 AggregateException ​ 异常,内含 OperationCanceledException​,task 的状态为 canceled ​。

在 await 某个 task 时,如果其状态变为 Faulted​ 或者 Canceled​,那么抛出的异常将不是 AggregateException​,而是 AggregateException内部的第一个 异常。

对于 Task.WhenAll()​ 这类方法,想获取全部异常可以通过遍历每个 Task​ 的 Exception ​ 属性。

5.6.5 完成方法

5.6.5.1 成功返回

执行成功有:

  • Task<TResult>​ 类型:return 语句需要返回一个类型为 T​(或可以转换为 TResult​ 的类型)的结果,然后由异步基础架构为 task 生成结果;
  • Task​ 或 void​:与同步 void 方法类似,无 return 语句或 return 无返回值,但要根据实际情况更新 task 的状态。

5.6.5.2 延迟异常和实参校验

我们前文有提到:

实际上,在异步设计之初,async 关键字不是必须的。为了代码的可维护性,设计团队引入了该关键字。而在生成的 IL 代码中,async 修饰符是被省略的。我们可以把现有普通方法(方法签名合适)改成 async 方法,或者反向操作。这种改动是源码和二进制 兼容 的。

实际上,二者在面对异步方面仍有差别。即便 async 方法执行的第一步就是抛出异常,那它也只是返回一个 faulted task 。(此时该 task 立即变为 faulted )。这导致只有调用方等待该任务时,才能获取异常信息:

static async Task MainAsync()
{Task<int> task = ComputeLengthAsync(null);  // 故意传入错误的实参Console.WriteLine("Fetched the task");int length = await task;Console.WriteLine("Length: {0}", length);   // await 结果
}static async Task<int> ComputeLengthAsync(string text)  // 尽早抛出异常
{if (text == null){throw new ArgumentNullException("text");}await Task.Delay(500);      // 模拟真实的异步操作return text.Length;
}

这导致异常的抛出到暴漏存在时间差,多数时候这种时间差可以容忍。消除这种时间差可以编写一个返回 task非 async 方法,专门负责 参数校验 ,校验完成后调用另一个异步函数。实现的形式有 3 种:

  • 参数校验与具体实现剥离为各自的方法;

  • 将具体实现改为异步匿名函数;

  • 将具体实现改为局部 async 方法。

    作者倾向于这种方式,它有如下好处:

    1. 不会向类中额外引入新方法;
    2. 避免创建委托的麻烦

如下代码演示了第一种形式:

static Task<int> ComputeLengthAsync(string text)    // 非 async 方法。异常
{                                                   // 不会被封装至 task 中if (text == null){throw new ArgumentNullException("text");}return ComputeLengthAsyncImpl(text);    // 校验完成后调用实现方法
}static async Task<int> ComputeLengthAsyncImpl(string text)
{await Task.Delay(500);      // async 方法的实现中return text.Length;         // 假设无需校验输入值
};

使用 null 作为实参调用 ComputeLengthAsync()​ 方法,异常会以 同步 方式抛出,而不是返回一个 faulted task。

5.6.5.3 处理取消

任务并行库为.NET 4 引入了一个统一的取消模型,主要依靠以下两个类型:

  • CancellationTokenSource
  • CancellationToken

我们一般调用 CancellationToken​ 的 ThrowIfCancellationRequested() ​ 方法进行取消操作,该方法利用“异步方法抛出 OperationCanceledException ​ 异常(或其子类,如 TaskCanceledException​),返回的 task 状态就是 Canceled ​”这一特点取消任务。

虽然 task 的状态是 Canceled​,await 时异常仍会暴漏。如果使用 Wait()​ 方法或通过 Result​ 属性请求结果,异常还是会在 AggregateException ​ 内部被抛出。

5.7 异步匿名函数

异步匿名函数的创建与匿名方法、lambda 表达式相同,在前面加上 async 即可。例如:

Func<Task> lambda = async () => await Task.Delay(1000);
Func<Task<int>> anonMethod = async delegate()
{Console.WriteLine("Started");await Task.Delay(1000);Console.WriteLine("Finished");return 10;
};

与普通匿名函数类似,异步匿名函数也可以捕获变量、添加参数等。

异步匿名函数的使用场景较少,它主要用于与 LINQ 搭配。

5.8 C#7 自定义 task 类型

在 C#5 和 C#6 中,异步函数只能返回 void​、Task​ 或 Task<TResult>​。C#7 对于这一限制有所放宽:某些通过特定方式修饰的类型也可以用作异步函数的返回类型。

5.8.1 99.9% 的情况:ValueTask<TResult>

ValueTask<TResult>​ 的诞生是出于对性能的极致优化,相比 Task<TResult>​ 优势体现在 内存分配和 垃圾回收 上。

有时 task 在 await 之前便完成,最常见的场景便是从流中读取数据,而流刚好缓存了数据。此时若使用 Task<TResult>​ 作为返回值,会给 垃圾回收器 带来负担。使用 ValueTask<TResult>​,只有在需要把流数据重新填充到缓存时才需要堆内存分配。

如下代码演示了 ValueTask<TResult>​ 的使用:

public sealed class ByteStream : IDisposable
{private readonly Stream stream;private readonly byte[] buffer;private int position;           // 待返回的缓冲区下一个索引private int bufferedBytes;      // 缓冲区读取的字节数public ByteStream(Stream stream){this.stream = stream;buffer = new byte[1024 * 8];    // 8KB 缓冲区大小,意味着}                                   // 几乎不需要 await 操作public async ValueTask<byte?> ReadByteAsync(){if (position == bufferedBytes)  // 根据需要重新填充缓冲区{position = 0;bufferedBytes = await                           // 从流中stream.ReadAsync(buffer, 0, buffer.Length)  // 异步读取.ConfigureAwait(false);         // 配置 await 操作if (bufferedBytes == 0)                 // 忽略上下文{return null;        // 指示已经读取到流的末尾}}return buffer[position++];      // 返回缓冲区中的下一个字节}public void Dispose(){stream.Dispose();}
}

5.8.2 剩下 0.1% 的情况:创建自定义 task 类型

自定义 task 类型必须实现可等待模式。以下方代码为例,实现的内容包括:

  • CustomTask

    核心作用:自定义的异步任务 容器 ,类似原生 Task<T>​,但使用自定义构建逻辑。

    关键方法

    • GetAwaiter()​:返回 CustomTaskAwaiter<T>​,使该类型支持 await ​ 关键字。

    特性标记[AsyncMethodBuilder]​ 指定编译器使用 CustomTaskBuilder<T> ​ 构建异步状态机。

  • CustomTaskAwaiter

    核心作用:实现 INotifyCompletion​ 接口,提供 await ​ 关键字的底层等待逻辑。

    关键成员

    • IsCompleted​:判断异步操作是否已完成(无需挂起)。
    • GetResult()​:获取异步结果(可能阻塞或抛出异常)。
    • OnCompleted(Action)​:注册延续回调,用于异步完成时恢复执行。
  • CustomTaskBuilder

    核心作用:编译器生成的异步状态机的控制器,管理状态流转和任务生命周期。

    关键方法

    • Create()​:工厂方法,创建构建器实例。
    • Start()​:启动状态机,触发首次 MoveNext()​ 调用。
    • SetResult(T)​/SetException()​:标记任务成功完成或失败。
    • AwaitOnCompleted()​/AwaitUnsafeOnCompleted()​:处理 await​ 挂起逻辑,绑定 Awaiter 与状态机。
    • Task​ 属性:暴露关联的 CustomTask<T>​ 实例。

    状态机交互:通过IAsyncStateMachine​接口与编译器生成的状态机协作。

[AsyncMethodBuilder(typeof(CustomTaskBuilder<>))]
public class CustomTask<T>
{public CustomTaskAwaiter<T> GetAwaiter();
}public class CustomTaskAwaiter<T> : INotifyCompletion
{public bool IsCompleted { get; }public T GetResult();public void OnCompleted(Action continuation);
}public class CustomTaskBuilder<T>
{public static CustomTaskBuilder<T> Create();public void Start<TStateMachine>(ref TStateMachine stateMachine)where TStateMachine : IAsyncStateMachine;public void SetStateMachine(IAsyncStateMachine stateMachine);public void SetException(Exception exception);public void SetResult(T result);public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)where TAwaiter : INotifyCompletionwhere TStateMachine : IAsyncStateMachine;public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)where TAwaiter : INotifyCompletionwhere TStateMachine : IAsyncStateMachine;public CustomTask<T> Task { get; }
}

其中,泛型、非泛型的主要差别在于 CustomTaskAwaiter.GetResult()​ 方法的返回值和 CustomTaskBuilder.SetResult()​ 的入参。

5.9 C#7.1 中的异步 Main()​ 方法

C# 语言中程序入口方法一直以来都有如下要求:

  • 方法名必须是 Main() ​;
  • 必须为 态;
  • 返回类型必须是 void ​ 或者 int ​;
  • 必须是无参方法或者只能有一个 string[] ​ 类型的参数(不能是 ref 和 out 参数);
  • 必须是非泛型并且在一个非泛型类中声明(如果是嵌套类,那么涉及的类也必须都是非泛型);
  • 不能是没有实现的局部类;
  • 不能有 async 修饰符。

从 C# 7.1 开始,废止了最后一条要求,同时略微修改了对返回类型的要求:

  • 可以有 async 修饰符;

  • async 入口方法返回类型必须是 Task ​ 或 Task<int> ​。

    不能是 void​,也不能使用自定义 task 类型

编译器在处理该 async 入口方法时,会创建一个同步的封装方法,该封装方法作为程序集真正的入口方法。封装方法依然满足前面所说的几个要求:无参数或者只有一个 string[] ​ 参数,返回值类型是 void ​ 或者 int ​(取决于 async 方法的参数和返回值类型)。封装方法会调用这段代码,然后对返回的 task 调用 GetAwaiter() ​ 方法,并且 在 awaiter 上调用 GetResult() 方法。封装方法的代码如下:

static void < Main >()    // 方法的名称在 C#中是非法的,但在 IL 中是合法的
{Main().GetAwaiter().GetResult();
}

5.10 使用建议

5.10.1 使用 ConfigureAwait()​ 避免上下文捕获(择机使用)

ConfigureAwait()​ 方法用于避免异步执行结束后回到 UI 线程,这对非 UI 类库十分有用。

调用 ConfigureAwait(false)​ 的结果是不会把续延安排到最初的同步上下文中执行,而是为它安排一个线程池的线程。该方法的返回值类型是 ConfiguredTaskAwaitable<int>​。

读者可能会担心这样的配置会对调用方有什么影响,不必多虑。即使异步方法内部的续延需要在线程池线程中运行,UI 代码中的 await 表达式也能够捕获 UI 的上下文,然后安排自己的续延在 UI 线程中执行,这样在 task 完成之后 UI 仍然可以正常更新。

Notice

注意,只有 await 的 task 还未完成时,两段代码的行为才有区别。如果 task 已经完成,那么方法会以 同步 方式继续执行,无论是否使用了 ConfigureAwait(false)​。

因此,库中每个 await task 都应当 以此进行配置 ,不能指望只对 async 方法的第一个task 使用 ConfigureAwait(false)​ 之后,其余代码就都能在线程池线程中执行了。

Suggest

NuGet Gallery | ConfigureAwaitChecker.Analyzer 5.0.0.1 Roslyn 分析器可以协助开发者检查是否进行了 ConfigureAwait()​ 配置。开发者编写类库时可以通过它协助检查是否所有的 await task 都进行了配置。

5.10.2 启动多个独立 task 以实现并行

试比较如下两段代码:

Task<decimal> hourlyRateTask = employee.GetHourlyRateAsync();
decimal hourlyRate = await hourlyRateTask;
Task<int> hoursWorkedTask = timeSheet.GetHoursWorkedAsync(employee.Id);
int hoursWorked = await hoursWorkedTask;
AddPayment(hourlyRate * hoursWorked);
Task<decimal> hourlyRateTask = employee.GetHourlyRateAsync();
Task<int> hoursWorkedTask = timeSheet.GetHoursWorkedAsync(employee.Id);
decimal hourlyRate = await hourlyRateTask;
int hoursWorked = await hoursWorkedTask;
AddPayment(hourlyRate * hoursWorked);

显然,第 段代码发挥了并行执行的优势。多数时候,应当尽可能 行执行独立的 task。需要注意的是,这种模式下想要记录所有 task 的失败结果,应使用 Task.WhenAll() ​ 方法等待。

5.10.3 避免同步代码和异步代码混用

在两种模式之间切换困难重重,并且困难程度随情况而异。

特别需要注意:通过 Task<TResult>.Result​ 属性和 Task.Wait()​ 方法以同步方式从异步操作获取结果容易导致 死锁

5.10.4 根据需要提供取消机制

5.10.5 测试异步模式

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Cloud Gateway 与 Knife4j 集成实践

注意:写于 2025/1/10,未来时间可能失效,请根据具体情况实践。在微服务架构中,Gateway 通常承担着路由转发、负载均衡、鉴权等职责,而 Knife4j 是一个集 Swagger2 和 OpenAPI3 为一体的增强解决方案,可以帮助开发者快速聚合使用OpenAPI 规范。 本文参考 Knife4j 文档,进行…

用户说:10分钟用通义灵码搞定“今天穿什么”!打开爽文世界……

当我仅用10分钟调教出一个会关心我穿不穿秋裤的管家时,突然想到,现在限制我们开发的已经不是编程能力,而在于你有没有把你的想象力塞进代码框!作者:ZLJ,浙江大学教育技术学研究生 当我仅用10分钟调教出一个会关心我穿不穿秋裤的管家时,突然想到,现在限制我们开发的已经…

Cyber Apocalypse 2025 forensics WP

Cyber Apocalypse 2025 forensics WPCyber Apocalypse 2025 WP 做了国际赛之后虽然(目前只做了两道,可是没环境了啊,前几天比赛有点多~www),只从取证这边说,感觉他们的题很有趣,情境也给的很真实连贯,出题灵活,就是能见到很多新兴的知识,拓展知识面,以后会多看一看…

python第六周作业(第四章课后程序练习题)

4.1 import random def guess_number(): target = random.randint(1, 100) count = 0 while True:guess = int(input("请输入你猜的数字(1-100): "))count += 1if guess < target:print("猜小了")elif guess > target:print("猜大了")else…

win安装oracle19c没有listener

然后就可以看到启动了本文来自博客园,作者:余生请多指教ANT,转载请注明原文链接:https://www.cnblogs.com/wangbiaohistory/p/18803159

《HarmonyOS Next开发进阶:打造功能完备的Todo应用华章》

章节 6:日期选择器与日期处理目标学习如何使用DatePicker组件。 理解日期格式化和日期计算。内容日期选择器基础使用DatePicker组件。 处理日期选择事件。日期格式化格式化日期为友好的文本。日期计算判断日期是否过期或即将到期。代码示例 @Entry @Component struct DatePick…

MarkDwon语法

MarkDown语法 1、标题用法 一级标题:#+空格+内容+回车 二级标题:##+空格+内容+回车 三级标题:###+空格+内容+回车 四级标题:####+空格+内容+回车 2、字体用法 粗体使用:快捷键ctrl+b或者内容两边加两个星号,示例 斜体使用:内容两边加一个星号,示例 斜体加粗:内容两边加…

图论(连通分量)

AT_abc284_c [ABC284C] Count Connected Components 题目描述 頂点に $ 1 $ から $ N $ の番号が、辺に $ 1 $ から $ M $ の番号がついた $ N $ 頂点 $ M $ 辺の単純無向グラフが与えられます。辺 $ i $ は頂点 $ u_i $ と頂点 $ v_i $ を結んでいます。 グラフに含まれる連結…

业务系统基础框架-Winform版-角色

角色列表,可刷新,可展开,可折叠编辑角色为角色权限为角色分配菜单查看拥有此角色的账号

3.31 学习记录

实现了使用springboot从文件中读取数据显示在前端

记一次GC导致线上服务超时问题

1、现象2024-12-28 23点左右,线上其他服务请求 content-cache 出现批量超时。content-cache-03 机器内存使用率如下:机器配置:4核8G这里因为JVM参数设置为:-Xms4g -Xmx4g -XX:MaxNewSize=1g所以达到42%时,内存的使用率已经达到了3.3G。 2、数据查看GC日志如下: (1)CMS老…

No.1 可视化大屏--vite+vue3项目环境搭建

一、DataV-Vue3 1.1 安装 官网:https://datav-vue3.netlify.app/Guide/Guide.htmlnpm install @kjgl77/datav-vue3 一、vite3构建Vue3项目 1.1 什么是Vite 1.2创建vite3项目 第一步:新建一个项目的文件夹第二步:输入cmd,回车 第三步: npm init vite 第四步:输入项目名称…