目录

介绍

代码库

类库

入门

更复杂

JobRunner

JobScheduler

结论和总结


介绍

本文采用一种实用的方法来演示其中的一些关键概念,并介绍更复杂的编码模式。本文基于DotNetCore控制台应用程序。

您将需要一个与DotNetCore兼容的开发环境,通常是Visual Studio或Visual Code,以及与此项目相关联的Repo的副本才能运行该代码。

免责声明——该代码是实验性的,而不是生产性的。设计简洁,错误捕获和处理很少,以使其易于阅读和理解。出于相同的原因,类保持简单。

代码库

此处的代码可在GitHub Repo中找到。该项目的代码在Async-Demo。忽略任何其他项目——它们是有关异步编程的进一步文章。

类库

在开始之前,您需要了解两个帮助程序类:

  1. LongRunningTasks ——模拟工作

    1. RunLongProcessorTaskAsync和RunLongProcessorTask使用素数计算来模拟处理器繁重的任务。
    2. RunYieldingLongProcessorTaskAsync 是每100次计算产生一次的版本。
    3. RunLongIOTaskAsync使用Task.Delay模拟缓慢的I / O操作。
  2. UILogger提供用于将信息记录到UI的抽象层。您将委托传递Action给方法。UILogger生成消息,然后调用Action将消息实际写入到Action配置为要写入的位置。在我们的例子LogToConsole的Program中,运行Console.WriteLine。它可以轻松地写入文本文件。

入门

我们的第一个挑战是从同步切换到异步。

