CPU指令集分为两种:
CISC:复杂指令集,指令多,功能多,一条指令可以完成很复杂的逻辑或者算数运算。
RISC:简单指令集,指令少,功能也少,但是基本上都是常用的指令,对于复杂的问题,需要通过N多个条指令才能完成,执行效率没有CISC高。
x86架构基于CISC,arm架构基于RISC。
在单处理器系统中,中断只会发生在指令与指令之间,能够在单条指令中完成的操作都可以认为是原子操作。X86使用CISC指令集,允许在一条指令里进行两次内存操作,因此,对于i++,i–这类操作在单处理器中可视为原子操作(必须显示使用addl r,%1命令)。
在多处理器系统中,多个处理器独立运行,在单条指令中能完成的操作也可能受到干扰,这种情况下,CPU提供了在指令执行期间对总线加锁的手段,使得同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
实现时使用了C内嵌汇编语言。
内嵌汇编语法:
__asm__:内嵌汇编关键字,告知编译器下述语句为汇编代码
__volatile__:告知编译器不要优化(比如重组优化)下述汇编语句
变量列表中常见符号:
“+”:操作数可读可写
“=”: 操作数只写
“&”:常用于输出操作,表示输出操作不能使用输入操作使用过的寄存器,只能+&或=&方式使用
“r”: 操作数是任何可用的通用寄存器
“m”: 操作数是内存变量
“p”: 操作数是一个合法的内存地址
“l”: 操作数是立即数
“Q”: A memory address which uses a single base register with no offset
“V”: 操作数是内存变量,但其寻址方式非偏移量类型
static __inline__ void atomic_ad(int i, atomic_t *v)
{
__asm__ __volatile__(
LOCK “addl %1,%0”
:"+m" (v->counter)
:"ir" (i),"m"(v->counter));
}
0% —–i
1% —–v->counter
ARM使用RISC指令集,在一次指令执行期间只能有一次内存操作,因此在单处理器系统中,i++, i–等也不能视为原子操作,ARM使用关中断来实现原子操作。
在ARM多核处理器中,使用专门的指令ldrex和strex实现原子操作。
static inline int atomic_add_return(int i, atomic_t *v)
{
unsigned long tmp;
int result;
__asm__ __volatile__("@ atomic_add_return\n"
"1: ldrex %0, [%3]\n"
" add %0,%0,%4\n"
" strex %1,%0,[%3]\n"
" teq %1, #0\n"
" bne 1b"
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
: "r" (&v->counter), "Ir"(i)
: "cc");
return result;
}
%0 —— result
%1 —— tmp
%3 —— v->counter的地址
%4 —– i
(1)ldrex %0,[%3]
独占式地加载(Load-Exclusive)v->counter的地址,把它的值放到result中,并更新exclusive monitor(s)
用C描述就是:
result = v->counter
(2)add %0,%0,%4
result = result + i
(3) strex %1, %0, [%3]
独占式地保存(Store-Exclusive)数据至v->counter的地址,数据来自result, 操作结果(成功/失败)保存在tmp中
“cc”是一个特殊的参数,用来标明汇报代码会修改标志寄存器(flags register),在某些机器平台上,GCC通过一个特殊的硬件寄存器表征条件类型的代码,“cc”就是这个特殊寄存器的名字,某些机器平台没有上述功能,“cc”会被忽略,不起作用。
sysbench是一个开源的、模块化的、跨平台的多线程性能测试工具,可以用来进行CPU、内存、磁盘I/O、线程、数据库的性能测试。目前支持的数据库有MySQL、Oracle和PostgreSQL。当前功能允许测试的系统参数有:
file I/O performance (文件I / O性能)
scheduler performance (调度性能)
memory allocation and transfer speed (内存分配和传输速度)
POSIX threads implementation performance (POSIX线程执行绩效)
database server performance (OLTP benchmark) (数据库服务器性能)
下载sysbench的源代码:
https://github.com/akopytov/sysbench/archive/1.0.17.zip
将sysbench-1.0.17.zip上传到安装目录。
使用如下命令解压:
unzip sysbench-1.0.17.zip
进入sysbench-1.0.17目录中
cd sysbench-1.0.17/
然后执行
./autogen.sh
然后执行如下命令
./configure –with-mysql-includes=/home3/mysql/include –with-mysql-libs=/home3/mysql/lib
Sysbench编译时需要通过 –-with-mysql-includes 和 –with-mysql-libs 选项对相关路径进行指定。
然后就可以执行
make 和make install
fileio
cpu
memory
threads
mutex
查看CPU相关信息:cat /proc/cpuinfo
./sysbench –test=cpu –cpu-max-prime=2000 run
./sysbench –test=threads –num-threads=500 –thread-yields=100 –thread-locks=4 run
seqwr顺序写入
seqrewr 顺序重写
seqrd 顺序读取
rndrd 随机读取
rndwr 随机写入
rndrw 混合随机读、写
./sysbench –test=fileio –num-threads=16 –file-total-size=2G –file-test-mode=rndrw prepare
./sysbench –test=fileio –num-threads=16 –file-total-size=2G –file-test-mode=rndrw run
./sysbench –test=fileio –num-threads=16 –file-total-size=2G –file-test-mode=rndrw cleanup
./sysbench –test=memory –memory-block-size=8k –memory-total-size=1G run
查找帮助:
./sysbench –test=mutex help
./sysbench –test=mutex –num-threads=100 –mutex-num=1000 –mutex-locks=100000 –mutex-loops=10000 run
sysbench 0.5 oltp测试结合了lua脚本
1) Prepare 操作。在这个步骤中,会对将要进行的测试进行数据准备,包括创建表和向表中插入数据。
2) Run 操作。主要测试步骤,在这个步骤中可以指定多种选项来,以对不同的测试情况进行模拟。
3) Cleanup 操作。将测试数据删除,主要是删除表。
./sysbench –test=/home/lzy/code/src/tools/sysbench0.5/share/sysbench/oltp.lua –mysql-host=127.0.0.1 –mysql-port=33071 –mysql-user=root –mysql-password= –mysql-db=test –mysql-table-engine=innodb –oltp-table-size=25000 –oltp-tables-count=50 –db-driver=mysql prepare
./sysbench –test=/home/lzy/code/src/tools/sysbench0.5/share/sysbench/oltp.lua –mysql-host=127.0.0.1 –mysql-port=33071 –mysql-user=root –mysql-password= –mysql-db=test –mysql-table-engine=innodb –oltp-table-size=25000 –oltp-tables-count=50 –db-driver=mysql run
/sysbench –test=/home/lzy/code/src/tools/sysbench0.5/share/sysbench/oltp.lua –mysql-host=127.0.0.1 –mysql-port=33071 –mysql-user=root –mysql-password= –mysql-db=test –mysql-table-engine=innodb –oltp-table-size=25000 –oltp-tables-count=50 –db-driver=mysql cleanup
–test=tests/db/oltp.lua 表示调用 tests/db/oltp.lua 脚本进行 oltp 模式测试
–mysql-table-engine=innodb 表示选择测试表的存储引擎 –oltp_tables_count=10 表示会生成 10 个测试表 –oltp-table-size=100000 表示每个测试表产生的记录行数为 100000 –rand-init=on 表示每个测试表都是用随机数据来填充的 –oltp-read-only=off:表示不止产生只读SQL,也就是使用oltp.lua时会采用读写混合模式。默认 off,如果设置为on,则不会产生update,delete,insert的sql –num-threads=8 表示发起 8个并发连接
–oltp-test-mode=nontrx:执行模式,这里是非事务式的。可选值有simple,complex,nontrx。默认是complex
| –oltp-skip-trx=[on | off]:省略begin/commit语句。默认是off |
–report-interval=10 表示每10秒输出一次测试进度报告 –rand-type=uniform 表示随机类型为固定模式,其他几个可选随机模式:uniform(均匀分布),gaussian(高斯分布),special(空间分布),pareto(帕累托),默认为special –max-time=120 表示最大执行时长为 120秒 –max-requests=0 表示总请求数为 0,因为上面已经定义了总执行时长,所以总请求数可以设定为 0;也可以只设定总请求数,不设定最大执行时长 –percentile=99 表示设定采样比例,默认是 95%,即丢弃1%的长请求,在剩余的99%里取最大值
OLTP test statistics:
Queries performed:
read: 140000 //读总数
write: 40000 //写总数
other: 20000 //其他操作总数(SELECT、INSERT、UPDATE、DELETE之外的操作,例如COMMIT等)
total: 200000 //全部总数
transactions: 10000 (30.04 per sec.) // TPS 总事务数(每秒事务数)
read/write requests: 180000 (540.77 per sec.) //QPS 读写总数(每秒读写次数)
other operations: 20000 (60.09 per sec.)//其他操作总数(每秒其他操作次数)
ignored errors: 0 (0.00 per sec.) //忽略的错误数
reconnects: 0(0.00 per sec.)
General statistics:
total time: 332.8696s //总耗时。可以通过max-time参数指定
total number of events: 10000 //总事件数,一般与transactions相同
total time taken by event execution: 332.8291s //所有事务耗时相加(不考虑并行因素),如果不并发执行需要花的时长
response time:(单个请求的时间)
min: 15.24ms //最小耗时
avg: 33.28ms //平均耗时
max: 60018.48ms //最大耗时
approx. 95 percentile: 40.64ms //超过95%平均耗时
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 332.8291/0.00
· TPS:Transaction Per Second,数据库每秒执行的事务数,每个事务中包含18条SQL语句。
· QPS:Query Per Second,数据库每秒执行的SQL数,包含insert、select、update、delete等。
1) common.lua 脚本。这个公用的脚本,所有其他的脚本(除了select_random_points.lua和select_random_ranges.lua )都加载了这个脚本。在这个脚本中主要实现了 prepare 命令和 cleanup 命令的功能。而其他脚本就对应了具体 run 令时要做的工作。所以在 prepare 和 run 命令时,可以选用任意一个脚本运行,但是需要指定除表的个数,即 oltp-tables-count 选项。创建的表的结构(MySQL数据库)
2) select.lua 脚本。这个脚本是在表中进行单点查询。(随机用的是uniform策略)
3) insert.lua 脚本。这个脚本是在表中进行 insert 操作。(随机用的是uniform策略)
4) delete.lua 脚本。这个脚本是在表中进行 delete 操作。(随机用的是uniform策略)
5) parallel_prepare.lua 脚本。这个脚本是多线程创建表,根据线程数选项,开多个线程进行数据库表的创建。
6) oltp_simple 脚本。同 select.lua。
7) oltp.lua 脚本。如果数据库支持事务,就使用 BEGIN/COMMIT语句,如果不支持,就使用 LOCK TABLES/UNLOCK TABLES 语句,可以使用 –oltp-skip-trx 选项,跳过。在这个模式中,操作不会对原本的数据库产生影响,因为插入一个数据,之后就会立即删除这个数据,所以数据库可以重复使用。可以指定在一次事务中,指定单点查询、简单范围查询(between)、聚集函数 SUM()、Order by、distinct c、索引更新或非索引更新语句的数目。
8) update_index.lua 脚本。这个脚本是对表中的索引字段进行更新。如果基准测试的时候,你只想比较两个项目的update(或insert)效率,那可以不使用oltp脚本,而直接改用update_index.lua
9) update_non_index.lua 脚本。这个脚本对表中的非索引字段进行更新。
指定参数的函数实现,通过指定的参数名访问
不指定参数的函数实现,函数调用的参数进行压栈处理(从右到左进行压栈)
可变参数函数:参数个数可变、参数类型不定的函数
“…” 表示0个或多个类型未知的参数
最常见的例子:
int printf(const char * format, ...)
调用:
int a=5;
char b='b';
printf("%d and %c",a,b);
参数压栈顺序:b,a,format
函数调用内存结构:
对不定参数部分用”…“表示
可变参数至少包含一个参数,用来寻址,实现对所有参数的访问
已知的指定参数必须声明在函数最左端
错误的声明:
void func(...)
或者
void func(..., int a);
求和。第一个参数指定要计算的值的个数
格式化字符串。第一个参数指定占位符
#include <iostream>
#include <cstdarg>
#include <cstdio>
#include <vector>
using namespace std;
int sum(int num,...) //利用变长函数进行求和运算
{
int sumval=0;
va_list args; //定义一个可变参数列表
va_start(args,num); //初始化args指向强制参数arg的下一个参数
while(num--)
{
sumval+=va_arg(args,int); //获取参数的值
}
va_end(args); //释放args
return sumval;
}
string format(const char* format, ...) //格式化字符串
{
string var_str;
va_list ap;
va_start(ap, format);
int len = _vscprintf(format, ap);
if (len > 0)
{
vector<char> buf(len + 1);
vsprintf(&buf.front(), format, ap);
var_str.assign(buf.begin(), buf.end() - 1);
}
va_end(ap);
return var_str;
}
int main()
{
cout<<sum(5,10,23,78,65,9)<<endl;
cout<<sum(8,1,2,3,4,5,6,7,8)<<endl;
cout<<format("%s#%s#%s","this","is","me");
//cout<<sum(5,10.23,23.78,78.59,65.12,9.08)<<endl;
return 0;
}
参数类型不匹配,程序会出错,可能导致程序崩溃。
“…” 表示0个或多个类型未知的参数,于是可以帮助我们完成递归
#include <iostream>
#include <bitset>
using namespace std;
void print()
{
}
template <typename T,typename... Types> //...用于模板参数
void print(const T& firstArg, const Types&... args)//...用于函数参数类型
{
cout<<firstArg<<endl;
print(args...);//...用于函数参数
}
int main()
{
print(7.5, "hello", bitset<16>(377),42);
return 0;
}
运行结果:

