linux的并发和竞争处理(最全)

在 Linux 系统中,处理并发和竞争的问题是非常重要的,特别是在多线程编程和多进程编程中。以下是处理并发和竞争的一些常见策略和技术:

互斥锁(Mutex)

互斥锁是最常见的并发控制机制之一。它可以确保在任何时候只有一个线程能够访问被保护的资源,从而防止竞争条件。在 Linux 中,可以使用 pthread_mutex_lock() 和 pthread_mutex_unlock() 函数来实现互斥锁。

互斥锁实现并发竞争处理的步骤

1、初始化互斥锁:在需要保护的临界区资源周围,首先要创建和初始化一个互斥锁。

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁

2、加锁(Lock):在访问临界区资源之前,需要先加锁,以防止其他线程同时访问。

pthread_mutex_lock(&mutex); // 加锁

3、访问临界区资源:在临界区内对共享资源进行操作。

4、解锁(Unlock):在临界区操作完成后,需要释放锁,以允许其他线程访问临界区资源。

pthread_mutex_unlock(&mutex); // 解锁

5、销毁互斥锁:在程序结束时,应该销毁不再需要的互斥锁。

pthread_mutex_destroy(&mutex); // 销毁互斥锁

互斥锁实现并发竞争处理的代码

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

#define NUM_THREADS 5

int shared_resource = 0;
pthread_mutex_t mutex;

void *thread_func(void *arg) {
    int thread_id = *(int *)arg;
    
    pthread_mutex_lock(&mutex); // 加锁
    
    // 临界区操作:对共享资源进行累加
    shared_resource += thread_id;
    printf("Thread %d: shared_resource = %d\n", thread_id, shared_resource);
    
    pthread_mutex_unlock(&mutex); // 解锁
    
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_args[NUM_THREADS];
    int i;

    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建多个线程
    for (i = 0; i < NUM_THREADS; i++) {
        thread_args[i] = i;
        pthread_create(&threads[i], NULL, thread_func, (void *)&thread_args[i]);
    }

    // 等待所有线程完成
    for (i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

信号量(Semaphore)

信号量是一种更加通用的同步原语,它允许多个线程同时访问共享资源,但可以限制同时访问资源的数量。在 Linux 中,可以使用 sem_init()、sem_wait() 和 sem_post() 函数来操作信号量。在 POSIX 线程库中,通常使用信号量来实现线程之间的同步。

信号量处理并发和竞争的一般步骤

创建信号量:首先需要创建一个信号量,并指定其初始值。可以使用 sem_init 函数来完成这个步骤。

对信号量进行操作:在需要访问共享资源之前,线程会尝试对信号量进行操作,以确定是否可以访问资源。可以使用 sem_wait 函数来尝试获取信号量,如果信号量的值大于 0,则表示可以访问资源;否则线程会阻塞等待。使用 sem_post 函数来释放信号量,表示资源已经被释放。

访问共享资源:一旦获得了信号量,线程就可以安全地访问共享资源。

释放信号量:线程在完成对共享资源的访问后,应该及时释放信号量,以便其他线程可以继续访问资源。

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>

#define NUM_THREADS 5

// 共享资源
int shared_resource = 0;

// 信号量
sem_t semaphore;

void *thread_func(void *arg) {
    int tid = *((int *)arg);

    // 尝试获取信号量
    sem_wait(&semaphore);

    // 访问共享资源
    printf("Thread %d: Accessing shared resource\n", tid);
    shared_resource++;
    printf("Thread %d: Updated shared resource: %d\n", tid, shared_resource);
    sleep(1);

    // 释放信号量
    sem_post(&semaphore);

    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];

    // 初始化信号量
    sem_init(&semaphore, 0, 1); // 初始值为 1

    // 创建线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]);
    }

    // 等待线程退出
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

    // 销毁信号量
    sem_destroy(&semaphore);

    return 0;
}

条件变量(Condition Variable)