确保您运行的是正确的框架和最新的语言版本。(C#7.1及更高版本支持基于Main的Task )。

<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net5</TargetFramework><LangVersion>latest</LangVersion><RootNamespace>Async_Demo</RootNamespace>
</PropertyGroup>

在#7.1之前的版本中Main只能同步运行,你需要一个“NONO”,使用Wait来防止Main从底部退出并关闭程序。发布#7.1后,声明Main返回Task。

async Main模式如下所示。声明async取决于代码中是否包含await:

// With await
static async Task Main(string[] args)
{// code// await somewhere in here
}// No awaits
static Task Main(string[] args)
{// code// no awaitsreturn Task.CompletedTask;
}

注意事项

  1. 如果使用async关键字但不带await,则编译器会发出警告,但无论如何都会进行编译,并将该方法视为同步代码。
  2. 您不能将方法声明为async并返回Task。您只需返回正确的值,编译器便会完成所有的繁琐工作。

因此,让我们运行一些代码。我们的第一次运行:

static Task Main(string[] args)
{var watch = new Stopwatch();watch.Start();UILogger.LogThreadType(LogToConsole, "Main");var millisecs = LongRunningTasks.RunLongProcessorTask(5);watch.Stop();UILogger.LogToUI(LogToConsole, $"Main ==> Completed in { watch.ElapsedMilliseconds} milliseconds", "Main");return Task.CompletedTask;
}

该任务按预期同步运行。Task中的一堆同步代码。没有yield。

[11:35:32][Main Thread][Main] >  running on Application Thread
[11:35:32][Main Thread][LongRunningTasks] > ProcessorTask started
[11:35:36][Main Thread][LongRunningTasks] > ProcessorTask completed in 3399 millisecs
[11:35:36][Main Thread][Main] > Main ==> Completed in 3523 milliseconds
Press any key to close this window . . .

我们的第二次运行:

static async Task Main(string[] args)
{var watch = new Stopwatch();watch.Start();UILogger.LogThreadType(LogToConsole, "Main");var millisecs = await LongRunningTasks.RunLongProcessorTaskAsync(5, LogToConsole);UILogger.LogToUI(LogToConsole, $"Yielded to Main", "Main");watch.Stop();UILogger.LogToUI(LogToConsole, $"Main ==> Completed in { watch.ElapsedMilliseconds} milliseconds", "Main");
}

任务同步运行——无yield。逻辑上是因为没有理由yield。RunLongProcessorTaskAsync是任务中的同步代码块——计算质数——因此运行完毕。该await是多余的,它可能是一个Task,但它并没有yield,所以从未放弃线程,直到完成。

[11:42:43][Main Thread][Main] >  running on Application Thread
[11:42:43][Main Thread][LongRunningTasks] > ProcessorTask started
[11:42:46][Main Thread][LongRunningTasks] > ProcessorTask completed in 3434 millisecs
[11:42:46][Main Thread][Main] > Yielded
[11:42:46][Main Thread][Main] > Main ==> Completed in 3593 milliseconds

我们的第三次运行:

static async Task Main(string[] args)
{var watch = new Stopwatch();watch.Start();UILogger.LogThreadType(LogToConsole, "Main");var millisecs = LongRunningTasks.RunYieldingLongProcessorTaskAsync(5, LogToConsole);UILogger.LogToUI(LogToConsole, $"Yielded to Main", "Main");watch.Stop();UILogger.LogToUI(LogToConsole, $"Main ==> Completed in { watch.ElapsedMilliseconds} milliseconds", "Main");
}

在看结果之前,让我们看一下RunLongProcessorTaskAsync和RunYieldingLongProcessorTaskAsync之间的区别。我们添加了Task.Yield()来控制每100个素数。

if (isPrime)
{counter++;// only present in Yielding versionif (counter > 100){await Task.Yield();counter = 0;}
}

长期运行的任务没有完成。在计算出最初的100个素数后,RunYieldingLongProcessorTaskAsync在计算完前100个素数(略低于173毫秒)后返回到Main,并且Main在yield期间运行完毕。

[12:13:56][Main Thread][Main] >  running on Application Thread
[12:13:56][Main Thread][LongRunningTasks] > ProcessorTask started
[12:13:57][Main Thread][Main] > Yielded to Main
[12:13:57][Main Thread][Main] > Main ==> Completed in 173 milliseconds

如果我们更新Main到await长处理器任务:

var millisecs = await LongRunningTasks.RunYieldingLongProcessorTaskAsync(5, LogToConsole);

它运行到完成。尽管它yield了,但在继续进入Main之前,我们需要await RunYieldingLongProcessorTaskAsync Task完成。这里还有一个重要的注意事项。查看长时间运行的任务在哪个线程上运行,并将其与以前的运行进行比较。从[Main Thread]开始后,它跳到了一个新线程[LongRunningTasks Thread]。

[12:45:10][Main Thread:1][Main] >  running on Application Thread
[12:45:11][Main Thread:1][LongRunningTasks] > ProcessorTask started
[12:45:14][LongRunningTasks Thread:7][LongRunningTasks] > ProcessorTask completed in 3892 millisecs
[12:45:14][LongRunningTasks Thread:7][Main] > Yielded to Main
[12:45:14][LongRunningTasks Thread:7][Main] > Main ==> Completed in 4037 milliseconds

在RunYieldingLongProcessorTaskAsync中添加一个快速Console.Write,以查看每个生成的迭代运行在哪个线程上——编写ManagedThreadId

counter++;
if (counter > 100)
{Console.WriteLine($"Thread ID:{Thread.CurrentThread.ManagedThreadId}");await Task.Yield();counter = 0;
}

结果如下所示。注意常规线程跳跃。Yield创建一个新的continuation Task,并将其调度为异步运行。首先Task.Yield,应用程序线程调度程序将新的Task消息传递给应用程序池,然后在应用程序池上,调度程序决定在何处运行Task。

引用MicrosoftTask.Yield()它创建了一个等待的任务,等待时异步地返回当前上下文。我的意思是说,它是句法糖,用于对树进行控制并创建延续`Task`,并在计划它时将其发布回Scheduler以运行。进一步引用在等待时将在等待时异步转换回当前上下文的上下文。换句话说,除非您告知,否则它不会等待。在继续中访问第一个yield,然后继续执行下面的代码`Task.Yield()`。我已经测试过了

但是,以下警告适用——再次引用官方文档:

但是,上下文将决定如何相对于其他可能待处理的工作来确定该工作的优先级。在大多数UI环境中,UI线程上存在的同步上下文通常会将发布到上下文的工作的优先级高于输入和呈现工作。因此,请勿依赖于等待Task.Yield来保持UI响应。

[12:38:16][Main Thread:1][Main] >  running on Application Thread
[12:38:16][Main Thread:1][LongRunningTasks] > ProcessorTask started
Thread ID:1
Thread ID:4
Thread ID:4
Thread ID:6
Thread ID:6
Thread ID:7

最后,切换到RunLongIOTaskAsync长期运行的任务。

var millisecs = await LongRunningTasks.RunLongIOTaskAsync(5, LogToConsole);

如果不await,则与以前相同:

[14:26:46][Main Thread:1][Main] >  running on Application Thread
[14:26:47][Main Thread:1][LongRunningTasks] > IOTask started
[14:26:47][Main Thread:1][Main] > Yielded to Main
[14:26:47][Main Thread:1][Main] > Main ==> Completed in 322 milliseconds

如果await运行完成,请再次使用线程开关。

[14:27:16][Main Thread:1][Main] >  running on Application Thread
[14:27:16][Main Thread:1][LongRunningTasks] > IOTask started
[14:27:21][LongRunningTasks Thread:4][LongRunningTasks] > IOTask completed in 5092 millisecs
[14:27:21][LongRunningTasks Thread:4][Main] > Yielded to Main
[14:27:21][LongRunningTasks Thread:4][Main] > Main ==> Completed in 5274 milliseconds

更复杂

好的,现在更接近现实并编写一些代码。

JobRunner

JobRunner是运行和控制异步作业的简单类。出于我们的目的,它运行长时间运行的任务之一来模拟工作,但是您可以将基本模式用于现实情况。

这是不言而喻的,但我将介绍TaskCompletionSource。

引用MS“代表未绑定到委托的Task<TResult>的生产方,通过Task属性提供对消费方的访问。您可以通过TaskCompletionSource.Task实例获得由TaskCompletionSource.Task公开的Task,换句话说,是从方法中分离的手动控制Task

Task表示JobRunner的状态作为JobTask属性而暴露。如果底层TaskCompletionSource没有设置它返回一个简单的Task.CompletedTask对象,否则返回JobTaskController的Task。该Run方法使用异步事件模式——我们需要异步运行的代码块,以进行控制await。Run控制Task状态,但其Task本身是独立于Run的。IsRunning确保作业一旦运行就无法再启动。

class JobRunner
{public enum JobType { IO, Processor, YieldingProcessor } public JobRunner(string name, int secs, JobType type = JobType.IO){this.Name = name;this.Seconds = secs;this.Type = type;}public string Name { get; private set; }public int Seconds { get; private set; }public JobType Type { get; set; }private bool IsRunning;public Task JobTask => this.JobTaskController == null ? Task.CompletedTask : this.JobTaskController.Task;private TaskCompletionSource JobTaskController { get; set; } = new TaskCompletionSource();public async void Run(){if (!this.IsRunning) {this.IsRunning = true;this.JobTaskController = new TaskCompletionSource();switch (this.Type){case JobType.Processor:await LongRunningTasks.RunLongProcessorTaskAsync(Seconds, Program.LogToConsole, Name);break;case JobType.YieldingProcessor:await LongRunningTasks.RunYieldingLongProcessorTaskAsync(Seconds, Program.LogToConsole, Name);break;default:await LongRunningTasks.RunLongIOTaskAsync(Seconds, Program.LogToConsole, Name);break;}this.JobTaskController.TrySetResult();this.IsRunning = false;}}
}

JobScheduler

JobScheduler是用于实际调度作业的方法。与Main分开来演示异步编程的一些关键行为。

  1. Stopwatch 提供时间安排。
  2. 创建四个不同的IO作业。
  3. 开始四个工作。
  4. 使用Task.WhenAll等待某些任务再继续。注意Task是JobRunnner实例公开的JobTask。

“WhenAll”是几种静态“Task”方法之一。whenAll创建一个单个Task,它唤醒提交数组中的所有Task。所有任务完成后,其状态将更改为Complete`WhenAny`很类似,但是当完成时将被设置为*Complete *。它们可以被命名为*AwaitAll**AwaitAny*“WaitAll”“WaitAny”是阻止版本,类似于“Wait”。不知道命名转换有点令人困惑的原因——我敢肯定有一个。

static async Task JobScheduler()
{var watch = new Stopwatch();watch.Start();var name = "Job Scheduler";var quickjob = new JobRunner("Quick Job", 3);var veryslowjob = new JobRunner("Very Slow Job", 7);var slowjob = new JobRunner("Slow Job", 5);var veryquickjob = new JobRunner("Very Quick Job", 2);quickjob.Run();veryslowjob.Run();slowjob.Run();veryquickjob.Run();UILogger.LogToUI(LogToConsole, $"All Jobs Scheduled", name);await Task.WhenAll(new Task[] { quickjob.JobTask, veryquickjob.JobTask }); ;UILogger.LogToUI(LogToConsole, $"Quick Jobs completed in {watch.ElapsedMilliseconds} milliseconds", name);await Task.WhenAll(new Task[] { slowjob.JobTask, quickjob.JobTask, veryquickjob.JobTask, veryslowjob.JobTask }); ;UILogger.LogToUI(LogToConsole, $"All Jobs completed in {watch.ElapsedMilliseconds} milliseconds", name);watch.Stop();
}

现在,我们需要对Main做一些改变:

static async Task Main(string[] args)
{var watch = new Stopwatch();watch.Start();UILogger.LogThreadType(LogToConsole, "Main");var task = JobScheduler();UILogger.LogToUI(LogToConsole, $"Job Scheduler yielded to Main", "Main");await task;UILogger.LogToUI(LogToConsole, $"final yield to Main", "Main");watch.Stop();UILogger.LogToUI(LogToConsole, $"Main ==> Completed in { watch.ElapsedMilliseconds} milliseconds", "Main");//return Task.CompletedTask;
}

运行此命令时,将在下面显示输出。需要注意的有趣的地方是:

  1. 每个作业都开始,然后在第一次等待时产生,将控制权交还给调用者——在这种情况下为JobSchedular。
  2. JobScheduler运行到第一个await并返回到Main。
  3. 当头两个作业完成时,它们的JobTask将设置为完成并JobScheduler继续进行下一个await作业。
  4. JobScheduler在最长的时间上需要一点时间完成Job。
[16:58:52][Main Thread:1][Main] >  running on Application Thread
[16:58:52][Main Thread:1][LongRunningTasks] > Quick Job started
[16:58:52][Main Thread:1][LongRunningTasks] > Very Slow Job started
[16:58:52][Main Thread:1][LongRunningTasks] > Slow Job started
[16:58:52][Main Thread:1][LongRunningTasks] > Very Quick Job started
[16:58:52][Main Thread:1][Job Scheduler] > All Jobs Scheduled
[16:58:52][Main Thread:1][Main] > Job Scheduler yielded to Main
[16:58:54][LongRunningTasks Thread:4][LongRunningTasks] > Very Quick Job completed in 2022 millisecs
[16:58:55][LongRunningTasks Thread:4][LongRunningTasks] > Quick Job completed in 3073 millisecs
[16:58:55][LongRunningTasks Thread:4][Job Scheduler] > Quick Jobs completed in 3090 milliseconds
[16:58:57][LongRunningTasks Thread:4][LongRunningTasks] > Slow Job completed in 5003 millisecs
[16:58:59][LongRunningTasks Thread:6][LongRunningTasks] > Very Slow Job completed in 7014 millisecs
[16:58:59][LongRunningTasks Thread:6][Job Scheduler] > All Jobs completed in 7111 milliseconds
[16:58:59][LongRunningTasks Thread:6][Main] > final yield to Main
[16:58:59][LongRunningTasks Thread:6][Main] > Main ==> Completed in 7262 milliseconds

现在将作业类型更改为Processor如下:

var quickjob = new JobRunner("Quick Job", 3, JobRunner.JobType.Processor);
var veryslowjob = new JobRunner("Very Slow Job", 7, JobRunner.JobType.Processor);
var slowjob = new JobRunner("Slow Job", 5, JobRunner.JobType.Processor);
var veryquickjob = new JobRunner("Very Quick Job", 2, JobRunner.JobType.Processor);

运行此命令时,您会看到所有操作都在Main Thread上按顺序运行。首先,您认为为什么?我们有多个可用线程,并且Scheduler展示了其在线程之间切换任务的能力。为什么不切换?

答案很简单。初始化JobRunnner对象后,我们一次将它们运行到一个Scheduler对象中。由于我们运行的代码是顺序的——不间断地计算素数——在第一个作业完成之前,我们不执行下一行代码(输入第二个作业)。

[17:59:48][Main Thread:1][Main] >  running on Application Thread
[17:59:48][Main Thread:1][LongRunningTasks] > Quick Job started
[17:59:53][Main Thread:1][LongRunningTasks] > Quick Job completed in 4355 millisecs
[17:59:53][Main Thread:1][LongRunningTasks] > Very Slow Job started
[17:59:59][Main Thread:1][LongRunningTasks] > Very Slow Job completed in 6057 millisecs
[17:59:59][Main Thread:1][LongRunningTasks] > Slow Job started
[18:00:03][Main Thread:1][LongRunningTasks] > Slow Job completed in 4209 millisecs
[18:00:03][Main Thread:1][LongRunningTasks] > Very Quick Job started
[18:00:05][Main Thread:1][LongRunningTasks] > Very Quick Job completed in 1737 millisecs
[18:00:05][Main Thread:1][Job Scheduler] > All Jobs Scheduled
[18:00:05][Main Thread:1][Job Scheduler] > Quick Jobs completed in 16441 milliseconds
[18:00:05][Main Thread:1][Job Scheduler] > All Jobs completed in 16441 milliseconds
[18:00:05][Main Thread:1][Main] > Job Scheduler yielded to Main
[18:00:05][Main Thread:1][Main] > final yield to Main
[18:00:05][Main Thread:1][Main] > Main ==> Completed in 16591 milliseconds

现在,将作业更改为可以运行YieldingProcessor。

var quickjob = new JobRunner("Quick Job", 3, JobRunner.JobType.YieldingProcessor);
var veryslowjob = new JobRunner("Very Slow Job", 7, JobRunner.JobType.YieldingProcessor);
var slowjob = new JobRunner("Slow Job", 5, JobRunner.JobType.YieldingProcessor);
var veryquickjob = new JobRunner("Very Quick Job", 2, JobRunner.JobType.YieldingProcessor);

结果是非常不同的。花费的时间将取决于计算机上处​​理器核心和线程的数量。您可以看到所有作业快速启动并在11秒内完成,最慢的作业需要9秒。此处的主要区别在于,处理器长时间运行的工作有规律地产生。这使调度程序有机会将工作分流到其他线程。

Yield处理器代码:

[17:50:12][Main Thread:1][Main] >  running on Application Thread
[17:50:12][Main Thread:1][LongRunningTasks] > Quick Job started
[17:50:12][Main Thread:1][LongRunningTasks] > Very Slow Job started
[17:50:12][Main Thread:1][LongRunningTasks] > Slow Job started
[17:50:12][Main Thread:1][LongRunningTasks] > Very Quick Job started
[17:50:12][Main Thread:1][Job Scheduler] > All Jobs Scheduled
[17:50:12][Main Thread:1][Main] > Job Scheduler yielded to Main
[17:50:16][LongRunningTasks Thread:7][LongRunningTasks] > Very Quick Job completed in 4131 millisecs
[17:50:18][LongRunningTasks Thread:7][LongRunningTasks] > Quick Job completed in 6063 millisecs
[17:50:18][LongRunningTasks Thread:7][Job Scheduler] > Quick Jobs completed in 6158 milliseconds
[17:50:21][LongRunningTasks Thread:6][LongRunningTasks] > Slow Job completed in 9240 millisecs
[17:50:23][LongRunningTasks Thread:9][LongRunningTasks] > Very Slow Job completed in 11313 millisecs
[17:50:23][LongRunningTasks Thread:9][Job Scheduler] > All Jobs completed in 11411 milliseconds
[17:50:23][LongRunningTasks Thread:9][Main] > final yield to Main
[17:50:23][LongRunningTasks Thread:9][Main] > Main ==> Completed in 11534 milliseconds

结论和总结

希望对您有帮助/信息丰富的?我在异步路线上的航行中学到了一些关键点,并在此处进行了演示:

  1. 一路异步等待。不要混用同步和异步方法。从底部开始——数据或流程接口——并通过数据和业务/逻辑层一直到UI一直进行代码异步。
  2. 如果不yield,您将无法异步运行。您必须给任务计划者一个机会!在Task中包装一些同步例程是在说而不是在做。
  3. 触发并遗忘void返回方法需要yield,以将控制权传递回调用方。它们的行为与任务返回方法没有什么不同。他们只是不为您返回任务以等待或监视进度。
  4. 如果您正在编写处理器密集型活动——建模、大量运算...请确保使其异步并在适当的位置yield。考虑将它们切换到任务池(请考虑以下注意事项)。测试不同的场景,没有一成不变的规则。
  5. 仅在UI中使用Task.Run,就在调用堆栈的顶部。永远不要在库中使用它。除非有充分的理由,否则不要使用它。
  6. 在awaits上使用记录和断点查看何时击中它们。您的代码以多快的速度回落到await外部是很好的响应能力指标。拿出你的外面await,看看你有多快掉到底部!
  7. 您可能已经注意到没有ContinueWith。我不经常使用它。通常,简单的await后跟延续代码可以达到相同的结果。我读过评论说它在处理上比较重,因为它创建了一个新任务,而等待/继续重复使用了相同的Task。我还没有对代码进行足够深入的研究。
  8. 始终使用Async和Await,不要花哨。
  9. 如果您的库同时提供了异步调用和同步调用,请分别对它们进行编码。“一次性编码”最佳实践不适用于此处。如果您不想在某个时候搬起石头砸自己的脚,就永远不要循环调用!

https://www.codeproject.com/Articles/5293229/A-Practical-Walkthrough-of-Async-Programming-in-Do

DotNetCore中异步编程的实用演练相关推荐

  1. 在Java中异步编程,同事非要用rxJava,被我一顿吐槽!

    在Java中异步编程,不一定非要使用rxJava, Java本身的库中的CompletableFuture可以很好的应对大部分的场景. 这篇文章介绍 Java 8 的 CompletionStage ...

  2. 【转】谈谈c#中异步编程模型的变迁**

    大家在编程过程中都会用到一些异步编程的情况.在c#的BCL中,很多api都提供了异步方法,初学者可能对各种不同异步方法的使用感到迷惑,本文主要为大家梳理一下异步方法的变迁以及如何使用异步方法. Beg ...

  3. 了解和使用DotNetCore和Blazor中的异步编程

    目录 介绍 您对异步编程了解什么? 那么,什么是异步编程? 我们什么时候应该使用它? 任务.线程.计划.上下文 到底是怎么回事? Asnyc编码模式 命名约定 异步任务模式 任务模式 事件模式 阻塞与 ...

  4. .NET中的异步编程(四)- IO完成端口以及FileStream.BeginRead

    本文首发在IT168 写这个系列原本的想法是讨论一下.NET中异步编程风格的变化,特别是F#中的异步工作流以及未来的.NET 5.0中的基于任务的异步编程模型.但经过三篇文章后很多人对IO异步背后实现 ...

  5. 「C#」异步编程玩法笔记-WinForm中的常见问题

    目录 1.异步更新界面 1.1.问题 1.2.解决问题 1.3.AsyncOperationManager和AsyncOperation 1.4.Invoke.BeginInvoke.EndInvok ...

  6. promise 的基本概念 和如何解决js中的异步编程问题 对 promis 的 then all ctch 的分析 和 await async 的理解

    * promise承诺 * 解决js中异步编程的问题 * * 异步-同步 * 阻塞-无阻塞 * * 同步和异步的区别? 异步;同步 指的是被请求者 解析:被请求者(该事情的处理者)在处理完事情的时候的 ...

  7. 第二十一章 异步编程

    异步编程的常规方法的问题是异步程序要么做完所有的事情,要么一件事也没有做完.重写所有的代码是为了保证程序不会阻塞,否则只是在浪费时间. -------Alvaro Videla & Jason ...

  8. 一起谈.NET技术,.NET异步编程:IO完成端口与BeginRead

    写这个系列原本的想法是讨论一下.NET中异步编程风格的变化,特别是F#中的异步工作流以及未来的.NET 5.0中的基于任务的异步编程模型.但经过前几篇文章(为什么需要异步,传统的异步编程,使用CPS及 ...

  9. 《CLR Via C# 第3版》笔记之(二十一) - 异步编程模型(APM)

    APM是.NET中异步编程模型的缩写(Asynchronous Programing Model). 通过异步编程,使得我们的程序可以更加高效的利用系统资源. 主要内容: 一个APM的例子 GUI中的 ...

最新文章

  1. 【小白的CFD之旅】16 流程
  2. Linux下SSH 客户端不用输入密码配置步骤
  3. docker pip 换源_Docker 部署 jupyterlab 3.0.3
  4. python中numpy模块的around方法_Python numpy.around()用法及代码示例
  5. iOS-给Category添加属性
  6. 我的pycharm+python常用快捷键(复习防遗忘版)
  7. HDU 2546 饭卡 动态规划01背包
  8. 基于Java毕业设计优课网设计与实现源码+系统+mysql+lw文档+部署软件
  9. 诺兰模型(百度百科)
  10. 《非暴力沟通》- 使人情意相通的沟通方式
  11. 笔记本连接显示器后没有声音_电脑连接HDMI显示器后没声音的解决办法
  12. 【FPGA】:ip核---乘法器(multiplier)
  13. 8个提高摸鱼效率的python自动化脚本,提高打工人幸福感~
  14. JavaScript常用的字符串操作对象方法
  15. fit 耐克外套storm_NIKE Storm-FIT面料
  16. arp同网段一条就能让对方断网攻击命令
  17. Excel如何使用CONCAT函数
  18. 免费的文件比较工具推荐一个
  19. 分享136个ASP源码,总有一款适合您
  20. C4D团队渲染联机渲染,异地电脑一起渲染,使用RS,OC等渲染器。

热门文章

  1. 深入java虚拟机需要读吗_《深入理解Java虚拟机》读后总结(一)JVM内存模型
  2. java 实体字段变更记录_java – Hibernate:检查哪个实体的字段被修改
  3. 圣诞节插画素材|设计师再忙也要看一看,没准可以帮助到你!
  4. UI素材资源|Material风格的插图,有品位的素材
  5. 可临摹素材,分层可编辑一步一步教你,肯定能把表单做好
  6. HTML5导航栏菜单的设计与实现
  7. Madagascar中的宏定义--圆周率PI
  8. ASN.1编解码:asn1c-ORAN-E2AP
  9. 论肱二头肌在日常生活中的锻炼的持久战|健身达人
  10. C语言动态库libxxx.so的几种使用方法