template<typename... Types>
void print(const Types&... args)
{
}
案例1:
class CustomerHash{
public:
std::size_t operator()(const Customer& c) const{
return hash_val(c.fname,c.lname,c.no);
}
};
template<typename...Types>
inline size_t hash_val(const Types&... args) //函数1
{
size_t seed=0;
hash_val(seed,args...);
return seed;
}
template<typename T, typename... Types>
inline void hash_val(size_t& seed, const T& val, const Types&... args) //函数2
{
hash_combine(seed,val);
hash_val(seed,args...);
}
template <typename T>
inline void hash_val(size_t& seed, const T& val) //函数3
{
hash_combine(seed,val);
}
template <typename T>
inline void hash_combine(size_t& seed, const T&val) //函数4
{
seed^=std::hash<T>()(val)+0x9e3779b9+(seed<<6)+(seed>>2);
}
案例2:
template<typename... Values> class tuple;
template<>class tuple<>{};
template<typename Head,typename... Tail>
class tuple<Head,Tail...>:private tuple<Tail...>
{
typedef tuple<Tail...> inherited;
public:
tuple(){}
tuple(Head v,Tail... vtail):m_head(v),inherited(vtail...){}
typename Head::type head(){return m_head;}
inherited& tail(){return *this;}
protected:
Head m_head;
};
tuple<int,float,string> t(41,6.3,"nico");
t.head() //41
t.tail() //6.3, nico
t.tail().head() //6.3
&(t.tail) //nico
…就是一个所谓的包(pack)
用于template parameters,就是template parameters pack (模板参数包)
用于function parameter types,就是function parameter types pack(函数参数类型包)
用于function parameters,就是function parameters pack(函数参数包)
什么是RPC
Remote Procedure Call Protocol,远程过程调用
入参、执行运算逻辑、出参。三个动作都发生在同一个进程空间里是本地函数调用。
两个进程约定一个协议格式,入参、执行运算逻辑、出参。就是远程调用。
RPC框架的职责是
· client端:序列化、反序列化、连接池管理、负载均衡、故障转移、队列管理,超时管理、异步管理等等
· server端:服务端组件、服务端收发包队列、io线程、工作线程、序列化反序列化等
序列化(Serialization),就是将“对象”形态的数据转化为“连续空间二进制字节流”形态数据的过程。这个过程的逆过程叫做反序列化。
怎么进行序列化?
使用成熟协议xml/json
自定义二进制协议来序列化对象
rpc和http的区别
HTTP 指的是通信协议。 RPC 则是远程调用,其对应的是本地调用。RPC 的通信可以用 HTTP 协议,也可以自定义协议,是不做约束的。 RPC主要用于公司内部服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的异构环境,浏览器调用,APP接口调用,第三方接口调用等等。
https://blog.csdn.net/xuduorui/article/details/78278808
https://developer.aliyun.com/article/713311
离不开的微服务架构,脱不开的RPC细节
https://bbs.huaweicloud.com/blogs/337073
千字带你了解什么是 RPC 协议
gRPC
互斥锁 是阻塞锁,当某线程无法获取互斥锁时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥锁后,操作系统会激活那个被挂起的线程,让其投入运行。
自旋锁 是一种非阻塞锁,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。
std::mutex临界区较小,可考虑使用更轻量级的锁,例如spin lock; 使用tbb::spin_mutex代替std::mutex
std::mutex用于保护std::unordered_map,可考虑使用lock free map替代原本的std::unordered_map。
spinlock/mutex
轻量级互斥锁,用户不可见,适用于多线程间简单共享数据结构的修改保护,只有exclusive(独占)模式。一般基于CAS类的原子操作实现,当ptr的值为0时swap为1,加锁成功,如果ptr的值为1,则swap失败,加锁不成功,继续等待重试。
两种锁适用于不同场景:
1.如果是多核处理器,如果预计线程等待锁的时间很短,使用自旋锁是划算的。
2.如果是多核处理器,如果预计线程等待锁的时间较长,建议使用互斥锁。
3.如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等待运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。
4.如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥锁的开销较大。
共享锁(读锁)
共享锁是指该锁可被多个线程所持有。
悲观锁
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁(使用队列进行排序,FIFO)
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。(概率上会造成优先级反转或者饥饿现象)
阻塞锁
阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间)时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。阻塞锁的优势在于,阻塞的线程不会占用cpu时间,不会导致cpu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。
在竞争激烈的情况下,阻塞锁的性能要明显高于自旋锁。
理想的情况:在线程竞争不激烈下使用自旋锁,竞争激烈下使用阻塞锁。
无锁队列
https://www.cnblogs.com/catch/p/5129586.html
CAS操作
type __sync_val_compare_and_swap(type *ptr, type oldval type newval, …)
比较ptr与oldval的值,如果两者相等,则将newval更新到ptr并返回操作之前*ptr的值。
http://wfeii.com/2021/08/07/atomic.html
FADD操作
type __sync_fetch_and_add (type *ptr, type value);
函数提供原子加并返回原来ptr的值。
Latch/RWLock
轻量级读写锁,用户不可见,其加锁的范围相对于spinlock来说范围更进一步扩大,适用于临界区较大且具有复杂的逻辑处理流程,更适合读多写少的场景)。加锁对象通常类似Btree index等结构体。锁模式分为shared(共享)和exclusive(独占)两种模式。
Innodb transaction lock
https://zhuanlan.zhihu.com/p/493415374
MySQL MDL lock
https://zhuanlan.zhihu.com/p/130318750
1.无锁编程:在多线程竞争激烈的情况下,使用无锁算法的整体吞吐量会优于加锁的算法,因为它避免了调度延时和频繁的上下文切换。如:单生产者单消费者的无锁队列。
2.大锁变小锁:并发任务高的场景下,如果系统中存在唯一的全局变量,那么每个CPU core都会申请这个全局变量对应的锁,导致这个锁的争抢严重。可以基于业务逻辑,为每个CPU core或者线程分配对应的资源。
3.使用gcc自带的原子操作:推荐使用GCC(7.3.0)实现的atomic系列代码,跨平台移植性好,性能也非常好。
4.使用自带内存屏障的指令:推荐使用ldaxr/stlxr指令实现锁或原子操作,替换ldxr/stxr +内存屏障(dmb)的实现。
5.调整线程数达到最佳并发效果:多线程可以提升系统吞吐量,但是却会增加锁的争抢,更会增加线程切换带来的CPU损耗。
6.避免cache line伪共享:通过数据结构优化或字节对齐,将频繁读和频繁写的数据放入不同Cache line,减少“锁”数据结构的伪共享。
spinlock是个while循环,会导致cpu使用率变高,一般仅用在短暂访问临界区场景。