条件变量允许线程在特定条件下等待或者被唤醒。它通常与互斥锁一起使用,用于线程之间的通信和同步。在 Linux 中,可以使用 pthread_cond_wait()、pthread_cond_signal() 和 pthread_cond_broadcast() 函数来操作条件变量。

条件变量处理并发和竞争条件的一般步骤

创建条件变量和互斥锁:首先需要创建一个条件变量和一个互斥锁,用于保护共享资源。可以使用 pthread_cond_init 函数和 pthread_mutex_init 函数来完成这个步骤。

对共享资源进行访问:线程在访问共享资源之前,需要先获取互斥锁,以确保同一时刻只有一个线程能够访问资源。

检查条件:线程在访问共享资源之前,可能需要检查某些条件是否满足。如果条件不满足,线程可以调用 pthread_cond_wait 函数来等待条件变量的发生。

等待条件变量的发生:如果条件不满足,线程会阻塞在条件变量上等待,直到其他线程通过 pthread_cond_signal 或 pthread_cond_broadcast 函数通知条件变量的发生。

释放互斥锁:一旦条件满足,线程就可以安全地访问共享资源。在访问完毕后,线程应该释放互斥锁,以便其他线程可以访问资源。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

#define NUM_THREADS 5

// 共享资源
int shared_resource = 0;

// 条件变量和互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *thread_func(void *arg) {
    int tid = *((int *)arg);

    // 获取互斥锁
    pthread_mutex_lock(&mutex);

    // 检查条件
    while (shared_resource < tid) {
        // 等待条件变量的发生
        pthread_cond_wait(&cond, &mutex);
    }

    // 访问共享资源
    printf("Thread %d: Accessing shared resource\n", tid);
    shared_resource++;
    printf("Thread %d: Updated shared resource: %d\n", tid, shared_resource);
    sleep(1);

    // 释放互斥锁
    pthread_mutex_unlock(&mutex);

    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];

    // 创建线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]);
    }

    // 发出条件变量的通知
    pthread_mutex_lock(&mutex);
    pthread_cond_broadcast(&cond);
    pthread_mutex_unlock(&mutex);

    // 等待线程退出
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}

读写锁(Read-Write Lock)

读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这在某些场景下可以提高性能。在 Linux 中,可以使用 pthread_rwlock_rdlock()、pthread_rwlock_wrlock() 和 pthread_rwlock_unlock() 函数来操作读写锁。读写锁适用于读操作频繁而写操作较少的场景,可以提高并发性能。

读写锁处理并发和竞争条件的一般步骤:

创建读写锁:首先需要创建一个读写锁,可以使用 pthread_rwlock_init 函数来完成这个步骤。

读取共享资源:对于读取共享资源的线程,首先需要获取读取锁(读锁),以允许多个线程同时读取共享资源。

写入共享资源:对于写入共享资源的线程,需要获取独占锁(写锁),以确保同一时刻只有一个线程能够写入共享资源。

释放锁:在读取或写入完成后,线程需要释放相应的锁,以便其他线程可以继续访问共享资源。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 定义读写锁并初始化为静态初始值
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int shared_resource = 0; // 共享资源

