多线程基础之一:进程间同步问题的来源和解决方案

news/2024/5/18 7:14:33 标签: 多线程, 同步锁, mutex, semaphore

同步问题诞生的最本质的原因:In fact, a process may be interrupted at any point in its instruction stream, and processing core may be assigned to execute instructions of another process.总之一句话,关于共享对象的更改操作并非原子操作,如假设两个进程对于共享对象counter进行不同的操作:

Process_1: counter++;
Process_2:counter --;

一般地,counter++将被翻译成低级别的原子操作序列

counter++:
    1:  register1 = counter
    2:  register1 = register1 +1
    3:  counter = register1

而counter–将被翻译成

counter--:
    1:  register1 = counter
    2:  register1 = register1 -1
    3:  counter = register1

考虑到现今操作系统为了提供更好的实时响应特性,抢占式内核是主流设计,故而上面两个原子操作序列的执行顺序是可以互相交叉的,故而最后counter的结果是不确定的。这种有多个进程操作同一数据对象,并且结果依赖于此次具体执行顺序的情况被称作race condition(竞争状况)。


0.同步问题的解决方案核心要求:互斥性(exclusion) + 等待有限性(bounded waiting)

操作系统领域将多进程中涉及对共享数据对象进行操作的代码段称为critical section,而对于临界区的操作设计必须要满足两特性:互斥性和等待有限性。

关于临界区的互斥性,Peterson Solution给出了经典的针对两个竞争进程的临界区软件实现策略。

do {
   flag[i] = true;
   turn = j;
   while (flag[j] && turn == j); //单步循环,除非对方flag[j]=false,或者turn=i
   //因为即便是两个进程交互进入,在任何瞬时turn也只能取一个值,这也是Peterson方案只能适用于两个race process的原因
      ****critical section****

   flag[i] = false;
      ****remain section****
 }while (ture);

在单处理器架构下,多进程共享数据一致性问题可以很好地解决,只要涉及到对共享数据对象进行修改时,不准“打断”存在,这时非抢占式内核(non-preemptive kernel)的常用手段;但SMP多处理器架构下,“组织打断或抢占”的机制将会极大地影响系统效率,故而现今主流操作系统都是支持抢占式内核的,并且额外提供硬件支持(可以保证在硬件层面上,实现对于一个字的内容修改或两个字内容互相交换等操作的原子性)Hardware features can make any programming task easier and improve system efficiency.

如操作系统提供如下的原子操作atomic_operation: compare_and_swap()test_and_set()

boolean test_and_set(boolean *target) {
   boolean rv = *target;
   *target = true;
   return  rv;
} 
int compare_and_swap(int * value, int expected, int new_value){
    int temp = *value;

    if (*value == expected)
        *value = new_value;

    return temp;
}

然后通过这两个原子操作可以实现进程间加锁互斥

do {
   while (test_and_set(&lock)); //给lock进行加锁,原子操作
      ****critical section****
   lock = false;
      ****remain section****
 }while (ture);
do {
   while (compare_and_swap(&lock, 0, 1) != 0); //给lock进行加锁,原子操作
      ****critical section****
   lock = false;
      ****remain section****
 }while (ture);

上述操作虽然满足了互斥性,但不能保证等待有限性,可以通过以下设计满足bounded-waiting设计

do {
   waiting[i] = true;
   key = true;
   while (waiting[i] && key)
      key = test_and_set(&lock);
   waiting[i] = false;

      ****critical section****

   j = (i+1) % n;
   while ( (j != i) && !waiting[j] ) //此处遍历全等待进程数组一遍,保证每个进程至多等n-1 turns
      j = (j+1) % n;

   if (j == i) //当此前所有等待进程都获得操作临界区的机会后,才释放本轮的锁
      lock = false;
   else
      waiting[j] = false;

      ****remain section****

 }while (true);

而各进程间共享的数据为:

 boolean  waiting[n];
 boolean  lock;

这种基于硬件特性的原子性操作test_and_set()\compare_and_swap()手工构建的互斥并行方案是一种需要coder承担很多工作的设计,显然封装底层实现,提供抽象接口才是程序世界的王道


1.锁机制之一Mutex Lock

既然提供了硬件加锁的可能,操作系统必然也是为给出一系列方便程序员使用的锁机制The hardware-based solutions to the critical-section problem using atomical function eg. test_and_set() are complicated as well as generally inaccessible to application programmers. Instead, operating-systems designers build software tools to solve the critical-section problem. Mutex便是操作系统提供的诸多锁机制中的最简单的一个(Mutex是mutual exclusion的合并)。

锁机制的经典使用机制

acquire() {
    while (!available); //要么获取,要么一直重复该步消耗掉本次获取的所有CPU时间片
    available = false; //获取成功,加锁
}

release() {
    available = true;
}

do {
    acquire lock:
           ****critical section****
    release lock:
           ****remain section****
}while (true);

Mutex是在上面介绍的原子操作test_and_set()等atom lock基础上封装而来的,可以实现锁的互斥性,但是Mutex最大的问题在于它存在“空等”情况,如果process_i进入了临界区,其余进程必须一直调用acquire()直到分配成功。正是因为“空等”的存在,故而Mutex这类锁也被称为自旋锁。In fact, this type of mutex lock is also called a spinlock because the process “spins” while waiting for the lock to become available.

