api windows 线程加锁_线程同步及线程锁

1 资源竞争与线程同步

在竞争态条件下,多个线程对同一竞态资源的抢夺会引发线程安全问题。竞态资源是对多个线程可见的共享资源,主要包括全局(非const)变量、静态(局部)变量、堆变量、资源文件等。

线程之间的竞争,可能带来一些列问题:

线程在操作某个共享资源的过程中被其他线程所打断,时间片耗尽而被迫切换到其他线程

共享资源被其他线程修改后的不到告知,造成线程间数据不一致

由于编译器优化等原因,若干操作指令的执行顺序被打乱,造成结果的不可预期

1.1 原子操作

原子操作,即不可分割开的操作;该操作一定是在同一个cpu时间片中完成,这样即使线程被切换,多个线程也不会看到同一块内存中不完整的数据。

原子表示不可分割的最小单元,具体来说是指在所处尺度空间或者层(layer)中不能观测到更为具体的内部实现与结构。对于计算机程序执行的最小单位是单条指令。我们可以通过参考各种cpu的指令操作手册,用其汇编指令编写原子操作。而这种方式太过于低效。

某些简单的表达式可以算作现代编程语言的最小执行单元

某些简单的表达式,其实编译之后的得到的汇编指令,不止一条,所以他们并不是真正意义原子的。以加法指令操作实现 x += n为例 ,gcc编译出来的汇编形式上如下:

...

movl 0xc(%ebp), %eax

addl $n, %eax

movl %eax, 0xc(%ebp)

...

复制代码

而将它放在所线程环境之中,显然也是不安全的:

dispatch_group_t group = dispatch_group_create();

__block int i = 1;

for (int k = 0; k < 300; k++) {

dispatch_group_enter(group);

dispatch_async(dispatch_get_global_queue(0, 0), ^{

++i;

dispatch_group_leave(group);

});

dispatch_group_enter(group);

dispatch_async(dispatch_get_global_queue(0, 0), ^{

--i;

dispatch_group_leave(group);

});

}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

NSLog(@"----result=%d i=%d",self.pro1,i);

});

复制代码

上述例子中,全局变量i理论上应该最后得到1,而实际上却几率性得到0,-1,2,-2,1。

为了避免错误,很多操作系统或编译器都提供了一些常用原子化操作的内建函数或API,包括把一些实际是多条指令的常用表达式。上述操作中,将i++/i--,替换为 OSAtomicIncrement32(&i) / OSAtomicDecrement32(&i) ,将得到预期的结果1

下边列举了不同平台上原子操作API的部分例子

windows API

macOS/iOS API

gcc内建函数

作用

InterlockExchange

OSAtomicAdd32

AO_SWAP

原子的交换两个值

InterlockDecrement

OSAtomicDecrement32

AO_DEC

原子的减少一个值

InterlockIncrement

OSAtomicIncrement32

AO_INC

原子的增加一个值

InterlockXor

OSAtomicXor32

AO_XOR

原子的进行异或

在OC中,属性变量的atomoc修饰符,起到的作用跟上述API相似,编译器会通过锁定机制确保所修饰变量的原子性,而且它是默认情况下添加的。而在实际应用场景中,在操作属性值时一般会包含三步(读取、运算、写入),即便写操作是原子,也不能保证线程安全。而ios中同步锁的开销很大(macOS中没有类似问题),所以一般会加上nonatomic修饰。

@property (nonatomic,assign)int pro1;

复制代码

在实际业务中,通常是给核心业务代码加同步锁,使其整体变为原子的,而不是针对具体的属性读写方法。

1.2 可重入与线程安全

函数被重入 一个程序被重入,表示这个函数没有执行完成,由于外部因数或内部调用,又一次进入函数执行。函数被重入分两种情况

多个线程同时执行这个函数

函数自身(可能是经过多层调用之后)调用自身

可重入 一个函数称为可重入的,表明该函数被重入之后没有产生任何不良后果。

可重入函数具备以下特点:

不使用任何局部(静态)非const变量

不使用任何局部(静态)或全局的非const变量的指针

仅依赖调用方法提供的参数

不依赖任何单个资源提供的锁(互斥锁等)

不调用任何不可重入的函数

可重入是并发的强力保障,一个可重入函数可以在多线程环境下放心使用。也就是说在处理多线程问题时,我们可以讲程序拆分为若干可重入的函数,而把注意的焦点放在可重入函数之外的地方。

在函数式编程范式中,由于整个系统不需要维护多余数据变量,而是状态流方式。所以可以认为全是由一些可重入的函数组成的。所以函数式编程在高并发编程中有其先天的优势。

1.3 CPU的过度优化

1.3.1 乱序优化与内存屏障

cpu有动态调度机制,在执行过程中可能因为执行效率交换指令的顺序。而一些看似独立的变量实际上是相互影响,这种编译器优化会导致潜在不正确结果。

面对这种情况我们一般采用内存屏障(memory barrier)。其作用就相当于一个栅栏,迫使处理器来完成位于障碍前面的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。确保一个线程的内存操作总是按照预定的顺序完成。为了使用一个内存屏障,你只要在你代码里面需要的地方简单的调用 OSMemoryBarrier() 函数。

class A {

let lock = NSRecursiveLock()

var _a : A? = nil

var a : A? {

lock.lock()

if _a == nil {

let temp = A()

OSMemoryBarrier()

_a = temp

}

lock.unlock()

return _a

}

}

复制代码

值得注意的是,大部分锁类型都合并了内存屏障,来确保在进入临界区之前它前面的加载和存储指令都已经完成。

1.3.2 寄存器优化与volatile变量

在某些情况下编译器会把某些变量加载进入寄存器,而如果这些变量对多个线程可见,那么这种优化可能会阻止其他线程发现变量的任何变化,从而带来线程同步问题。

在变量之前加上关键字volatile可以强制编译器每次使用变量的时候都从内存里面加载。如果一个变量的值随时可能给编译器无法检测的外部源更改,那么你可以把该变量声明为volatile变量。在许多原子性操作API中,大量使用了volatile 标识符修饰。譬如 在系统库中,所有原子性变量都使用了

int32_tOSAtomicIncrement32( volatile int32_t *__theValue )

复制代码

##2.线程同步的主要方式--线程锁

线程同步最常用的方法是使用锁(Lock)。锁是一种非强制机制,每一个线程访问数据或资源之前,首先试图获取(Acquireuytreewq)锁,并在访问结束之后释放(release)。在锁已经被占用时获取锁,线程会等待,直到该锁被释放。

2.1 互斥锁(Mutex)

2.1.1 基本概念

互斥锁 是在很多平台上都比较常用的一种锁。它属于sleep-waiting类型的锁。即当锁处于占用状态时,其他线程会挂起,当锁被释放时,所有等待的线程都将被唤醒,再次对锁进行竞争。在挂起与释放过程中,涉及用户态与内核态之间的context切换,而这种切换是比较消耗性能的。

互斥锁和二元信号量很相似,唯一不同是只能由获取锁的线程释放而不能假手于人。在某些平台中,他是用二元信号量实现的。关于信号量,我们将在2.3中详细介绍。

互斥锁可以是多进程共享的,也可以是进程内线程可见的。它可以分为分为普通锁、检错锁、递归锁。让我们通过pthread中的pthread_mutex,来详细了解互斥锁的一些用法及注意事项。

2.1.2 pthread_mutex

pthread_mutex 是pthread中的互斥

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值