C#异步多线程——浅谈async/await底层原理

async/await是块语法糖,编译器帮助我们做了很多工作,下面我们就简单剖析一下async/await的底层原理。

反编译工具ILSpy安装

我用的是ILSpy反编译生成的dll程序集。还没有ILSpy工具的小伙伴可以直接在VS中安装;点击Extensions=>Manage Extensions,搜索ILSpy,按步骤下载安装即可,重启VS在Tool中打开就可以使用了;有可能我们用的.NET版本低,提示需要安装一个高版本的运行时环境,按照步骤下载安装就行,非常简单。
在这里插入图片描述
使用时注意把C#的版本换的低一些,使用低版本我们才方便看到更多细节;视图设置为显示所有类型和成员。
在这里插入图片描述

入门分析

分析源码本身就是一件需要细心,耐心,又极度枯燥的事,尤其是接下来我们要看的代码是反编译出来的编译器给我们生成的很底层的代码,这不像我们自己写程序还可以加点打印,或者设置个断点去调试下,一旦if语句一多,可能程序该进哪个分支我们都要蒙圈了,代码追的越深越难理解。我们不是“学院派”,先摆正自己的目的,我们要的是比使用更高一个层次,简单了解下背后的原理即可。

简单示例

我是基于.NET6创建了一个控制台项目,不使用顶级语法。项目非常简单,没有什么实际意义,就是为了展示底层原理。

static async Task Main(string[] args)
{
    Console.WriteLine("Project start!");
    await TestAsync();

    Console.WriteLine("TestAsync执行结束");
    await Task.Delay(1000);

    Console.WriteLine("等待1s");
    await Task.Delay(2000);

    Console.WriteLine("Project end!");
}

static Task TestAsync()
{
    return Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("TestAsync");
    });
}

异步Main

[SpecialName]
[DebuggerStepThrough]
private static void <Main>(string[] args)
{
	Main(args).GetAwaiter().GetResult();
}

[AsyncStateMachine(typeof(<Main>d__0))]
[DebuggerStepThrough]
private static Task Main(string[] args)
{
	<Main>d__0 stateMachine = new <Main>d__0();
	stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
	stateMachine.args = args;
	stateMachine.<>1__state = -1;
	stateMachine.<>t__builder.Start(ref stateMachine);
	return stateMachine.<>t__builder.Task;
}
  • 怎么有两个Main?
    写过异步方法的都知道,如果方法内使用了await,方法声明就必须用async修饰,编译器为了能让我们在Main方法中调用异步方法,也是煞费苦心,直接搞出两个Main,一个是我们熟悉的void Main,另一个是我们项目中的Task Main,编译器在入口Main中调用了一下我们的异步Main。
  • Main方法中怎么跟我们的业务完全不同?
    我们来看看Main方法中干了什么事。
    • 创建了一个类型为<Main>d__0 的状态机 stateMachine。
    • 初始化了一些成员变量:
      • <>t__builder:异步Main方法的核心,负责异步操作,相当于引擎,提供Start方法启动状态机
      • <>1__state:状态机当前状态
    • 调用Sart启动状态机执行我们的异步方法。
  • 所以我们真正的业务就在这个stateMachine状态机中,了解状态机的应该都知道,状态机是一个被多次调用的程序,通过切换状态来决定具体执行哪部分代码。

Start启动状态机

//AsyncTaskMethodBuilder结构体
public void Start<[Nullable(0)] TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	AsyncMethodBuilderCore.Start(ref stateMachine);
}

public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	if (stateMachine == null)
	{
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
	}
	Thread currentThread = Thread.CurrentThread;
	ExecutionContext executionContext = currentThread._executionContext;
	SynchronizationContext synchronizationContext = currentThread._synchronizationContext;
	try
	{
		stateMachine.MoveNext();
	}
	finally
	{
		if (synchronizationContext != currentThread._synchronizationContext)
		{
			currentThread._synchronizationContext = synchronizationContext;
		}
		ExecutionContext executionContext2 = currentThread._executionContext;
		if (executionContext != executionContext2)
		{
			ExecutionContext.RestoreChangedContextToThread(currentThread, executionContext, executionContext2);
		}
	}
}

这里我们能看懂的就是这句stateMachine.MoveNext();,下面我们重点看一下MoveNext ,这才是真正的状态机处理方法。

MoveNext

