可重入函数与线程安全

2023-11-10

指令乱序和线程安全

先来看什么是指令乱序问题以及为什么有指令乱序。程序的代码执行顺序有可能被编译器或CPU根据某种策略打乱指令执行顺序,目的是提升程序的执行性能,让程序的执行尽可能并行,这就是所谓指令乱序问题。理解指令乱序的策略是很重要的,因为软件设计人员可以在正确的位置告诉编译器或CPU哪里可以允许指令乱序,哪里不能接受指令乱序,从而在保证软件正确性的同时允许编译或执行层面的性能优化。

指令乱序问题需要分为三个层次:

  • 第1层是多线程编程中的业务逻辑层面的函数可重入性和线程安全问题;
  • 第2层是编译器编译优化造成的指令乱序;
  • 第3层是CPU乱序执行指令的问题。

我们在讨论CPU指令乱序问题和编译器指令乱序问题之前,先来简要讨论一下可重入函数与线程安全相关的问题。

可重入函数与线程安全

线程的基本概念

线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。

操作系统中的线程概念也被延伸到CPU硬件上,多线程CPU就是在一个CPU上支持同时运行多个指令流,而多核CPU就是在一块芯片上集成了多个CPU核,比如4核8线程CPU芯片就是在集成了4个CPU核,每个CPU核上支持2个线程。

有了多核多线程CPU,操作系统就可以让不同进程运行在不同的CPU核的不同线程上,从而大大减少进程调度进程切换的资源消耗。传统上操作系统工作在单核单线程CPU上是通过分时共享CPU来模拟出多个指令执行流,从而实现多进程和多线程的。

函数调用堆栈框架

借助函数调用堆栈可以将我们写的函数调用代码整理成一个顺序执行的指令流,也就是一个线程,每一个线程都有一个独自拥有的函数调用堆栈空间,其中函数参数和局部变量都存储在函数调用堆栈空间中,因此函数参数和局部变量也是线程独自拥有的。除了函数调用堆栈空间,同一个进程的多个线程是共享其他进程资源的,比如全局变量是多个线程共享的。

可重入函数

可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。

int g = 0;
int function()
{
    g++; /* switch to another thread */
    printf("%d", g);
}

int function2(int a)
{
   a++;
   printf("%d", a); 
}

function()函数为不可重入函数,其中的变量g为全局变量,多个线程同时执行function函数时会出现变量g的值未按照预想的结果输出的情况,function2(int a)为可重入函数,function2函数中的变量a是对传入的实参变量的拷贝,并不影响原来传入的变量。

可重入函数的基本要求

(1)不为连续的调用持有静态数据;     

(2)不返回指向静态数据的指针;     

(3)所有数据都由函数的调用者提供;     

(4)使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;  

(5)使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突;     

(6)绝不调用任何不可重入函数。

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

函数的可重入性与线程安全之间的关系

可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;同一个可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;不可重入的函数一定不是线程安全的。

int g = 0;
int plus()
{
    pthread_mutex_lock(&gplusplus);
    g++; /* switch to another thread */
    printf("%d", g);
    pthread_mutex_unlock(&gplusplus);
}
int minus()
{
    pthread_mutex_lock(&gminusminus);
    g--; /* switch to another thread */
    printf("%d", g);
    pthread_mutex_unlock(&gminusminus);
}

上述两个函数plus() 和 minus() 经过加锁处理之后均称为可重入函数,但由于使用的是两个不同的互斥锁,所以在并发执行时会出现g的值与预期不一致的现象。故说明可重入函数不一定是线程安全的。

线程安全和指令乱序

我们这里讨论可重入函数与线程安全本质上也是指令乱序执行问题,指令乱序问题本质上也是线程安全问题,编译器编译优化或CPU指令乱序执行所引发的程序正确性问题尽管所处的层次不同但本质上与此相似,接下来我们分别讨论一下CPU指令乱序问题和编译器指令乱序问题。

CPU的流水线技术能够让指令的执行尽可能地并行起来,但是如果两条指令前后存在依赖关系,比如数据依赖、控制依赖等,此时后一条指令就必需等到前一条指令完成后才能开始执行。为了提高流水线的运行效率,CPU会对无依赖的前后指令做适当的乱序和调整,对控制依赖的指令做分支预测,对内存访问等耗时操作提前预先处理等,这些都会导致指令乱序执行。

