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

news2025/1/19 3:46:42

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配置任务延续了,也没必要获取等待器了,这个状态机也就执行这一次。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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

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

【Python通过UDP协议传输视频数据】(界面识别)

提示&#xff1a;界面识别项目 前言 随着网络通信技术的发展&#xff0c;视频数据的实时传输在各种场景中得到了广泛应用。UDP&#xff08;User Datagram Protocol&#xff09;作为一种无连接的协议&#xff0c;凭借其低延迟、高效率的特性&#xff0c;在实时性要求较高的视频…

深度学习中的张量 - 使用PyTorch进行广播和元素级操作

深度学习中的张量 - 使用PyTorch进行广播和元素级操作 元素级是什么意思&#xff1f; 元素级操作在神经网络编程中与张量的使用非常常见。让我们从一个元素级操作的定义开始这次讨论。 一个_元素级_操作是在两个张量之间进行的操作&#xff0c;它作用于各自张量中的相应元素…

几个Linux系统安装体验(续): 中科方德服务器系统

本文介绍中科方德服务器系统&#xff08;NFSDesktop&#xff09;的安装。 下载 下载地址&#xff1a; https://www.nfschina.com/index.php?catid68 下载文件&#xff1a;本文下载的文件名称为NFSCNS-4.0-G330-x86_64-241128.iso。 下载注意事项&#xff1a;无法直接下载&…

浅谈计算机网络03 | 现代网络组成

现代网络组成 一 、网络生态体系1.1网络生态系统的多元主体1.2 网络接入设施的多样类型 二、现代网络的典型体系结构解析三、高速网络技术3.1 以太网技术3.2 Wi-Fi技术的深度剖析3.2.1 应用场景的多元覆盖3.2.2 标准升级与性能提升 3.3 4G/5G蜂窝网的技术演进3.3.1 蜂窝技术的代…

JavaWeb 前端基础 html + CSS 快速入门 | 018

今日推荐语 指望别人的救赎&#xff0c;势必走向毁灭——波伏娃 日期 学习内容 打卡编号2025年01月17日JavaWeb 前端基础 html CSS018 前言 哈喽&#xff0c;我是菜鸟阿康。 今天 正式进入JavaWeb 的学习&#xff0c;简单学习 html CSS 这2各前端基础部分&am…

内网渗透测试工具及渗透测试安全审计方法总结

1. 内网安全检查/渗透介绍 1.1 攻击思路 有2种思路&#xff1a; 攻击外网服务器&#xff0c;获取外网服务器的权限&#xff0c;接着利用入侵成功的外网服务器作为跳板&#xff0c;攻击内网其他服务器&#xff0c;最后获得敏感数据&#xff0c;并将数据传递到攻击者&#xff0…

Git 安装 操作 命令 远程仓库 多人协作

Git作用 Git诞生史 很多人都知道&#xff0c;Linus在1991年创建了开源的Linux&#xff0c;从此&#xff0c;Linux系统不断发展&#xff0c;已经成为最大的服务器系统软件了。Linus虽然创建了Linux&#xff0c;但Linux的壮大是靠全世界热心的志愿者参与的&#xff0c;这么多人在…

Mockito+PowerMock+Junit单元测试

一、单元测试用途 1、日常开发团队要求规范&#xff0c;需要对开发需求代码进行单元测试并要求行覆盖率达到要求&#xff0c;DevOps流水线也会开设相关门禁阀值阻断代码提交&#xff0c;一般新增代码行覆盖率80%左右。 二、Mock测试介绍 1、Mock是为了解决不同的单元之间由于…

2024CVPR《HomoFormer》

这篇论文提出了一种名为HomoFormer的新型Transformer模型,用于图像阴影去除。论文的主要贡献和创新点如下: 1. 研究背景与动机 阴影去除的挑战:阴影在自然场景图像中普遍存在,影响图像质量并限制后续计算机视觉任务的性能。阴影的空间分布不均匀且模式多样,导致传统的卷积…

JavaEE之CAS

上文我们认识了许许多多的锁&#xff0c;此篇我们的CAS就是从上文的锁策略开展的新概念&#xff0c;我们来一探究竟吧 1. 什么是CAS&#xff1f; CAS: 全称Compare and swap&#xff0c;字⾯意思:“比较并交换”&#xff0c;⼀个CAS涉及到以下操作&#xff1a; 我们假设内存中…

国产编辑器EverEdit - 复制为RTF

1 复制为RTF 1.1 应用背景 在写产品手册或者其他文档时&#xff0c;可能会用到要将产品代码以样例的形式放到文档中&#xff0c;一般的文本编辑器拷贝粘贴到Word中也就是普通文本&#xff0c;没有语法着色&#xff0c;这样感观上不是太好&#xff0c;为了让读者的感观更好一点…

LLM - 大模型 ScallingLaws 的 C=6ND 公式推导 教程(1)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/145185794 Scaling Laws (缩放法则) 是大模型领域中&#xff0c;用于描述 模型性能(Loss) 与 模型规模N、数据量D、计算资源C 之间关系的经验规律…

CSS认识与实践

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

vue项目引入阿里云svg资源图标

1&#xff1a;生成svg图标 登录阿里云官网 1.1 创建项目组 1.2 从阿里云网站上面获取喜欢的图标加入到已有的项目组 1.3 如果团队有自己的设计师&#xff0c;也可以让设计师上传自己的svg图标到阿里云指定的项目组&#xff1b; 使用的时候&#xff0c;把 资源包下载到本地项…

Redis 中 TTL 的基本知识与禁用缓存键的实现策略(Java)

目录 前言1. 基本知识2. Java代码 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 单纯学习Redis可以看我前言的Java基本知识路线&#xff01;&#xff01; 对于Java的基本知识推荐阅读&#xff1a; java框架…

使用nginx搭建通用的图片代理服务器,支持http/https/重定向式图片地址

从http切换至https 许多不同ip的图片地址需要统一进行代理 部分图片地址是重定向地址 nginx配置 主站地址&#xff1a;https://192.168.123.100/ 主站nginx配置 server {listen 443 ssl;server_name localhost;#ssl证书ssl_certificate ../ssl/ca.crt; #私钥文件ssl_ce…

Unix 与 Linux 深度应用与对比分析

文章目录 引言Unix 与 Linux 的相似之处1. 设计理念2. 文件系统结构3. 命令行界面4. 多用户多任务支持 Unix 与 Linux 的不同之处1. 开源性2. 内核架构3. 应用场景4. 发行版 Unix 和 Linux 的开发语言1. C 语言2. 脚本语言 在 Unix 和 Linux 上开发简单应用示例1. 编写一个简单…

事务机制及Spring事务管理

事务概览 事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位。 事务会将所有的操作作为一个整体一起向系统提交或撤销操作请求&#xff0c;换句话说&#xff1a;这些操作要么同时成功、要么同时失败。 具体案例 我们先看一个需求&#xff1a;现在有两张数据库表&…

48.【6】BUUCTF WEB BabySQL

进入靶场 order by 判断字节数 输入内容是 1 order by 2# 显示图片内容&#xff0c;知被过滤了 一般最简单的绕过方法是双写或大小写 尝试双写 It is ok continue 经过多次尝试&#xff0c;4时异常&#xff0c;所以字节数是3 union select都被过滤了 双写解决&#xff0c;成…