private sealed class <Main>d__0 : IAsyncStateMachine
{
	public int <>1__state;
	public AsyncTaskMethodBuilder <>t__builder;
	public string[] args;
	private TaskAwaiter <>u__1;

	private void MoveNext()
	{
		//在异步Main方法中我们初始化<>1__state=-1
		int num = <>1__state;
		try
		{
			TaskAwaiter awaiter3;
			TaskAwaiter awaiter2;
			TaskAwaiter awaiter;
			switch (num)
			{
			default:
				Console.WriteLine("Project start!");
				//TaskAwaiter是个很重要的对象,用来监测TestAsync的运行状态
				//TestAsync只是启动个线程去执行其它任务,这里不会等待,程序继续向下执行
				awaiter3 = TestAsync().GetAwaiter();
				//一般来说异步任务比较耗时,大概率程序会进入该分支				
				if (!awaiter3.IsCompleted)
				{
					//状态机状态从-1变为0				
					num = (<>1__state = 0);
					<>u__1 = awaiter3;
					<Main>d__0 stateMachine = this;
					//这里很重要,用于配置TestAsync完成后的延续					
					<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
					return;
				}
				goto IL_008a;
			case 0:
				awaiter3 = <>u__1;
				<>u__1 = default(TaskAwaiter);
				num = (<>1__state = -1);
				goto IL_008a;
			case 1:
				awaiter2 = <>u__1;
				<>u__1 = default(TaskAwaiter);
				num = (<>1__state = -1);
				goto IL_00f9;
			case 2:
				{
					awaiter = <>u__1;
					<>u__1 = default(TaskAwaiter);
					num = (<>1__state = -1);
					break;
				}
				IL_00f9:
				awaiter2.GetResult();
				Console.WriteLine("等待1s");
				awaiter = Task.Delay(2000).GetAwaiter();
				if (!awaiter.IsCompleted)
				{
					num = (<>1__state = 2);
					<>u__1 = awaiter;
					<Main>d__0 stateMachine = this;
					<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
					return;
				}
				break;
				IL_008a:
				awaiter3.GetResult();
				Console.WriteLine("TestAsync执行结束");
				awaiter2 = Task.Delay(1000).GetAwaiter();
				if (!awaiter2.IsCompleted)
				{
					num = (<>1__state = 1);
					<>u__1 = awaiter2;
					<Main>d__0 stateMachine = this;
					<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
					return;
				}
				goto IL_00f9;
			}
			awaiter.GetResult();
			Console.WriteLine("Project end!");
		}
		catch (Exception exception)
		{
			<>1__state = -2;
			<>t__builder.SetException(exception);
			return;
		}
		<>1__state = -2;
		<>t__builder.SetResult();
	}

	void IAsyncStateMachine.MoveNext()
	{
		this.MoveNext();
	}
}

接下来我们对MoveNext的调用进行梳理;
MoveNext的第一次调用:

  • 开始状态机状态为-1,程序先进入switch中的default分支,这里我们看到了我们所写的第一句代码Console.WriteLine("Project start!");
  • 接着执行异步方法TestAsync(),获得一个等待器(TaskAwaiter),这个对象也非常重要,里面有个Task类型的变量m_task,保存了异步方法返回的Task对象。
  • 我们在TestAsync里进行了Sleep,比较耗时,不会立即完成所以会进入if (!awaiter3.IsCompleted)分支
    • 切换状态机状态:<>1__state = 0
    • 调用AwaitUnsafeOnCompleted方法,参数传入等待器和状态机对象,用于配置TestAsync完成后的延续,也就是再次调用MoveNext。
    • 最后return,也就是第一次调用MoveNext结束了。

这里我们小结一下,第一次状态机的调用对应我们的代码,执行了前两句

Console.WriteLine("Project start!");
await TestAsync();

MoveNext的第二次调用:

  • 此时状态机状态为0,进入case 0分支,这里又对状态机状态初始化为了<>1__state = -1,然后goto跳转到I L_008a
  • 终于又执行了我们写的下一行代码Console.WriteLine("TestAsync执行结束");
  • 接着执行Task.Delay(1000),获得等待器
  • 很显然执行这个任务要1s钟,不会立即完成,所以会进入if (!awaiter2.IsCompleted)分支
    • 切换状态机状态:<>1__state = 1
    • 调用AwaitUnsafeOnCompleted方法,参数传入等待器和状态机对象,用于配置Task.Delay(1000)完成后的延续,也就是再次调用MoveNext。
    • 最后return,也就是第二次调用MoveNext结束了。