编译器很重要的一项工作就是优化我们的代码以提高性能。这包括在不改变程序正确性的条件下重新排列指令,也就是编译器指令乱序问题。

CPU指令执行的顺序一致性

为了提高流水线的运行效率,CPU会对无依赖的前后指令做适当的乱序和调整,对控制依赖的指令做分支预测,对内存访问等耗时操作提前预先处理等,这些都会导致指令乱序执行。

但是我们编程时一般理解代码在CPU上的执行顺序和代码的逻辑顺序是一致的呀?这有点让人困惑。从单核单线程CPU的角度来看,指令在CPU内部可能是乱序执行的,但是对外表现却是顺序执行的。因为指令集架构(ISA)中的指令寄存器作为CPU的对外接口,CPU只需要把内部真实的物理寄存器按照指令的执行顺序,顺序映射到ISA寄存器上,也就是CPU只要将结果顺序地提交到ISA寄存器,就可以保证顺序一致性(Sequential consistency)

多核CPU上指令乱序执行

显然在单核单线程CPU上指令乱序问题被指令集架构所屏蔽,但是在多核多线程CPU上依然存在指令乱序执行的可能性。比如存在变量x = 0,CPU0上执行写入操作x = 1。接着在CPU1上,执行读取操作依然得到x = 0,这在X86和ARM多核CPU上都是可能出现的。原因是如图所示CPU核和Cache以及内存之间,存在着Store Buffer,当x = 1执行写入操作成功后,修改只存在于Store Buffer中,并未写到cache以及内存上,因此CPU1读取不到最新的x值。除了Store Buffer,而且还可能会有Invalidate Queue,导致CPU1读不到最新的x值。为了能够保证多核之间的修改可见性,我们在写程序的时候需要加上内存屏障,例如X86上的mfence指令。

ARM64 CPU指令乱序

对于ARM64架构的CPU来说,编程就变得危险多了。除了存在数据依赖、控制依赖和地址依赖等不能被乱序执行外,其余指令间都有可能存在乱序执行。ARM64上没有依赖关系的读后读、写后写、读后写和写后读都是可以乱序执行的。ARM64架构下Store Buffer并不是FIFO的,而且还可能存在Invalidate Queue,这让并发编程变得困难重重。总之ARM64是弱内存序模型因为精简指令集把访存指令和运算指令分开了,为了性能允许几乎所有的指令乱序,但前提是不影响程序的正确性。因此ARM64架构的指令乱序问题需要引入不同类型的barrier来保证程序的正确性。

需要特别指出的是ARM64允许指令乱序执行是出于性能的考虑,这是架构特性,不是漏洞。但是指令乱序的影响却给系统可靠性带来了风险,驱动模块、基础软件和应用软件都要做排查和设计优化。

高级语言定义了逻辑关系,逻辑关系与应用程序的业务逻辑有关;编译器将内含逻辑关系的高级语言代码翻译成机器语言或汇编语言,其中就定义了数据依赖、控制依赖和地址依赖等依赖关系;ARMv8架构定义了内存模型以及实现处理这些依赖关系的机器语言指令,从而防止有依赖的指令乱序执行影响程序正确性。

显然CPU指令乱序与硬件内存模型及防止指令乱序的机器语言指令内部实现紧密相关,这些需要深入到处理器微架构深处才能一探究竟,与我们专注于Linux内核的目标不符,这里不再深入探讨它。但是我们需要清楚的一点是,CPU仅能看到机器指令或汇编指令序列中的数据依赖、控制依赖和地址依赖等依赖关系,并不能理解高级语言中定义的逻辑关系,因此CPU指令乱序执行和编译优化指令乱序都可能会破坏高级语言中定义的逻辑关系,这是我们学习指令乱序问题的原因。

编译器指令乱序问题

编译器很重要的一项工作就是优化我们的代码以提高性能。这包括在不改变程序正确性的条件下重新排列指令,也就是编译器指令乱序问题。

