进程内的多个线程共享同一虚拟地址空间,每个线程都是一个独立的执行流,所以,多个线程对应多个执行流,多个执行流会竞争同一资源的情况,资源包括内存数据、打开的文件句柄、套接字等,如果不加以控制和协调,则有可能出现数据不一致,而这种数据不一致可能导致结果错误,甚至程序奔溃,因此,需要努力避免。

并发编程的错误非常诡谲且难以定位,它总是隐藏在某个的角落,大多数时候,程序运转良好,等代码交付上线后,莫名其妙的出错,就像墨菲定律描述的那样:凡是可能出错,就一定会出错。

数据不一致源于什么?

CPU与内存访问性能差距很大,Cache被作为内存的缓存插入到CPU与内存之间,数据会在Cache里缓存内存数据的副本,内存数据与缓存数据不总是一致。比如修改变量(写入数据),如果采取写穿透(Write Through)的方式,则会在更新缓存中对应的Cache Line的同时把数据写入内存,如果数据不在缓存,则直接写入内存,但每次写操作都写入内存,而内存的访问时延通常高达几十个指令周期,这种写的方式性能太低。而采用写回(Write Back)的方式,如数据在Local Cache里,则更新缓存后就直接返回,这样就减少访问内存的频率,也就提升了性能,但这样的话,内存数据和Cache里的数据是不一致的。

线程同步关键字_多线程同步_线程同步的3种方法

现代处理器朝着多CPU多核架构发展,每个核有自己的L1/L2 Cache ,核之间共享L3 Cache,然后再通过总线连接内存,内存被所有CPU/Core所共享,所以,一个内存数据会被多个CPU/Core Cache,不仅内存与Cache中的数据可能不一致,Cache里的多份拷贝也会不一致,Cache一致性协议用于处理这个问题。

CPU如何使用内存数据?

– CPU通常不会直接操作内存

– 这是因为有些指令对操作数有限制,比如X86-64限制mov指令的源和目的操作数不能都是内存地址,所以把一个字节从一个内存地址复制到另一个内存地址,需要两条mov汇编指令,先从源地址move到寄存器,再从寄存器move到目标地址

– 即使mov的一个操作数是内存地址,实际上,CPU处理的时候,也会先将内存地址的数据加载到Cache Line,再作用于Cache Line,而非直接修改内存

– 多CPU多核系统上,如果Core的local Cache没有对应变量的数据,它并不是只有从内存里加载数据到Cache这一条路,而是会通过CPU/Core间消息,从别的CPU/Core的Cache里拿数据的拷贝,这个核间消息不是通过共享的总线传递,而是基于Interconnect的message passing

– 当某个Core更新Local Cache里的数据时,它需要通过CPU/Core间消息把这个写入操作传播到其他Core的Cache,如果其他Core也Cache了这个数据,要让对应Cache Line失效,这个叫写传播(Write Propagation),总线嗅探通过感知到核间消息来实现写传播

– 另外,某个CPU核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串形化(Transaction Serialization),而做到这一点,则需要CPU的缓存更新需要引入‘锁’的概念,多个核心有相同数据的Cache,那么对于数据的更新,只有拿到锁进行,而基于总线嗅探实现的MESI协议就是为了实现事务串行化,如果一个数据在某个Core的Cache Line是独占(Exclusive)状态,则它相当于拿到了自由修改权,如果一个数据被加载到多个Core的Cache,则是Shared的状态,这时候,需要通过向其他核广播请求,Invalidate其他核里的Cache Line才能修改。

为什么需要多线程同步

我们先用2个例子来描述,如果不做线程同步,程序会出现什么问题。

例子1

有一个货物售卖程序,变量int item_num记录某商品的数量,它被初始值为100(代表可售卖数量为100)。售卖函数检查剩余商品数,如果剩余商品数大于等于售卖数量,则扣除商品件数,并返回成功;否则,返回失败。代码如下:

“`c++

bool sell(int num) {

if (item_num >= num) {

item_num -= num;

return true;

return false;

“`

单线程下,sell函数被多次调用,程序运转良好,结果符合预期,但在多线程环境下,会出错。

为了理解上述代码行为,需要先了解一个基本事实:程序变量存放在内存中,对变量做加减,会先将变量加载到通用寄存器,再执行算术运算(更新寄存器中的值),最后把寄存器的新值存入内存位置。

寄存器里会保存内存变量值的副本,对变量的加减乘除等运算直接作用于副本,而非变量内存位置。不过,如果对变量赋值,则指令会接受一个内存位置作为操作数,指令会直接操作内存位置。

#### 多核并行

– 假设线程t1和线程t2,分别在core1和core2同时执行,t1执行sell(50), t2执行sell(100)。

– 2个线程代表2个执行(指令)流,如果2个执行流同时执行到if (item_num >= num)这一行判断语句。

– 内存位置保存的item_num的值会被分别加载到2个核心的寄存器,因为item的值为100,所以加载到寄存器后也都为100,2个线程的条件检查都顺利通过。

– 线程t1,执行减法运算item_num -= num,参数num为50,结果为100-50=50,更新寄存器。

– 线程t2,执行减法运算item_num -= num,参数num为100,结果为100-100=0,更新寄存器。

– 然后,t1和t2线程先后把各自寄存器里的新值store到内存,后一个线程的store操作会覆盖前值。

– 如果t1线程先store,内存中的item_num被修改为50,t2线程再执行store,内存中的item_num被覆盖,item_num值被替换为0。

– 如果t2线程先store,t1线程后store,则item_num的最终值为50。

无论哪种情况,结果都是错误的,我们只有100件商品,却超卖出150件。

单核并发

– 如果程序在单CPU单Core的机器上运行,t1线程和t2线程并发(非并行)交错执行,因为只有一个CPU,所以同一时刻,只有一个线程在执行。

– 假设t1在CPU上执行,它把item_num(100) load到寄存器,判断通过(100 >= 100)。

– 这时候,发生线程调度(比如t1的时间配额耗尽),t2被调度到CPU上执行,然后t2依次完成load、check、compute和store操作。

– 然后t1又被调度到CPU上恢复执行,t1会直接用寄存器中的item_num副本(100),执行计算,item -= 50的结果为50,更新寄存器,所以t1线程执行后,新值50被store入item_num所在内存。

– 我们期望t1或者t2的sell只有一个操作成功,但结果并非如此。

CPU核相当于工人,而程序线程相当于任务,核的数量决定了并行度,多个核代表多个任务可以被同时执行,但多线程竞争导致数据的不一致,在单核环境也有可能出现。

让我们再看一个计数的例子:

例子2

全局变量int count用来计数,我们启动100个线程,每个线程的处理逻辑:在100次循环里累加count;主函数启动线程并等待所有线程执行完成,程序退出前打印count数值,代码如下:

“`c++

#include

#include

int count = 0;

void thread_proc() {

for (int i = 0; i < 100; ++i) {

++count;

int main() {

std::thread threads[100];

for (auto &x : threads) {

x = std::move(std::thread(thread_proc));

for (auto &x : threads) {

x.join();

std::cout

限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: lzxmw777

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注