但也正是因为自旋空等的存在,自旋锁生效期间,等待进程没有产生进程上下文内容切换(no context switch is required),因为上下文内容切换也是要花费时间的(a context switch may take considerable time)。所以如果单一线程占用共享资源的时间较短时,采用自旋锁是合理的,其实就是比较盲等时间和让进程切换上下文执行其他内容带来的收益。


2.锁机制之二 semaphores

旗语semaphores分为计数counting和binary两种,前者取值范围是(-∞,+∞),后者只能在0和1间取值,binary semaphore相当于Mutex Locks。

Counting semaphores相当于为某一资源提供次数有限的访问权限,类似于线程池thread pool为外部服务请求进行处理的场景,需要用counting semaphores管理每时刻线程池空闲服务线程数目。

上面我们说道Mutex虽然简单,但是存在等待进程盲等的计算资源浪费情况。在Semaphores中,相比于Mutex存在盲等,Semaphores有着更经济的操作方式,一旦进程检测到Semaphores Value小于等于0,则当前进程自我阻断,进入一个和Semaphores绑定的等待队列中,该进程的状态被置为waiting状态,控制权交给CPU调度中心,由其选择下一进程执行(The block operation places a process into a waiting queue associated with the semaphore, and the state of the process is switched to the waiting state. The control is transferred to the CPU scheduler, which selects another process to execute.)而进入休眠状态的进程需wakeup()指令激活,然后进入ready queue等待获取资源。

typedef struct{
   atomic_t  count;
   int sleepers;
   wait_queue_head_t  wait;
} semaphore;

其中block()操作将会中止它的宿主进程,而wakeup(P)则会激活进程P,这两个功能函数均是由操作系统实现的(基于中断机制实现)。而Semaphores配备的等待队列采用的排队策略,最简单的当然是FIFO,但是配备其他策略也是可以的,并不影响semaphore的正常使用。

简单一句话,如果critical section的操作指令并不多,完全可以借助Mutex或者Binary Semaphores来实现锁机制,没必要使用Semaphores的这种配备等待队列的机制来实计算时间节约,但如果临界区内操作复杂(使用进程占用时间很长),这种情况下应该使用Semaphores配备等待队列机制,以集中计算资源给当前正在临界区中的进程使用。


http://www.niftyadmin.cn/n/1030879.html

相关文章

UWA发布|Unity手游性能蓝皮书

作为游戏行业的服务商,UWA不仅为游戏开发者提供高效的性能优化工具,也致力于为行业提供更全面、更具体的信息和服务。为此,UWA今天发布2019-2020年度手游蓝皮书,从总体性能数据、引擎各模块开销、内存占用等方面进行汇总分析&…

多线程基础之二:mutex和semaphore使用方法

mutex和semaphore都是内核对象,是用来实现多进程间或多线程锁机制的基础。本文将要介绍两者的使用方式。 0. 多线程锁机制涉及的Windows API创建mutex内核对象,用来作为线程间或进程间通信的API接口如下 HANDLE WINAPI CreateMutex( __in_opt LPSECUR…

Instruments如何看Mono内存分配

1)Instruments如何看Mono内存分配 ​2)关于Addressable v1.11.2的疑问 3)展开UV2时导致Mesh顶点数增加 4)提升Unity编辑器中代码的编译速度 5)Renderdoc调试的疑问 这是第217篇UWA技术知识分享的推送。今天我们继续为大…

多线程基础之三:使用event, mutex, semaphore实现多进程间互斥

前面文章介绍了使用mutex和semaphore在多线程场景中实现线程互斥。事实上,因为mutex, semaphore是内核对象,虽然是在某一个进程中创建的,但是由于进程间可以共享内核模块,故而使用mutex, semaphore在进程间作为互斥标识量也是可以…

多线程基础之四:Linux提供的原子锁类型atomic_t

在x86体系下,任何处理器平台下都会有一些原子性操作,在单处理器情况下,单步指令的原子性容易实现。但是在SMP多处理器情况下,只有那些单字的读(将变量读进寄存器)或写(从寄存器写入到变量地址&a…

Packages目录下Shader打包疑问

1)Packages目录下Shader打包疑问 ​2)如何关闭资源的RW选项 3)RenderTexture单个像素的色值大于Shader的输出值 4)客户端背包刷新机制 5)PBXProject.AddCapability添加失败 这是第218篇UWA技术知识分享的推送。今天我们…

多线程基础之五:Windows API提供的mutex和semaphore性能比较

Windows系统提供HANDLE WINAPI CreateSemaphore(xxx)和HANDLE WINAPI CreateMutex(xxx)直接提供Mutex和Semaphore两种内核对象供程序员使用在临界区互斥操作。在前面的多线程基础之一提到过Semaphore存在Binary和Counting两种,其中Binary Semaphore等同于Mutex&…

多线程基础之六:Pthread Win32实现的非阻塞请求机制的Semaphore

前面看到Windows API直接提供的Semaphore并没有为其配备等待队列,从而无法实现非阻塞请求机制以实现操作加速,对于临界区耗时的情况下,显然是存在实现非阻塞请求机制的Semaphore的。Linux下的Pthread库实现了这样的增加版Semaphore&#xff0…