因为编译器不知道什么样的代码需要线程安全,所以编译器假设代码都是单线程执行的,也就是编译器对函数的可重入问题是没有感知的,因此编译器进行指令重排优化只能保证是单线程安全。因此当多线程应用程序的逻辑关系在编译器重新排序指令的时候可能影响程序正确性时,除非你显式告诉编译器,我不需要重排指令顺序,否则编译器可能会在优化指令顺序时影响程序的正确性。这一部分我们一起探究编译器编译优化相关的指令乱序问题。

编译器屏障

在阅读Linux内核源代码时,会看到额外插入的汇编指令如下,是告诉编译器不要优化指令顺序。如下代码摘自Linux内核源代码include/linux/compiler-gcc.h。

#define barrier() __asm__ __volatile__("": : :"memory")

如上代码定义的宏barrier()就是常说的编译器屏障(compiler barriers),它的主要用途就是告诉编译器不要优化重排指令顺序。为了说明这个问题我们用C语言代码及对应的ARM64汇编代码简要说明指令乱序造成的问题及编译器屏障的作用。

编译器优化造成指令乱序问题

编译器的主要工作就是将高级语言源代码翻译成机器指令,当然翻译的过程中编译器还会进行编译优化以提高代码的执行效率。编译优化主要就是在不影响程序正确性的情况下对机器指令顺序重排从而统筹调度CPU资源改善程序性能,但是对于多线程应用程序编译器并不能理解程序的并发执行逻辑,很可能会好心干坏事。为了说明编译优化指令乱序造成的问题,我们考虑下面的compiler_reordering.c文件中C语言函数function的代码。

int flag, data;
 
int function(void)
{
    data = data + 1;
    flag = 1;
} 

编译时未开启编译优化 

