C++ 0x: 内存模型
2019独角兽企业重金招聘Python工程师标准>>>
自C++11有了多线程,自然 原子类型(atomic)也是少不了的.
提到原子类型必然是与内存模型(std::memory_order)相互关联的.其实半年前就有接触到,半年的时间里对它的理解还是一知半解,而且一直没有时间深入的看看,正好得了肩周炎,就躺着看了看,又查查了在此也做一个总结.
C++标准库提供一下几种memory_order:
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
在正式进入正题之前还有几个概念是我们必须要了解的:
Sequence before: 在同一线程内 evaluation A操作可能先于 evaluation B操作遵循evaluation order.
Carries dependency: 在同一线程内 evaluation A 先于 evaluation B执行,如果evaluation B操作需要evaluation A的值那么我们就说 evaluation B 依赖 evaluation A操作的值.
如果 evaluation B 依赖 evaluation A操作的值需要满足以下条件:
1) evaluation A操作的值作为evaluation B的操作数,除了以下几种情况
a)evaluation B调用了std::kill_dependency
b)evaluation A操作的值作为 &&, ||, :? 和 , 运算符的左操作数.
2) evaluation A操作的值写入到一个变量(variable)M中, evaluation B从M中读取该值.
3) 假设有 evaluation A操作依赖 evaluation X操作的值,而evaluation B操作依赖evaluation A操作的值那么此时evaluation B依赖evaluation X.
(1),在了解内存模型之前我们还需要了解:
1,由于技术的革新,我们在运行我们写好的代码的时候,比如读取变量的值,并不一定是从内存中读取的,由于为了快速读取变量的值, 所以该值的一个备份(在第一次从内存中读取该值的时候)可能会被放在CPU高速缓存/寄存器(当然这也不是说内存中就没有值了,只是因为从CPU高速缓存/寄存器中读取更快!内存中依然有一份值).
CPU高速缓存:http://baike.baidu.com/link?url=uOY6wdHUpoeMQ0NWyo2957fXIdljtBThvVnGNtwX1nDSJZ3TsglHdDQuKsj_oKMReUTqXHO3v5DxOOozI1iTHK
CPU高速缓存好文章一篇(这一篇最好读读前几节):http://blog.csdn.net/robertsong2004/article/details/38340247
2,还有不得不说的是(乱序,见下面补充):我们的代码并不一定按照完全按照我们写的方式来运行(当然了肯定是有办法来限制编译器优化造成的适当的乱序的具体参阅这里:https://my.oschina.net/u/2516597/blog/676927).
(2),关于乱序:
1,编译器出于优化目的,在编译阶段对代码进行适当重排序.
2,程序执行期间,CPU乱序执行指令流.
3, inherent cache 的分层及刷新策略使得有时候某些写读操作的从效果上看,顺序被重排 。(这个我也不懂)
以上乱序现象虽然来源不同,但从源码的角度,对上层应用程序来说,他们的效果其实相同:写出来的代码与最后被执行的代码是不一致的。这个事实可能会让人很惊讶:有这样严重的问题,还怎么写得出正确的代码?这担忧是多余的了,乱序的现象虽然普遍存在,但它们都有很重要的一个共同点:在单线程执行的情况下,乱序执行与不乱序执行,最后都会得出相同的结果 (both end up with the same observable result), 这是乱序被允许出现所需要遵循的首要原则,也是为什么乱序虽然一直存在但却多数程序员大部分时间都感觉不到的根本原因.
(3),接着我们需要明白的另外一个概念是:
上面也说了,单线程下面感觉不到,也没啥影响,那么多线程就问题来了!
1,即使是普通的变量我们对它的读写也不是atomic的,也就是说我们在对它读的时候也能对它写,但是得到的结果可能是之前的旧值,也可能是新写入的值,也可能是个写了一半的值.
2,CPU高速缓存带来的一个问题,如果你看了上面的百度百科以及那篇博客关于CPU高速缓存的介绍,我想您应该明白,我们对于变量的读写都是在高速缓存中完成的!也就是说我们并没有实际修改到内存中的数据!!问题可就大了呀,多线程下一个线程修改了数据!另外一个线程居然没看到新值!(仅仅看到了内存中的旧值,此时新的值还没被同步到内存里面)。我们可以理解为,把高速缓存中的值往内存中同步(牢记这个词后面N多次用到)的不及时.另外在通常情况下 何时同步 与 同步 是两个不同的概念(这里牢记!!!!)
当上面这两个问题交织在一起的时候想想有多可怕,简直是个黑洞! 那么我们就需要通过内存模型(memory_order)来限制这些了!
(4),std::memory_order_relaxed(自由序列)
该模式下仅仅保证了读写的完整性,且还有要求是单个线程内的同一个原子类型的所有操作不能进行乱序.也就是说对一个多线程下的变量进行读写的时候,保证一定会读到完整的值(不会出现读到一个不完整的值,至于该值是新值还是旧值,是没法保证的,同步几次 和 何时同步 CPU说了算),写的时候仅仅保证值一定会被完整的写入(寄存器),至于何时同步,以及对是否在每次写入结束后立即同步到内存中就要看CPU了.
demo1 for std::memory_order_relaxed
#include <atomic>
#include <thread>
#include <cassert>std::atomic<bool> x,y;
std::atomic<int> z;void write_x_then_y()
{x.store(true,std::memory_order_relaxed); // 1y.store(true,std::memory_order_relaxed); // 2
}
void read_y_then_x()
{while(!y.load(std::memory_order_relaxed)); // 3if(x.load(std::memory_order_relaxed)) // 4++z;
}
int main()
{x=false; //5y=false; //6z=0; //7std::thread a(write_x_then_y); //Astd::thread b(read_y_then_x); //Ba.join();b.join();assert(z.load()!=0); // 8return 0;
}
解析demo1:
assert是仍然有可能触发的! 首先我们需要注意到的是无论是read操作(load),还是write(store),我们都指定内存模型为std::memory_order_relaxed,这样一来也就意味着1和2处的操作有可能被乱序执行,5,6和7处也一样可能被乱序执行。不仅仅是乱序这么简单1, 2, 5, 6, 7处的写操作很可能都仅仅写入了高速缓存/寄存器,也可能并没有同步内存中的,那么在3和4处执行的read操作就可能即使y load到了true, 而x仍然load到false.
demo2 for std::memory_order_relaxed
// Thread 1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B// Thread 2:
r2 = x.load(memory_order_relaxed); // C
y.store(42, memory_order_relaxed); // D
x 和 y都默认初始化为0.
demo2解析:
很有可能 r1 = r2 = 42,因为尽管在线程1中A sequece-before B,线程2中 C sequence-before D.但是没有什么保证D一定是在A之后运行, 同样也无法保证B一定在C之前运行.尽管如此D产生的side-effect还是对于A可见的同理B产生的side-effect也是对于C可见的.
(4.5), std::memory_order_release
该memory_order用于atomic store operation且保证所有发生在该operation操作之前的读写操作都不能被乱序(reordered).
(5), std::memory_order_release 和 std::memeory_order_acquire(注意这两个一定是成对出现的).
该模型下不仅仅提供了保证读写的完整性,保证同一个线程内的同一个原子变量的所有操作不能乱序,而且提供了同步(这个同步可厉害了见下文)!
如果有一个atomic变量,在线程A中atomic store(taged std::memory_order_release)一个值到该变量中去,在线程B中atomic load(taged std::memory_order_acquire)atomic 变量中的值,当atomic 变量的load(taged std::memory_order_acquire)操作完成后,所有发生在线程A中atomic store(taged std::memory_order_release)操作之前的其他变量(普通变量以及store taged std::memory_order_relaxed)写入的值(产生的side effect)对于线程B中atomic 变量load(taged std::memory_order_acquire)之后的操作均可见(可以把线程B中atomic 变量load taged std::memory_order_acquire理解为一个同步点但是这个同步点不仅仅同步了该atomic变量也同步了其他变量(其他变量包括普通变量以及taged std::memory_order_relaxed的atomic变量)!
比如:
线程 A 原子性地把值写入 x (release), 然后线程 B 原子性地读取 x 的值(acquire). 这样线程 B 尽可能的读取到 x 的最新值。注意 release - acquire 有个牛逼的副作用:线程 A 中所有发生在 release x 之前的写操作,对在线程 B acquire x 之后的任何读操作都可见!本来 A, B 间读写操作顺序不定。这么一同步,在 x 这个点前后, A, B 线程之间有了个顺序关系,称作: inter-thread happens-before.
demo1 for std::memory_order_release/acquire
#include <atomic>
#include <thread>
#include <assert.h>std::atomic<bool> x,y;
std::atomic<int> z;
void write_x()
{x.store(true,std::memory_order_release);
}
void write_y()
{y.store(true,std::memory_order_release);
}
void read_x_then_y()
{while(!x.load(std::memory_order_acquire)); //1if(y.load(std::memory_order_acquire)) //2++z;
}
void read_y_then_x()
{while(!y.load(std::memory_order_acquire)); //3if(x.load(std::memory_order_acquire)) //4++z;
}
int main()
{x=false;y=false;z=0;std::thread a(write_x); //A线程std::thread b(write_y); //B线程std::thread c(read_x_then_y); //C线程std::thread d(read_y_then_x); //D线程a.join();b.join();c.join();d.join();assert(z.load()!=0); // 5return 0;
}
上面的demo中:
assert仍然有可能触发.
上面的例子中 x 和 y 都是由不同的线程写入的,因此在 线程C和D 中:1和3 的load(acqiire)操作由于是位于while中因此可以多次进行同步(如果一次不成功就再同步一次直到load出来的值为true为止). 但是3和4的load(acquire)操作可能会load到false,因为 x.store和y.store 操作是作为 两个线程(A和B) 来执行的,所以在线程C中 1处的操作只能看到 线程A(线程A必须早于线程C时)中的 store(release) 的值,至于 线程C 中 2处的操作 可能由于CPU的乱序执行 造成运行到了 2处 而 线程B 却还没执行(或者刚刚开始执行,或者执行一半)这样一来看到就还是false; 线程D中同理.
demo2 for std::memory_order_release/acquire
#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>#include "arg.h"std::atomic<bool> a{ false };
std::atomic<bool> b{ false };
std::atomic<int> c{ 0 };void writeA()noexcept
{a.store(true, std::memory_order_release);//1
}void writeB()noexcept
{b.store(true, std::memory_order_release); //2
}void setA()noexcept
{a.store(false, std::memory_order_release);//3
}void readAB()noexcept
{while (a.load(std::memory_order_acquire)); //4if (b.load(std::memory_order_acquire)) { //5++c;}
}int main()
{a = false;b = false;c = 0;std::thread tOne{ writeA };//A线程std::thread tTwo{ writeB };//B线程std::thread thread{ setA };//C线程std::thread tThree{ readAB };//D线程tOne.join();tTwo.join();tThree.join();thread.join();assert(c.load() != 0);return 0;
}
这个demo和上面的demo有稍许不同:
assert还有可能触发的!上面的demo中有两个线程 A和C 对 a 进行 store(release) 操作,但是在 线程D 中只有 4处 的操作对 a 进行了 load(acquire) 操作,如果 线程A和C 是先于 线程D 执行的 那么在 load(acquire) 的时候就能看到线程 A和C store(release) 的值并进行同步至于哪个先同步顺序是不确定的,可能同步一次就成功了,可能要几次(这样一来就相当于线程间有了个简单秩序).
demo3 for std::memory_order_release/acquire
#include <atomic>
#include <thread>
#include <cassert>std::atomic<bool> x,y;
std::atomic<int> z;void write_x_then_y()
{x.store(true,std::memory_order_relaxed); // 1 自旋,等待y被设置为truey.store(true,std::memory_order_release); // 2
}
void read_y_then_x()
{while(!y.load(std::memory_order_acquire)); // 3if(x.load(std::memory_order_relaxed)) // 4++z;
}
int main()
{x=false;y=false;z=0;std::thread a(write_x_then_y); //A线程std::thread b(read_y_then_x); //B线程a.join();b.join();assert(z.load()!=0); // 5return 0;
}
这个demo又与上面两个稍许不同:
assert是不会被触发的! 我们注意到 1和2操作 是在同一个线程中分别对 x和y 进行 store(release)。且通常情况下 1操作 是先于 操作2 的,而且 线程A 是先于 线程B 的(即使 线程A 不是先于 线程B 的由于在 3操作 处有个while直到同步到 y 的值为true为止, 既然 y load(acquire)到了true 那么多半情况下 线程A 也差不多执行完成了),这样一来在 3操作 处 y.load(acquire) 后 线程B 就能看到 所有在 该 load(acquire) 操作之前的 store(release) 操作的值,不管是不是同一个原子类型(也就是说其实也相当于对x也同步了一次)!
(6), std::memory_order_release/cousume(其实是:release/acquire的一种只是副作用小了点)
在进行了解之前我们需要了解:
Denpendency-ordered before(前序依赖):
前序依赖 是针对线程之间的!符合以下2点:
1, 线程A 对 原子变量M 执行release操作, 线程B 对 原子变量M 运行consume操作读取 线程A 对 原子变量M 进行release操作存储的值.这个时候就是 B线程 前序依赖 A线程.
2,如果 Y前序依赖 X, Z前序依赖Y, 那么 Z前序依赖X. 也就是Y携带依赖.
假设有一个atomic变量M,在线程A中对M atomic store(taged std::memory_order_release),在线程B中对M atomic load(taged std::memory_order_consume)里面存储的值.所有发生在线程A 对M atomic store操作之前的变量(普通变量或者taged std::memory_order_relaxed的atomic变量)的写入的值,如果M 在线程A中的atomic store(taged std::memory_order_release)依赖这些变量(普通变量或者taged std::memory_order_relaxed的atomic变量)的值,那么线程B中M的atomic load(taged std::memory_order_consume)之后的操作都能看到M在线程A中atomic store(taged std::memory_order_release)依赖的这些变量的值(其实就是 数据依赖 和 携带依赖).
现在在release/consume模型中: 同步还是一样的在每次对 原子类型(atomic)进行 load taged consume操作的时候进行 同步,这回副作用弱了点:可以理解为只同步该原子变量本身以及该原子变量store(taged release)时候依赖的那些变量的值.
例如:
假设有一个atomic variable M, 在线程A中 M.store(val, release), 在线程B中 M.load(consume), 但是在线程A中对 M.store(val, release) 之前 val 的值需要 依赖其他变量的值 这个时候我们就说 M.store(release)前序依赖val, 由于 val 又 依赖其他变量 因此 val携带依赖,因此M.store也依赖其他变量, 因此在线程B中 M.load(consume) 前序依赖 线程A 中的 M.store(val, release)操作且对 val 以及其他变量也有 数据依赖 和 前序依赖,也就是说 线程A 中的 M.stroe(val, release)携带依赖!
demo for std::memory_order_release/consume
struct X
{
int i;
std::string s;
};std::atomic<X*> p;
std::atomic<int> a;void create_x()
{X* x=new X;x->i=42; //xx->s="hello";//ya.store(99,std::memory_order_relaxed); // 1p.store(x,std::memory_order_release); // 2
}void use_x()
{X* x;while(!(x=p.load(std::memory_order_consume))) // 3std::this_thread::sleep(std::chrono::microseconds(1));assert(x->i==42); // 4assert(x->s=="hello"); // 5assert(a.load(std::memory_order_relaxed)==99); // 6
}int main()
{std::thread t1(create_x); //A线程std::thread t2(use_x); //B线程t1.join();t2.join();return 0;
}
上面的demo中:
4和5 是不会触发的,但是 6有可能触发!
尽管 1操作 是位于 2操作 之前的 由于 2操作 存储时为relaxed仅仅保证读写的完整性同步以及何时同步,并不由代码控制。 但是 1和2 的存储操作用的是release的内存模型也就是 赖A(线程)写,而在 线程B 由于需要读取p的数据也就是 赖B(线程)读, 那么 线程B 同步(load(consume))后就能看到 线程A 中 p写入的值.
总结就是: 线程B 前序依赖 线程A, 操作4和5数据依赖x和y.
当然还是有办法来打破依赖关系的:
std::kill_dependency();
它的实现很简单很简单:
template<class _Ty>
_Ty kill_dependency(_Ty _Arg) noexcept
{ // magic template that kills dependency ordering when called
return (_Arg);
}
就只是简单的拷贝,告诉程序读取值的时候从寄存器/高速缓存读取.
如果提供了atomic而没有提供fence(把fencen想象成一个指定的同步点,其实也就是)就不算一个完整的atomic接下来我们了解一下c++标准库提供的fence:
Fence-atomic synchronization:
假设有原子变量: std::atomic<int> val;
一个 std::atomic_thread_fence(release) 在线程A 中通过 线程B 中的 val.load(acquire)进行同步 需要满足以下:
①, 有一个val.store()操作(注意可以以任意memory_order指定该操作).
②, 线程B中的val.load(acquire)操作读取①中store的值(或者这个值将被写入在①的操作之后, 但此时1的store操作为release的!)
③, std::atomic_thread_fence(release)位于①操作之前.
当执行到 线程B val.load(acquire)操作的时候,会同步发生在 std::atomic_thread_fence(release)之前所有非原子类型和relaxed模式下的原子写操作, 但是位于std::atomic_thread_fence(release)和val.store()之间非原子类型和relaxed模式下的原子写操作并不会被同步。
demo for Fence-atomic synchronization:
#include <iostream>
#include <thread>
#include <atomic>
#include <cassert>std::atomic<bool> bAtom{ false };
bool bValue{ false };
int iValue{ 0 };void writeValue()
{bValue = true; //1std::atomic_thread_fence(std::memory_order_release); //2iValue = 1; //3bAtom.store(true, std::memory_order_release);//4
}void readValue()
{while (!bAtom.load(std::memory_order_acquire)); //5assert(bValue); //不可能触发! 6assert(iValue == 1); //可能触发! 7
}int main()
{std::thread tOne{ writeValue }; //线程Astd::thread tTwo{ readValue }; //线程BtOne.join();tTwo.join();return 0;
}
Atomic-fence synchronization:
假设有原子变量: std::atomic<int> val;
一个 线程A 中的 val.store(release) 操作被同步通过 线程B 中的 std::atomic_thread_fence(acquire) 需要满足以下:
①, 有一个 val.load() 的读操作(可以使任意memory_order).
②, 通过①读取 线程A 中 val.store(release) 存储的值
③, 其中②操作要先于 线程B 中的 std::atomic_thread_fence(acquire)
总结只要是发生在 线程A val.store(release) 操作之前 非原子操作和relaxed的原子操作都会在 线程B中的std::atomic_thread_fence(acquire)处被同步.
#include <iostream>
#include <thread>
#include <atomic>
#include <cassert>std::atomic<bool> bAtom{ false };
std::atomic<bool> bOne{ false };
std::atomic<bool> bTwo{ false };
bool bValue{ false };
int iValue{ 0 };void writeValue() //thread A
{bValue = true;bOne.store(true, std::memory_order_relaxed);bAtom.store(true, std::memory_order_release);bTwo.store(true, std::memory_order_relaxed);iValue = 1;
}void readValue() //thread B
{while (!bAtom.load(std::memory_order_consume)); //当然这里也可以是: acquire.std::atomic_thread_fence(std::memory_order_acquire);assert(bValue); //不可能触发! assert(iValue == 1); //可能触发! assert(bOne.load(std::memory_order_relaxed)); //可能触发!assert(bTwo.load(std::memory_order_relaxed)); //可能触发!
}int main()
{std::thread tOne{ writeValue }; //线程Astd::thread tTwo{ readValue }; //线程BtOne.join();tTwo.join();return 0;
}
Fence-fence synchronization:
在 线程A 中有 std::atomic_thread_fence(release) 通过 线程B 中的 std::atomic_thread_fence(acquire) 同步 需要满足以下:
①, 有一个原子变量: std::atomic<int> val;
②, 在 线程A 中调用 val.store()(可以是任何memory_order).
③, 线程A 中的 std::atomic_thread_fence(release)一定是在 val.store()之前的.
④, 有一个 val.load()(可以是任何memory_order)在线程B中.
⑤, 通过 B线程中的val.load() 读取 A线程中的val.store()写入的值(或者读取一个在 ② 操作之前一个原子类型通过release写入的值).
⑥, 其中⑤操作一定要发生在 线程B的std::atomic_thread_fence(acquire)操作之前.
总结所有比线程B中std::atomic_thread_fence先发生,非原子读写以及原子relaxed的读写都会被同步.
demo for Fence-fence-synchronization
include <iostream>
#include <thread>
#include <atomic>
#include <cassert>
#include <initializer_list>std::atomic<bool> bAtom{ false };
std::atomic<bool> bOne{ false };
std::atomic<bool> bTwo{ false };
bool bValue{ false };
int iValue{ 0 };void writeValue() //thread A
{bValue = true;bOne.store(true, std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_release);bAtom.store(true, std::memory_order_release);bTwo.store(true, std::memory_order_relaxed);iValue = 1;
}void readValue() //thread B
{while (!bAtom.load(std::memory_order_consume)); //当然这里也可以是: acquire.std::atomic_thread_fence(std::memory_order_acquire);assert(bValue); //不可能触发! assert(bOne.load(std::memory_order_relaxed)); //不可能触发!assert(iValue == 1); //可能触发! assert(bTwo.load(std::memory_order_relaxed)); //可能触发!
}int main()
{std::thread tOne{ writeValue }; //线程Astd::thread tTwo{ readValue }; //线程BtOne.join();tTwo.join();return 0;
}
转载于:https://my.oschina.net/SHIHUAMarryMe/blog/805489
C++ 0x: 内存模型相关推荐
- C ++ 11引入了标准化的内存模型。这是什么意思?它将如何影响C ++编程?
C ++ 11引入了标准化的内存模型,但究竟是什么意思呢? 它将如何影响C ++编程? 这篇文章 (引用Herb Sutter的Gavin Clarke )说, 内存模型意味着C ++代码现在有一个标 ...
- Linux的Application 内存模型---
Linux的内存模型,一般为: 现在的每个进程使用了全部4G线性空间.在加载程序时内核把程序加载到线性地址0x08048000开始的位置.这个位置当然>128MB.2G开始是共享库,3G开始是内 ...
- C++ 原子操作和内存模型
最近有一个困扰我的问题:如何使C++的原子变量高效而且可移植? 我知道Java volatile是怎么工作的--它强制实行顺序一致性(sequential consistency),但是这个方法并不总 ...
- java中实现具有传递性吗_Java中volatile关键字详解,jvm内存模型,原子性、可见性、有序性...
一.Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作 ...
- 从底层吃透java内存模型(JMM)、volatile、CAS
前言 随着计算机的飞速发展,cpu从单核到四核,八核.在2020年中国网民数预计将达到11亿人.这些数据都意味着,作为一名java程序员,必须要掌握多线程开发,谈及多线程,绕不开的是对JMM(Java ...
- 循序渐进:带你理解什么是Java内存模型
近期笔者在阅读<深入理解Java虚拟机:JVM高级特性与最佳实现(第3版)>,书中提到关于Java内存模型的知识点,但是看完之后还是感觉有些模糊,便查阅一些其他相关资料.本文是笔者经过对知 ...
- 深入理解C语言-二级指针三种内存模型
二级指针相对于一级指针,显得更难,难在于指针和数组的混合,定义不同类型的二级指针,在使用的时候有着很大的区别 第一种内存模型char *arr[] 若有如下定义 char *arr[] = {&quo ...
- java并发编程实战:第十六章----Java内存模型
一.什么是内存模型,为什么要使用它 如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果 编译器把变量保存在本地寄存器而不是内存中 编译器中生成的指令顺序,可以与源代码中的顺 ...
- JSR 133 Java内存模型以及并发编程的最权威论文汇总
Java内存模型 先看官方文档: https://docs.oracle.com/javase/specs/ JSR 133:Java TM内存模型和线程规范修订版:https://www.jcp.o ...
最新文章
- C#进行Visio二次开发之判断图纸是否有设备
- 补充前几天测试用到的Linux命令
- php中显示不出图像,php – 无法显示图像,因为它包含错误
- 实战:Redis 性能测试
- linux野指针追踪,【华清远见】野指针和空指针的两个小点
- 机器学习:算法视角pdf_何时使用不同的机器学习算法:简单指南
- (三)Appium-desktop 打包
- 大数据之-Hadoop完全分布式_RM启动注意事项---大数据之hadoop工作笔记0041
- 用python计算长方体的体积用什么函数_python处理DICOM并计算三维模型体积
- 开源审计的最佳时机是什么时候?
- sql里的正则表达式
- 医学统计学-为什么是个医学生就都要学R语言?
- Pr:图形与基本图形面板
- java 爬取网页的数据_java爬取网页数据
- springmvc(表现层/Web层框架)
- 指令,机器指令,指令周期,机器周期的辨析
- 华为手机adb connect连接失败解决方案(转)
- 图解正则表达式,这一篇就够了
- android打败苹果,苹果iOS打败安卓的另一面:配件多于Android
- 拓嘉辰丰电商:如何投诉拼多多商家一直不发货
热门文章
- 初学者没有搞明白的GOROOT,GOPATH,GOBIN,project目录
- 北师大版图形的旋转二教案_新北师大版八年级下册数学 《图形的旋转(2)》教案...
- 手工编译Linux内核rpm包
- G1垃圾收集器之对象分配过程
- 【分享创造】react-typewriter-hook: 用react hooks来实现打字机的效果
- 洛谷试炼场-简单数学问题-二分查找
- Confluence 6 € 欧元字符集不能正常显示
- swoole_event_add实现异步
- Web开发小结 - 2
- spark2.0配合hive0.13.1使用问题处理