当前位置: 首页 > news >正文

Lecture 9: Concurrency 2

上次幻灯片从进程(processes)的角度定义了死锁和临界区,这是打字错误吗

答:这并没有太大的区别。对于所讨论的定义,您需要两个关键的东西:

  • 并发执行多个指令序列——线程和进程都满足此属性。
  • 共享资源——线程更容易共享资源。有一些机制可以在进程之间共享资源,包括满足互斥的资源。一个标准的例子是被命名为信号量

回顾:

  1. 并发性问题的示例(例如,counter++)
  2. 并发性问题的根本原因——不可预测的执行顺序。
  3. 潜在的解决方案-关键部分强制互斥
  4. 进一步的问题——死锁

互斥方法

基于软件:Peterson的解决方案
基于硬件:test_and_set(), swap_and_compare()
基于操作系统:内核阻塞。

基于软件:Peterson的解决方案

Peterson方案是一种基于软件的解决方案,在旧机器上运行良好
使用了两个共享变量

  • turn:表示下一个进入临界区的进程是哪个
  • bool flag [2]:表示一个进程准备进入临界区

推广到多个进程
两个进程的Peterson解满足所有“临界区要求”(互斥、进度、公平性)

过程i的Peterson解:

do {flag[i] = true; // i wants to enter critical sectionturn = j; // allow j to access firstwhile (flag[j] && turn == j);// whilst j wants to access critical section// and its j’s turn, apply busy waiting// CRITICAL SECTION, e.g. counter++flag[i] = false;// remainder section
} while (...);

过程j的Peterson解:

do {flag[j] = true; // j wants to enter critical sectionturn = i; // allow i to access firstwhile (flag[i] && turn == i);// whilst i wants to access critical section// and its i’s turn, apply busy waiting// CRITICAL SECTION, e.g. counter++flag[j] = false;// remainder section
} while (...);

互斥要求

变量turn一次最多只能有一个值

  • flag [i]和flag [j]在进入临界区时都为true
  • turn是一个只能存储一个值奇异变量
  • 因此,while(flag[i] && turn == i)或while(flag[j] && turn == j)中最多有一个为真,并且最多有一个进程可以进入其临界区(互斥)。

进程要求

任何进程都必须能够在某个时间点进入其临界区域

       ⇒flag[j] == false
⇒while(flag[j] && turn == j)将终止进程 i
⇒i 进入临界区

公平/有限等待要求

公平分配的等待时间/进程不能无限期地等待

如果Pi和Pj都想进入临界区域:
⇒flag[i] == flag[j] = true
⇒turn是 i 或 j ⇒假设turn == i ⇒while(flag[j] && turn == j)终止,i 进入临界区
⇒i完成临界区⇒flag[i] = false ⇒while(flag[i] && turn == i)终止,j进入临界区。

即使它再次循环,它也会设置turn = j,让另一个线程先进入。

基于硬件:原子操作指令

将test_and_set()和swap_and_compare()指令实现为一组原子(=不可中断)指令

它们可以与锁变量结合使用,如果锁正在使用,则假定为true(或1)

test_and_set()
  • 剩余代码”中的进程/线程不会影响对临界区的访问
  • 如果进程 j 不想进入它的临界区
  • 读取和设置变量显示为单个指令
  • 如果同时调用test_and_set() / compare_and_swap(),它们将依次执行
// Test and set method
bool test_and_set(bool* bIsLocked) {bool rv = *bIsLocked;*bIsLocked = true;return rv;
}// Example of using test and set method
do {// WHILE the lock is in use, apply busy waitingwhile (test_and_set(&bIsLocked));// Lock was false, now true// CRITICAL SECTION...bIsLocked = false;...// remainder section
} while (...)

test_and_set必须是原子的/不可中断的

compare_and_swap()
    // Compare and swap methodint compare_and_swap(int* iIsLocked, int expected, int new_value) {int const old_value = *iIsLocked;if(old_value == expected)*iIsLocked = new_value;return old_value;}do {// While the lock is in use (i.e. == 1), apply busy waitingwhile (compare_and_swap(&iIsLocked, 0, 1));// Lock was false, now true// CRITICAL SECTION...iIsLocked = 0;...// remainder section
} while (...);

基于操作系统