这里我们小结一下,第二次状态机的调用对应我们的代码,执行了:

Console.WriteLine("TestAsync执行结束");
await Task.Delay(1000);

MoveNext的第三次调用:

  • 此时状态机状态为1,进入case 1分支,这里又对状态机状态初始化为了<>1__state = -1,然后goto跳转到 IL_00f9
  • 又执行了我们写的下一行代码Console.WriteLine("等待1s");
  • 接着执行Task.Delay(2000),获得等待器
  • 很显然执行这个任务要2s钟,不会立即完成,所以会进入if (!awaiter.IsCompleted)分支
    • 切换状态机状态:<>1__state = 2
    • 调用AwaitUnsafeOnCompleted方法,参数传入等待器和状态机对象,用于配置Task.Delay(2000)完成后的延续,也就是再次调用MoveNext。
    • 最后return,也就是第三次调用MoveNext结束了。

这里我们小结一下,第三次状态机的调用对应我们的代码,执行了:

Console.WriteLine("等待1s");
await Task.Delay(2000);

MoveNext的第四次调用:

  • 此时状态机状态为2,进入case 2分支,这里又对状态机状态初始化为了<>1__state = -1,然后break跳出switch
  • 执行我们写的最后一句代码Console.WriteLine("Project end!");
  • 最后切换状态机状态:<>1__state = -2,代码执行完了,不需要再配置延续了,MoveNext也不会再被调用了。

这里我们小结一下,第四次状态机的调用对应我们的代码,执行了:

Console.WriteLine("Project end!");

所以上面对MoveNext的四次调用对应到我们的代码执行为:
在这里插入图片描述

总结

  • async方法会被C#编译器编译成一个状态机类,根据await调用进行切分成多个状态,对async方法的调用会被拆分为多次对MoveNext的调用。

async方法不启用多线程

一看到异步自然而然就会和多线程关联起来,那我们就是要写一个不使用多线程的async方法,看看底层又做了什么。

简单示例

static async Task Main(string[] args)
{
    Console.WriteLine("Project start!");
    await TestFakeAsync();

    Console.WriteLine("TestAsync执行结束");
    await Task.Delay(1000);

    Console.WriteLine("等待1s");
    await Task.Delay(2000);

    Console.WriteLine("Project end!");
}

static async Task TestFakeAsync()
{
    Thread.Sleep(1000);
    Console.WriteLine("TestFakeAsync");
}

MoveNext

我们的异步Main方法仍然加了async,方法内也使用了await调用,所以对异步Main的处理和上面入门分析的执行逻辑没什么不同。前面的启动过程就不列举了,我们直接看下MoveNext。

private void MoveNext()
{
	int num = <>1__state;
	try
	{
		TaskAwaiter awaiter3;
		TaskAwaiter awaiter2;
		TaskAwaiter awaiter;
		switch (num)
		{
		default:
			Console.WriteLine("Project start!");
			//TestFakeAsync没有启动多线程,内部是同步执行,比较耗时
			awaiter3 = TestFakeAsync().GetAwaiter();
			//TestFakeAsync返回时方法是执行完成的,所以不会进入分支
			if (!awaiter3.IsCompleted)
			{
				num = (<>1__state = 0);
				<>u__1 = awaiter3;
				<Main>d__0 stateMachine = this;
				<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
				return;
			}
			goto IL_008a;
		case 0:
			awaiter3 = <>u__1;
			<>u__1 = default(TaskAwaiter);
			num = (<>1__state = -1);
			goto IL_008a;
		case 1:
			awaiter2 = <>u__1;
			<>u__1 = default(TaskAwaiter);
			num = (<>1__state = -1);
			goto IL_00f9;
		case 2:
			{
				awaiter = <>u__1;
				<>u__1 = default(TaskAwaiter);
				num = (<>1__state = -1);
				break;
			}
			IL_00f9:
			awaiter2.GetResult();
			Console.WriteLine("等待1s");
			awaiter = Task.Delay(2000).GetAwaiter();
			if (!awaiter.IsCompleted)
			{
				num = (<>1__state = 2);
				<>u__1 = awaiter;
				<Main>d__0 stateMachine = this;
				<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
				return;
			}
			break;
			IL_008a:
			awaiter3.GetResult();
			Console.WriteLine("TestAsync执行结束");
			awaiter2 = Task.Delay(1000).GetAwaiter();
			if (!awaiter2.IsCompleted)
			{
				num = (<>1__state = 1);
				<>u__1 = awaiter2;
				<Main>d__0 stateMachine = this;
				<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
				return;
			}
			goto IL_00f9;
		}
		awaiter.GetResult();
		Console.WriteLine("Project end!");
	}
	catch (Exception exception)
	{
		<>1__state = -2;
		<>t__builder.SetException(exception);
		return;
	}
	<>1__state = -2;
	<>t__builder.SetResult();
}