gcc -S compiler_reordering.c -o compiler_reordering.s
function:
	adrp	x0, :got:data
	ldr	x0, [x0, #:got_lo12:data]
	ldr	w0, [x0] 	// load data to w0
	add	w1, w0, 1 // w1 = w0 + 1
	adrp	x0, :got:data
	ldr	x0, [x0, #:got_lo12:data]
	str	w1, [x0]	// data = data + 1
	adrp	x0, :got:flag
	ldr	x0, [x0, #:got_lo12:flag]
	mov	w1, 1		// w1 = 1
	str	w1, [x0]	// flag = 1
	nop
	ret

编译时开启编译优化  

gcc -O2 -S compiler_reordering.c -o compiler_reordering_O2.s
function:
	adrp	x1, :got:data
	adrp	x3, :got:flag
	mov	w4, 1		// w4 = 1
	ldr	x1, [x1, #:got_lo12:data]
	ldr	x3, [x3, #:got_lo12:flag]
	ldr	w2, [x1]	// load data to w2
	str	w4, [x3]	// flag = 1
	add	w2, w2, w4	// w2 = w2 + 1
	str	w2, [x1]	// data = data + 1
	ret

与上述C语言函数function中的代码比较,这段优化后的ARM64汇编代码的执行顺序是不同的。C代码中是先存储了data的值,后存储了flag的值,而优化后的ARM64汇编代码正好相反,先存储了flag,后保存了data。

这就是编译器指令乱序问题的典型范例。为什么编译器会这么做呢?对于单线程来说,data和 flag的写入顺序,编译器认为没有任何问题的。并且最终的结果data和flag的值也是正确的。

实际上这种编译器指令乱序问题在大部分情况下是没有问题的。但是在某些情况下可能会引入问题。例如我们使用的全局变量flag标记共享数据data是否就绪。另外一个线程检测到flag == 1就认为data已经就绪,而由于编译器指令乱序,实际上data的值可能还没有存入内存。

下面我们加入内存屏障,再来看看编译后产生的汇编文件。 

#define barrier() __asm__ __volatile__("": : :"memory")

int flag, data;

int function(void)
{
    data = data + 1;
    barrier();
    flag = 1;
}
function:
	adrp	x0, :got:data
	ldr	x0, [x0, #:got_lo12:data]
	ldr	w1, [x0]		// load data to w1
	add	w1, w1, 1		// w1 = w1 + 1
	str	w1, [x0]		// data = data + 1
	adrp	x1, :got:flag
	mov	w2, 1			// w2 = 1
	ldr	x1, [x1, #:got_lo12:flag]
	str	w2, [x1]		// flag = 1
	ret

barrier就是编译器提供的内存屏障,作用是告诉编译器内存中的值已经改变,之前对内存的缓存(缓存到寄存器)都需要抛弃,barrier之后的内存操作需要重新从内存加载,而不能使用之前寄存器缓存的值。可以防止编译器优化barrier前后的内存访问顺序。barrier就像是代码中的一道不可逾越的屏障,barrier前的内存读写操作不能跑到barrier后面;同样barrier后面的内存读写操作不能在barrier之前。


以上内容为中科大软件学院《高级软件工程》课后总结,感谢孟宁老师的倾心教授,老师讲的太好啦(^_^)

参考资料:《代码中的软件工程》    孟宁  编著

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

可重入函数与线程安全 的相关文章

  • IIS应用程序池回收+quartz调度

    我正在 IIS 7 5 上运行一个 Web 应用程序 它需要偶尔回收 否则内存使用情况会失控 这是我正在研究的问题 当它回收时 它实际上不会运行 直到另一个请求到来 而quartz不会运行 有没有办法让IIS在回收应用程序池后立即自动启动1
  • 使用 CLion 进行 OpenCV Windows 设置

    我想在 Windows 上为 CLion IDE 设置 OpenCV 我尝试使用 OpenCV 3 1 和 2 4 得到相同的结果 我有 Windows 10 64 位 CLion 使用 cygwin 环境 到目前为止我做了什么 1 从Op
  • 如何在另一个应用程序中挂钩 api 调用

    我正在尝试挂钩另一个应用程序的 ExtTextOut 和 DrawTextExt GDI 方法调用 我知道我需要使用 GetProcAddress 来查找 gdi32 dll 中那些方法的地址 并用我的函数的地址覆盖我想要挂钩的进程中的地址
  • 在现代 C++ 中,临时生命周期延长何时有用?

    在 C 中 您可以将函数的返回值 返回值 而不是引用 绑定到 const 引用 并且代码仍然有效 因为该临时对象的生命周期将延长到作用域末尾 例如 std string get string return abc void f const
  • 从代码中,如何创建对存储在附加属性中的对象的属性的绑定?

    我们有一个继承的附加属性来存储一个对象 在可视化树的更下方 我们希望从代码绑定到该对象的属性 通常我们像这样构建绑定的路径部分 var someBinding new Binding Path new PropertyPath Attach
  • 在 omp 并行 for 循环中使用 unique_ptr 会导致 SEG.FAULT

    采取以下代码 include
  • C# 开源 NMEA 解析器 [已关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我正在寻找 C 开源 NMEA 解析器 嗯 我自己也不熟悉 但是一些快速搜索显示了一个代码项目 htt
  • 使用 LINQ 更新 IEnumerable 对象的简单方法

    假设我有一个这样的业务对象 class Employee public string name public int id public string desgination public int grade List
  • UI 函数在快速事件完成之前触发

    我有一个停靠在 Silverlight 应用程序中的 Web 浏览器框架 有时会在其上弹出全窗口 XAML Silverlight UI 元素 我已经或多或少修复了一个老问题 即 Web 框架的内容似乎与 Silverlight 内容不能很
  • 如何在三个 IEnumerable 上使用 Zip [重复]

    这个问题在这里已经有答案了 可能的重复 使用 Linq 从 3 个集合创建项目 https stackoverflow com questions 5284315 create items from 3 collections using
  • 析构函数中的异步操作

    尝试在类析构函数中运行异步操作失败 这是代码 public class Executor public static void Main var c1 new Class1 c1 DoSomething public class Class
  • Project Euler #8,我不明白我哪里出了问题[关闭]

    Closed 这个问题是无法重现或由拼写错误引起 help closed questions 目前不接受答案 我正在做项目欧拉第八题 https projecteuler net problem 8 其中我得到了这个大得离谱的数字 7316
  • 使用 GCC 生成可读的程序集?

    我想知道如何使用GCC http en wikipedia org wiki GNU Compiler Collection在我的 C 源文件中转储机器代码的助记符版本 这样我就可以看到我的代码被编译成什么 你可以使用 Java 来做到这一
  • 从浏览器访问本地文件?

    您好 我想从浏览器访问系统的本地文件 由于涉及大量安全检查 是否可以通过某种方式实现这一目标 或使用 ActiveX 或 Java Applet 的任何其他工作环境 请帮帮我 要通过浏览器访问本地文件 您可以使用签名的 Java Apple
  • 选择查询不适用于使用Parameters.AddWithValue 的参数

    C 中的以下查询不起作用 但我看不出问题所在 string Getquery select from user tbl where emp id emp id and birthdate birthdate cmdR Parameters
  • 如何停止无限循环?

    我正在编写一个程序 该程序将计算三角形或正方形的面积 然后提示用户是否希望计算另一个 我的代码已经运行到可以计算任一形状的面积的程度 但随后不再继续执行代码的其余部分 例如 如果选择了正方形 则计算面积 然后返回到正方形边长的提示 我假设这
  • CUDA 8 编译错误 -std=gnu++11

    我正在尝试转换一些代码以使用 CUDA 并且我认为我遇到了兼容性问题 我们使用CMake 这些是我使用的 gcc 和 CUDA 版本 gcc version gcc Ubuntu 5 4 0 6ubuntu1 16 04 5 5 4 0 2
  • 如何在 winforms 应用程序的主屏幕显示之前显示欢迎屏幕?

    我想在应用程序启动时加载欢迎屏幕 然后用户单击欢迎屏幕上的按钮 然后关闭欢迎屏幕 最后显示主屏幕 static void Main startup method being called Application EnableVisualSt
  • 为什么以下 C 程序会出现总线错误?

    我认为这是第一个失败的 strtok 调用 好久没写C了 有点不知所措 非常感谢 include
  • 如何将 SQL“LIKE”与 LINQ to Entities 结合使用?

    我有一个文本框 允许用户指定搜索字符串 包括通配符 例如 Joh Johnson mit ack on 在使用 LINQ to Entities 之前 我有一个存储过程 该存储过程将该字符串作为参数并执行以下操作 SELECT FROM T

随机推荐

  • 开发一个具有分页功能的web表格组件

    效果图 代码 index html table thead tr th ID th tr thead table
  • Web经典BS快速开发框架,强大后台+简洁UI一体化开发工具

    本框架旨在为 NET开发人员提供一个Web后台快速开发框架 采用本框架 能够极大的提高项目开发效率 整个框架包括三个版本 net net core java 开发中 以上三个版本中 NET为初始版本 开发时间最长 是目前老客户使用的主要产品
  • Hudi数据湖-基于Flink、Spark湖仓一体、实时入湖保姆级教学

    目录 Hudi源码编译 Hudi扫盲 基于Spark shell集成Hudi 基于Spark Hive集成Hudi手动创建HIVE表 基于SparkSQL集成Hudi自动创建HIVE表 基于FlinkSQL集成Hudi 基于FlinkSQL
  • 八大排序比较(时间复杂度,空间复杂度,稳定性的比较)

    排序算法的稳定性 含义 假定在待排序的记录序列中 存在多个具有相同的关键字的记录 若经过排序 这些记录的相对次序保持不变 即在原序列中 r i r j 且r i 在r j 之前 而在排序后的序列中 r i 仍在r j 之前 则称这种排序算法
  • python 使用前馈神经网络处理IrIs数据集(BP)

    本文章包含以下内容 数据 lris数据集 模型 前馈神经网络 激活函数 Logistic 损失函数 交叉嫡损失 优化器 梯度下降法 评价指标 准确率 输出层使用了Softmax分类 通过使用前馈神经网络实现BP学习算法 进一步理解前馈神经网
  • Java基础 --- 注解 Annotation

    Java基础 注解 Annotation Java注解 Java自带的标准注解 自定义注解 Java注解 Java注解它提供了一种安全的类似注释的机制 用来将任何的信息或元数据 metadata 与程序元素 类 方法 成员变量等 进行关联
  • Pytest-UnitTest

    2023暑期学习 Pytest Pytest pytest是python的一种单元测试框架 与python自带的unittest测试框架类似 但是比unittest框架使用起来更简洁 效率更高 pip install pytest pyte
  • JavaScript的三大组成部分(收藏)

    JavaScript是一种属于网络的脚本语言 已经被广泛用于Web应用开发 常用来为网页添加各式各样的动态功能 为用户提供更流畅美观的浏览效果 通常JavaScript脚本是通过嵌入在HTML中来实现自身的功能的 各位小伙伴在进阶的时候总会
  • 放个手机在单位自动打卡_几步神奇操作让你的钉钉自动打卡

    这款叫做 FreeDing 的 Android 应用 是一个钉钉自动打卡工具 只需要把手机放在办公室 就能实现每天自动定时打卡了 Appinn 这是一个程序员一不爽就造轮子的故事 diy0504 同学所在公司打卡要求越来越严格 忘记打卡不准
  • 实用mysql命令

    1 显示表中所有列的详细信息 show full columns table name 2 查看服务器版本 show version 3 查看当前登录用户 select current user 4 显示表的详细信息 show table
  • 聊聊 Docker 和 Dockerfile

    目录 一 前言 二 了解Dockerfile 三 Dockerfile 指令 四 多阶段构建 五 Dockerfile 高级用法 六 小结 一 前言 对于开发人员来说 会Docker而不知道Dockerfile等于不会Docker 上一篇文
  • This beta version of Typora is expired, please download and install a newer version.

    一 问题 打开typora软件提示如下图 翻译过来就是 此测试版Typora已过期 请下载并安装新版本 二 解决办法 1 win R调用运行窗口 输入regeedit 打开注册表 2 在注册表数据输入 计算机 HKEY CURRENT US
  • javaScript基础面试题 ---call、apply、bind三者的异同 - 改变this的方法,apply和call最初设计的时候为什么要设计这两个,为什么apply参数是数组call不是

    call apply bind三者的异同 call方法 apply方法 bind方法 call apply bind三者的异同 改变this的方法 apply和call最初设计的时候为什么要设计这两个 为什么apply参数是数组call不是
  • 如何使用JLINK在ADS1.2环境下调试硬件?

    注 虽然文章是对LPC2148而写的 但是对三星的44B0芯片同样适用 只需要在选择时将相应的CPU选择的S3C44B0就可以了 JLINK在ADS下调试心得 前两天一个客户用jlink 在ADS来调试LPC2148总报错 这个错误我之前在
  • 对象有多个字段新增日志执行的方法却只有几个?

    我以上代码 AssetCard对象有多个字段 最后执行新增却只有几个字段 我之前开发的项目都是公司自己开发的框架 是解决了字段可以包含下划线的 然后这次客户需要我写个插件 我就自己搭建了一个项目 让后发现字段包含下划线底层sql就会缺少字段
  • 根据地理位置多语言切换(3)-多语言切换

    在手机应用的实现中经常会遇到需要语言切换用于满足用户环境的多样性 可以根据所处地理位置信息进行经纬度及国家 城市 地区的获取 可以根据此内容进行多语言情况的推荐及切换 完成上述的想法需要进行几个功能的开发 需要通过手机进行地理位置信息获取
  • 【无监督】6、SimSiam

    文章目录 一 背景 二 方法 三 效果 论文 Exploring Simple Siamese Representation Learning 出处 FAIR 何恺明大佬 本文作者抛出了两个爆炸 性结论 结论一 基于孪生网络的对比的学习的成
  • 外盘国际期货招商

    100万在各省市能生活多久 按照去年人均消费支出计算 上海100万能花不到22年 西藏能花近63年 2021年我国居民人均预期寿命 78 2岁 2022年我国各省市人均消费支出 365日 西藏43 5 日 62年11个月 2022年我国各省
  • M ySql基础3

    一 MySql存储过程和函数 概念 好处 存储过程和函数的区别 创建存储过程 调用存储过程 查看存储过程 删除存储过程 存储过程语法 二 MySql触发器 创建触发器 查看触发器 删除触发器 触发器的总结 三 MySQL事务 事物的概念 未
  • 可重入函数与线程安全

    指令乱序和线程安全 先来看什么是指令乱序问题以及为什么有指令乱序 程序的代码执行顺序有可能被编译器或CPU根据某种策略打乱指令执行顺序 目的是提升程序的执行性能 让程序的执行尽可能并行 这就是所谓指令乱序问题 理解指令乱序的策略是很重要的