test_and_set和compare_and_swap级别比较低,需要忙等。
操作系统可以使用这些硬件指令来实现更高级别的互斥机制,即互斥体(mutexes)信号量(semaphores

互斥体

互斥体是提供互斥的抽象。(Mutexes = mutual exclusion)
互斥锁提供了两个函数的接口:

  • acquire(&mutex):在进入临界区之前调用,当临界区没有其他人时返回。
  • release (&mutex):在退出临界区后调用,允许其他线程获取互斥锁。应该只在匹配获取之后调用。

此接口的细节可能会有所不同——名称或使用面向对象语言中的方法。
如何准确地提供这个接口是一个实现细节

  • 在朴素的假设下,我们可以使用Peterson算法。
  • 我们可以使用原子硬件操作和忙等。
  • 操作系统可能会阻塞试图获取不可用互斥锁的线程。
  • 根据设计假设,可以选择结合多种策略的混合解决方案来优化性能。

例子:Pthreads互斥锁

int counter = 0;
pthread_mutex_t lock;
void* calc(void* param) {int const iterations = 50000000;for(int i = 0; i < iterations; i++) {pthread_mutex_lock(&lock); // acquirecounter++;pthread_mutex_unlock(&lock); // release}return 0;
}int main() {pthread_t tid1 = 0, tid2 = 0;pthread_mutex_init(&lock,NULL);pthread_create(&tid1, NULL, calc, 0);pthread_create(&tid2, NULL, calc, 0);pthread_join(tid1,NULL);pthread_join(tid2,NULL);printf("The value of counter is: %d\n", counter);
}

理解

  1. 能否“纯粹软件”地在并发硬件上实现互斥,完全不依赖操作系统或硬件特殊指令?
    • 在 单处理器、关中断 的模型里可以:只要把时钟中断关掉,当前线程独占 CPU,直到主动开中断,自然互斥。
    • 在 多核/乱序/缓存一致性 的现代硬件上,不行。任何纯软件算法(Dekker、Peterson、Lamport Bakery 等)都隐含要求“读/写按某种顺序对所有 CPU 立即可见”,而现代 CPU 的重排序、Store Buffer、Invalidate Queue 会打破这些顺序,导致算法失效。因此必须至少依赖一条“有同步语义”的硬件原语(Test-And-Set、Load-Link/Store-Conditional、CAS、SWAP、fence 等)。→ 结论:现代并发硬件下无法仅靠软件实现可靠互斥。

  2. Peterson 算法在现代机器上能用吗?
    不能直接使用。它隐含“写后读”必须立即全局可见的假设;现代 CPU 的乱序执行和缓存延迟会违反这一假设,出现丢失更新或死锁。
    • 若 在每条关键读写前后插入全屏障(memory fence),则可恢复正确性,但这就等于借用了硬件提供的同步支持,不再是“纯软件”了。
    → 作为教学实验,可在 x86 上用 volatile+_mm_mfence() 验证,但生产代码不会用它。

  3. 只读共享变量还需要加锁吗?
    读本身不需要互斥,但有两个陷阱:
    a) “读-改-写”的组合(即使你觉得只是读)仍要保护;
    b) 如果变量可能被并发写入,读线程可能看到撕裂值(例如跨缓存行的 64-bit 整数在 32 位总线上)。
    • 因此:
    – 若变量初始化后永不改变,或已用 const/static const 修饰,则无需锁;
    – 若变量仍可能被写,即使当前代码段只读,也应使用原子读、读锁或 RCU 等机制,以保证可见性和原子性

http://www.dtcms.com/a/333770.html

相关文章:

  • AAAI爆款:目标检测新范式,模块化设计封神之作
  • fs模块_写入文件
  • 高可用双向存储服务GlusterFS
  • 【SpringBoot】SpringBoot 整合JDBC、Mybatis、Druid
  • PCA降维理论详解
  • Spring Boot 拦截器详解
  • 固定资产管理系统 OCR 识别功能技术解析
  • 无脑整合springboot2.7+nacos2.2.3+dubbo3.2.9实现远程调用及配置中心
  • 强制从不抱怨环境。
  • [Julia] 网络和流
  • vue2 + SimpleMindMap 制作思维导图
  • 野指针:程序崩溃的隐形杀手
  • 订单状态定时处理(Spring Task 定时任务)
  • 机械学习---词向量转化评价,附代码实例
  • 力扣(接雨水)——单调栈
  • 第454题.四数相加II
  • JavaWeb开发_Day12
  • 基于Selenium的web自动化框架
  • 电视同轴电缆全面指南:从基础到应用,批量测量一键计量
  • 第四章:大模型(LLM)】06.langchain原理-(2)langchain Chain的使用方法
  • 力扣top100(day04-03)--二分查找
  • MqSQL中的《快照读》和《当前读》
  • [论文笔记] WiscKey: Separating Keys from Values in SSD-Conscious Storage
  • Linux core dump
  • Flutter开发 webview_flutter的基本使用
  • MC0423铺砖块
  • Linux系统编程—Linux基础指令
  • OpenCV Python——图像查找(特征匹配 + 单应性矩阵)
  • Linux软件编程(五)(exec 函数族、system、线程)
  • SQL:生成日期序列(填补缺失的日期)