眼睛都看疼了,除了调用的异步方法名字改了和上面入门分析的MoveNext完全一样,那我们就来好好看看这个TestFakeAsync。

TestFakeAsync

private static Task TestFakeAsync()
{
	<TestFakeAsync>d__1 stateMachine = new <TestFakeAsync>d__1();
	stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
	stateMachine.<>1__state = -1;
	stateMachine.<>t__builder.Start(ref stateMachine);
	return stateMachine.<>t__builder.Task;
}

private sealed class <TestFakeAsync>d__1 : IAsyncStateMachine
{
	public int <>1__state;
	public AsyncTaskMethodBuilder <>t__builder;

	private void MoveNext()
	{
		int num = <>1__state;
		try
		{
			Thread.Sleep(1000);
			Console.WriteLine("TestFakeAsync");
		}
		catch (Exception exception)
		{
			<>1__state = -2;
			<>t__builder.SetException(exception);
			return;
		}
		<>1__state = -2;
		<>t__builder.SetResult();
	}

	void IAsyncStateMachine.MoveNext()
	{
		this.MoveNext();
	}
}

TestFakeAsync也是有async关键字修饰的,所以编译器也同样把它处理为状态机,同样通过stateMachine.<>t__builder.Start(ref stateMachine);启动状态机,并且第一次调用MoveNext。接下来我们对MoveNext的调用进行梳理;

  • 开始状态机状态为-1,程序开始执行我们的代码Thread.Sleep(1000);,接着同步执行第二句Console.WriteLine("TestFakeAsync");
  • 最后状态切换为<>1__state = -2;

总结

  • 异步方法TestFakeAsync,没有启动多线程,里面没有用到await,代码没有分块处理,所以反编译我们看到也没有调用AwaitUnsafeOnCompleted配置任务延续,虽然被编译为状态机,但MoveNext只调用一次,TestFakeAsync方法内的代码都是同步执行,直到结束。

  • 我们看下TestFakeAsync同步执行导致的连锁反应:

    • 首先,因为同步执行,耗时操作都在TestFakeAsync状态机的MoveNext里,所以执行stateMachine.<>t__builder.Start(ref stateMachine);会比较耗时,不会立即完成;
    • 最后return stateMachine.<>t__builder.Task;返回Task结果比较慢。
    • 异步Main状态机中的MoveNext执行awaiter3 = TestFakeAsync().GetAwaiter();就不会立即拿到等待器
    • 拿到等待器,表示任务已经执行完了所以异步Main中第一次调用MoveNext不会进入if (!awaiter3.IsCompleted)分支;
    • 这异步Main中用了await相当于白用了,代码还是同步执行了。
  • 我们对比看下入门分析例子中的TestAsync的连锁反应:

    • TestAsync里面使用Task.Run启用了子线程,耗时任务在子线程中执行,但Run方法是立即返回的
    • 所以异步Main中调用MoveNext执行awaiter3 = TestAsync().GetAwaiter();是立即拿到等待器的。
    • 因为等待器提前拿到,而耗时任务在子线程中执行还没结束,所以第一次调用MoveNext会进入if (!awaiter3.IsCompleted)分支;
  • 刚开始接触async/await的小伙伴可能有个理解上的误区,认为加上async既然是异步编译器会自动帮我们把程序封装到一个子线程中执行,其实async不等于多线程:异步方法的代码并不会自动在新的线程中执行,除非手动把代码放到新线程中执行。

  • 看了上面的分析和例子,你还会给一个同步方法加上async吗?应该不会了,加上asyc编译器生成了那么多代码,执行起来还是同步的,是不是更影响效率了。