// 读取线程函数
void *reader(void *arg) {
    // 获取读锁
    pthread_rwlock_rdlock(&rwlock);
    // 读取共享资源
    printf("Reader: read shared resource: %d\n", shared_resource);
    // 释放读锁
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

// 写入线程函数
void *writer(void *arg) {
    // 获取写锁
    pthread_rwlock_wrlock(&rwlock);
    // 更新共享资源
    shared_resource++;
    printf("Writer: update shared resource: %d\n", shared_resource);
    // 释放写锁
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

int main() {
    pthread_t readers[3], writers[2];

    // 创建读取线程
    for (int i = 0; i < 3; ++i) {
        pthread_create(&readers[i], NULL, reader, NULL);
    }

    // 创建写入线程
    for (int i = 0; i < 2; ++i) {
        pthread_create(&writers[i], NULL, writer, NULL);
    }

    // 等待读取线程结束
    for (int i = 0; i < 3; ++i) {
        pthread_join(readers[i], NULL);
    }

    // 等待写入线程结束
    for (int i = 0; i < 2; ++i) {
        pthread_join(writers[i], NULL);
    }

    return 0;
}

原子操作

原子操作是不可中断的操作,它们可以确保在多线程环境中对共享数据的访问是原子的,即不会被其他线程打断。在 C/C++ 中,可以使用原子操作来避免竞争条件和数据竞争的问题。

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> shared_resource(0);

void reader() {
    int value = shared_resource.load(); // 读取共享资源的值
    std::cout << "Reader: read shared resource: " << value << std::endl;
}

void writer() {
    shared_resource.fetch_add(1); // 更新共享资源的值
    std::cout << "Writer: update shared resource: " << shared_resource << std::endl;
}

int main() {
    std::thread readers[3], writers[2];

    // 创建读取线程
    for (int i = 0; i < 3; ++i) {
        readers[i] = std::thread(reader);
    }

    // 创建写入线程
    for (int i = 0; i < 2; ++i) {
        writers[i] = std::thread(writer);
    }

    // 等待读取线程结束
    for (int i = 0; i < 3; ++i) {
        readers[i].join();
    }

    // 等待写入线程结束
    for (int i = 0; i < 2; ++i) {
        writers[i].join();
    }

    return 0;
}

除了上述的一些锁,还有一些其他的方法处理竞争和并发。比如,在代码中使用锁和同步原语时,需要小心避免出现竞争条件。可以使用工具如 Valgrind、ThreadSanitizer 等来检测和调试并发问题。尽量避免使用全局变量或者共享数据结构,尽可能将数据局部化,减少竞争条件的发生。根据具体的场景和需求选择合适的并发控制策略,例如读多写少的场景适合使用读写锁,而写多的场景适合使用互斥锁。

  • 28
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
第1章 操作系统概述 1 1.1 认识操作系统 1 1.1.1 从使用者角度 1 1.1.2 从程序开发者角度 2 1.1.3 从操作系统在整个计算机系统所处位置 2 1.1.4 从操作系统设计者的角度 3 1.2 操作系统的发展 4 1.2.1 操作系统的演变 4 1.2.2 硬件的发展轨迹 5 1.2.3 软件的轨迹 6 1.2.4 单内核与微内核操作系统 7 1.3 开放源代码的Unix/Linux操作系统 8 1.3.1 Unix的诞生和发展 8 1.3.2 Linux的诞生 9 1.3.3 操作系统标准POSIX 9 1.3.4 GNU和Linux 9 1.3.5 Linux的开发模式 10 1.4 Linux内核 10 1.4.1 Linux内核的位置 10 1.4.2 Linux内核的作用 11 1.4.3 Linux内核子系统 11 1.5 Linux内核源代码 13 1.5.1 多版本的内核源代码 13 1.5.2 Linux内核源代码的结构 13 1.5.3 Linux内核源代码分析工具 14 习题1 15 第2章 内存寻址 17 2.1 内存寻址简介 17 2.1.1 Intel x86 CPU寻址方式的演变 18 2.1.2 IA32寄存器简介 19 2.1.3 物理地址、虚拟地址及线性地址 21 2.2 分段机制 22 2.2.1 地址转换及保护 24 2.2.2 Linux的段 24 2.3 分页机制 25 2.3.1 页与页表 25 2.3.2 线性地址到物理地址的转换 28 2.3.3 分页示例 28 2.3.4 页面高速缓存(cache) 29 2.3.5 Linux的分页机制 30 2.4 Linux的汇编语言 31 2.4.1 AT&T与Intel汇编语言的比较 31 2.4.2 AT&T汇编语言的相关知识 32 2.5 Linux系统地址映射示例 33 习题2 35 第3章 进程 37 3.1 进程介绍 37 3.1.1 程序和进程 37 3.1.2 进程的层次结构 38 3.1.3 进程状态 39 3.1.4 进程实例 40 3.2 进程控制块 41 3.2.1 进程状态 42 3.2.2 进程标识符 43 3.2.3 进程之间的亲属关系 43 3.2.4 进程控制块的存放 44 3.3 进程的组织方式 45 3.3.1 进程链表 45 3.3.2 散列表 46 3.3.3 可运行队列 47 3.3.4 等待队列 47 3.4 进程调度 48 3.4.1 基本原理 48 3.4.2 时间片 50 3.4.3 Linux进程调度时机 50 3.4.4 进程调度的依据 51 3.4.5 调度函数schedule()的实现 52 3.5 进程的创建 54 3.5.1 创建进程 55 3.5.2 线程及其创建 56 3.6 与进程相关的系统调用及其应用 58 3.6.1 fork系统调用 58 3.6.2 exec系统调用 59 3.6.3 wait系统调用 60 3.6.4 exit系统调用 62 3.6.5 进程的一生 63 3.7 与调度相关的系统调用及应用 63 习题3 65 第4章 内存管理 67 4.1 Linux的内存管理概述 67 4.1.1 虚拟内存、内核空间和用户空间 67 4.1.2 虚拟内存实现机制间的关系 69 4.2 进程用户空间的管理 70 4.2.1 进程用户空间的描述 71 4.2.2 进程用户空间的创建 74 4.2.3 虚存映射 76 4.2.4 进程的虚存区示例 76 4.2.5 与用户空间相关的系统调用 78 4.3 请页机制 79 4.3.1 缺页异常处理程序 79 4.3.2 请求调页 81 4.3.3 写时复制 83 4.4 物理内存的分配与回收 83 4.4.1 伙伴算法 85 4.4.2 物理页面的分配 86 4.4.3 物理页面的回收 88 4.4.4 slab分配模式 89 4.4.5 内核空间非连续内存区的分配 93 4.5 交换机制 95 4.5.1 交换的基本原理 95 4.5.2 页面交换守护进程kswapd 99 4.6 内存管理实例 99 4.6.1 相关背景知识 100 4.6.2 代码体系结构介绍 100 4.6.3 实现步骤 103 4.6.4 程序代码 103 习题4 108 第5章 断和异常 110 5.1 断的基本知识 110 5.1.1 断向量 110 5.1.2 外设可屏蔽断 111 5.1.3 异常及非屏蔽断 112 5.1.4 断描述符表 112 5.1.5 相关汇编指令 113 5.2 断描述符表的初始化 114 5.2.1 IDT表项的设置 114 5.2.2 对陷阱门和系统门的初始化 115 5.2.3 断门的设置 116 5.3 处理 116 5.3.1 断和异常的硬件处理 116 5.3.2 断请求队列的建立 117 5.3.3 处理程序的执行 119 5.3.4 从断返回 121 5.4 断的下半部处理机制 121 5.4.1 为什么把断分为两部分来处理 122 5.4.2 小任务机制 122 5.4.3 下半部 124 5.4.4 任务队列 125 5.5 断应用——时钟断 125 5.5.1 时钟 125 5.5.2 时钟运作机制 126 5.5.3 Linux的时间系统 127 5.5.4 时钟处理程序 128 5.5.5 时钟断的下半部处理 129 5.5.6 定时器及其应用 129 习题5 132 第6章 系统调用 133 6.1 系统调用与应用编程接口、系统命令、内核函数的关系 133 6.1.1 系统调用与API 133 6.1.2 系统调用与系统命令 134 6.1.3 系统调用与内核函数 134 6.2 系统调用处理程序及服务例程 135 6.2.1 初始化系统调用 136 6.2.2 system_call()函数 136 6.2.3 参数传递 137 6.2.4 跟踪系统调用的执行 139 6.3 封装例程 140 6.4 添加新系统调用 141 6.5 实例——利用系统调用实现一个调用日志收集系统 143 6.5.1 代码体系结构 143 6.5.2 把代码集成到内核 146 6.5.3 实现步骤 148 习题6 148 第7章 内核的同步 149 7.1 临界区和竞争状态 149 7.1.1 临界区举例 149 7.1.2 共享队列和加锁 150 7.1.3 确定保护对象 151 7.1.4 死锁 152 7.1.5 并发执行的原因 153 7.2 内核同步方法 153 7.2.1 原子操作 153 7.2.2 自旋锁 155 7.2.3 信号量 156 7.3 并发控制实例 157 7.3.1 内核任务及其并发关系 158 7.3.2 实现机制 158 7.3.3 关键代码解释 162 7.3.4 实现步骤 163 习题7 164 第8章 文件系统 165 8.1 Linux文件系统基础 165 8.1.1 Linux文件结构 165 8.1.2 Linux文件系统 166 8.1.3 文件类型 167 8.1.4 文件访问权限 168 8.2 虚拟文件系统 168 8.2.1 虚拟文件系统的引入 168 8.2.2 VFS的数据结构 170 8.2.3 VFS超级块数据结构 171 8.2.4 VFS的索引节点 173 8.2.5 目录项对象 174 8.2.6 与进程相关的文件结构 176 8.2.7 主要的数据结构之间的关系 179 8.3 文件系统的注册、安装与卸载 180 8.3.1 文件系统的注册和注销 180 8.3.2 文件系统的安装 181 8.3.3 文件系统的卸载 183 8.4 页缓冲区 183 8.4.1 address_space对象 183 8.4.2 address_space对象的操作函数表 184 8.5 文件的打开与读写 185 8.5.1 打开文件 185 8.5.2 读写文件 187 8.6 编写一个文件系统 189 8.6.1 Linux文件系统的实现要素 189 8.6.2 什么是romfs文件系统 191 8.6.3 romfs文件系统的布局与文件结构 191 8.6.4 具体实现的对象 192 习题8 195 第9章 设备驱动 196 9.1 概述 196 9.2 设备驱动程序基础 198 9.2.1 I/O端口 199 9.2.2 设备文件 200 9.2.3 处理 201 9.2.4 设备驱动程序框架 203 9.3 字符设备驱动程序 204 9.3.1 字符设备驱动程序的注册 204 9.3.2 简单的字符设备驱动程序示例 205 9.4 块设备驱动程序 208 9.4.1 块设备驱动程序的注册 209 9.4.2 块设备请求 212 习题9 215 附录A 内核的链表 216 A.1 链表数据结构简介 216 A.2 内核链表数据结构的定义及初始化 217 A.3 操作链表的接口 218 A.4 遍历链表 219 附录B 内核模块 221 B.1 什么是模块 221 B.2 编写一个简单的模块 221 B.3 模块编程的基础知识 222 B.4 模块的编译 224 B.5 模块实用程序modutils 226 附录C Linux内核编译 228 C.1 内核简介 228 C.2 为什么重新编译内核 228 C.3 内核编译模式 229 C.4 新版本内核的获取和更新 229 C.5 内核编译 230 C.6 修改并重启管理器 232 附录D Linux编程基础(C语言环境) 233 D.1 Linux编程常识 233 D.1.1 相关标准(ANSI C、POSIX、SVID、XPG) 233 D.1.2 函数库和系统调用 234 D.1.3 在线文档(man、info、HOWTO) 235 D.1.4 C语言编程风格 237 D.2 Linux上的C/C++编译器和调试器 238 D.2.1 运行gcc/egcs 238 D.2.2 gcc/egcs的主要选项 240 D.2.3 gdb简介 240 D.2.4 gdb的常用命令 241 D.2.5 gdb使用示例 242 D.3 GNU make和makefile 243 D.3.1 GNU make 243 D.3.2 makefile的基本结构 243 D.3.3 makefile的变量 244 D.3.4 GNU make的主要预定义变量 245 D.3.5 GNU make的隐含规则 245 D.3.6 运行make 246

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

稚肩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值