async方法不使用await

What?这么简单还要分析?直接贴出来代码方便和上面对比,剩下的自己分析吧,哈哈。

static async Task Main(string[] args)
{
    Console.WriteLine("Project start!");
    TestNoAwaitAsync();

    Console.WriteLine("TestAsync执行结束");
    Task.Delay(1000);

    Console.WriteLine("等待1s");
    Task.Delay(2000);

    Console.WriteLine("Project end!");
}

static Task TestNoAwaitAsync()
{
    return Task.Run(() =>
    {
        Thread.Sleep(1000);
        Console.WriteLine("TestAsync");
    });
}

直接上反编译:

private void MoveNext()
{
	int num = <>1__state;
	try
	{
		Console.WriteLine("Project start!");
		TestAsync();
		Console.WriteLine("TestAsync执行结束");
		Task.Delay(1000);
		Console.WriteLine("等待1s");
		Task.Delay(2000);
		Console.WriteLine("Project end!");
	}
	catch (Exception exception)
	{
		<>1__state = -2;
		<>t__builder.SetException(exception);
		return;
	}
	<>1__state = -2;
	<>t__builder.SetResult();
}

是不是很简单,没有await就不会调用AwaitUnsafeOnCompleted配置任务延续了,也没必要获取等待器了,这个状态机也就执行这一次。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ThinkPHP 8的一对多关联

【图书介绍】《ThinkPHP 8高效构建Web应用》-CSDN博客 《2025新书 ThinkPHP 8高效构建Web应用 编程与应用开发丛书 夏磊 清华大学出版社教材书籍 9787302678236 ThinkPHP 8高效构建Web应用》【摘要 书评 试读】- 京东图书 使用VS Code开发ThinkPHP项目-CSDN博客 编程与应用开…

npm的包管理

从哪里下载包 国外有一家 IT 公司&#xff0c;叫做 npm,Inc.这家公司旗下有一个非常著名的网站: https://www.npmjs.com/&#xff0c;它是全球最大的包共享平台&#xff0c;你可以从这个网站上搜索到任何你需要的包&#xff0c;只要你有足够的耐心!到目前位置&#xff0c;全球约…

【python】OpenCV—Extract Horizontal and Vertical Lines—Morphology

文章目录 1、功能描述2、代码实现3、效果展示4、完整代码5、参考 更多有趣的代码示例&#xff0c;可参考【Programming】 1、功能描述 基于 opencv-python 库&#xff0c;利用形态学的腐蚀和膨胀&#xff0c;提取图片中的水平或者竖直线条 2、代码实现 导入基本的库函数 im…

《Keras 3 在 TPU 上的肺炎分类》

Keras 3 在 TPU 上的肺炎分类 作者&#xff1a;Amy MiHyun Jang创建日期&#xff1a;2020/07/28最后修改时间&#xff1a;2024/02/12描述&#xff1a;TPU 上的医学图像分类。 &#xff08;i&#xff09; 此示例使用 Keras 3 在 Colab 中查看 GitHub 源 简介 设置 本教程将介…

CSS认识与实践

目录 CSS 是什么 基本语法规范 引入方式 内部样式表 行内样式表 外部样式 空格规范 选择器 选择器的功能 选择器的种类 基础选择器 标签选择器 类选择器 id 选择器 通配符选择器 基础选择器小结 复合选择器 后代选择器 子选择器 并集选择器 伪类选择器 复合…

Windows环境:使用命令行脚本批量发送http请求

因为服务器Windows版本问题&#xff0c;无法使用curl&#xff0c;所以只能使用wget。 C:\Windows\System32\wget.exe 需求背景&#xff1a; 传入客户端参数&#xff0c;请求服务端。 将请求参数保存到文本文件中&#xff0c;命令行读取文本文件&#xff0c;然后分割字符串&am…

Logback日志技术

Logback日志技术 日志 日志&#xff08;Logging&#xff09;是软件开发和运维中用于记录系统或应用程序运行期间发生的运行信息、状态变化、错误信息等的一种机制&#xff0c;这种记录的方式就好像我们日常生活中写日记一样。它提供了一种持久化的方式&#xff0c;使得开发者…

6. 快速掌握抽象类及接口

目录 1. 抽象类1.1 抽象类语法1.2 抽象类特性1.3 抽象类的作用 2. 接口2.1 接口语法2.2 接口的特性 3. 接口案例4. 常用接口4.1 Comparable接口---compareTo()方法4.2 clonable接口---clone方法4.2 深拷贝和浅拷贝 5. Object类5.1 equals()方法5.2 toString()方法5.3 hashCode(…

womb子宫一词解趣

英语单词 womb&#xff0c;是“子宫”的意思&#xff0c;这个单词非常有趣&#xff0c;下面我们来说说它有趣在什么地方。 首先&#xff0c;womb 这个单词&#xff0c;大体可以分成两个部分&#xff0c;即wom-和b-&#xff0c;其中wom-可对应子宫的子&#xff0c;b-可对应子宫…

Spring Cloud概述

&#xff08;一&#xff09;定义 Spring Cloud是一个基于Spring Boot实现的云应用开发工具&#xff0c;它为基于JVM的云应用开发中涉及的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一套完整的解决方案…

年后找工作需要注意的事项

大家好&#xff01;我是 [数擎 AI]&#xff0c;一位热爱探索新技术的前端开发者&#xff0c;在这里分享前端和 Web3D、AI 技术的干货与实战经验。如果你对技术有热情&#xff0c;欢迎关注我的文章&#xff0c;我们一起成长、进步&#xff01; 开发领域&#xff1a;前端开发 | A…

windows远程桌面连接限定ip

1&#xff0c;Windows防火墙->高级设置->远程桌面 - 用户模式(TCP-In)->作用域->远程IP地址 2&#xff0c;启用规则

电脑换固态硬盘

参考&#xff1a; https://baijiahao.baidu.com/s?id1724377623311611247 一、根据尺寸和缺口可以分为以下几种&#xff1a; 1、M.2 NVME协议的固态 大部分笔记本是22x42MM和22x80MM nvme固态。 在京东直接搜&#xff1a; M.2 2242 M.2 2280 2、msata接口固态 3、NGFF M.…

3.无重复字符的最长字串--力扣

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长 子串 的长度。 示例 1: 输入: s “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”&#xff0c;所以其长度为 3。 示例 2: 输入: s “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “…

西门子【Library of Basic Controls (LBC)基本控制库”(LBC) 提供基本控制功能】

AF架构中使用的库 文章目录 Table of contents Legal information ..............................................................................................................................2 1 Introduction ................................................…

Golang Gin系列-2:搭建Gin 框架环境

开始网络开发之旅通常是从选择合适的工具开始的。在这个全面的指南中&#xff0c;我们将引导你完成安装Go编程语言和Gin框架的过程&#xff0c;Gin框架是Go的轻量级和灵活的web框架。从设置Go工作空间到将Gin整合到项目中&#xff0c;本指南是高效而强大的web开发路线图。 安装…

Visual Studio Community 2022(VS2022)安装方法

废话不多说直接上图&#xff1a; 直接上步骤&#xff1a; 1&#xff0c;首先可以下载安装一个Visual Studio安装器&#xff0c;叫做Visual Studio installer。这个安装文件很小&#xff0c;很快就安装完成了。 2&#xff0c;打开Visual Studio installer 小软件 3&#xff0c…

《offer 来了:Java 面试核心知识点精讲 -- 原理篇》

在 Java 面试的战场上&#xff0c;只知皮毛可不行&#xff0c;面试官们越来越看重对原理的理解。今天就给大家分享一本能让你在面试中脱颖而出的 “武林秘籍”——《offer 来了&#xff1a;Java 面试核心知识点精讲 -- 原理篇》。 本书详细介绍了Java架构师在BAT和移动互联网公…

1,Linux环境变量基本定义(基于Ubuntu示例进行讲解)

linux环境变量的概念 Linux环境变量&#xff08;准确说应该是shell变量&#xff09;&#xff0c;是直接存储在操作系统中的一组键值对&#xff08;dict类型&#xff09;&#xff0c;用于配置系统和应用程序的操作行为。 【有经验的描述】&#xff1a;它们的工作原理很简单&am…

5、docker-compose和docker-harbor

安装部署docker-compose 自动编排工具&#xff0c;可以根据dockerfile自动化的部署docker容器。是yaml文件格式&#xff0c;注意缩进。 1、安装docker-compose 2、配置compose配置文件docker-compose.yml 3、运行docker-compose.yml -f&#xff1a;指定文件&#xff0c;up&…