文章目录
- 前言
- 进程和线程有什么不同
- 进程,线程的通讯方式
- 什么是锁
- 为什么说锁可以使线程安全
- 加锁有什么副作用
- 总结
前言
这是个人总结进程和线程方面的面试题。如果有错,欢迎佬们前来指导!!!
进程和线程有什么不同
进程是程序的动态执行的实例,每个进程都有独立的地址空间,资源(如文件描述符,内存地址空间)和系统状态。
独立性:进程之间资源隔离,一个进程崩溃不会直接影响到其它进程
唯一标识:通过PID和PPID标识,形成树状结构(init为根进程)
动态性:生命周期包括创建(fork()),运行,阻塞,终止等状态变化
线程是进程内的执行单元,共享进程的资源(如内存,文件描述符),但拥有独立的栈和寄存器
轻量级:线程创建和切换的开销远小于进程
共享性:线程可直接访问进程的全局变量,通信更高效,但也需要同步机制(如互斥锁)来避免竞争
调度单位:线程是CPU调度的最小单位,进程是资源分配的最小单位
资源分配差异
进程是操作系统资源分配的基本单位,每个进程都有独立的内存地址空间(代码段,数据段,堆栈段),文件描述符等资源。这意味着每个进程在内存中都有自己的私有数据和执行环境,互补影响。
线程是进程内的执行流,共享同一进程的资源,包括内存空间,文件描述符等。多个线程可以访问同一个进程的内存空间,因此线程之间的通信和数据共享更加方便
调度机制
进程是操作系统调度的基本单位,由操作系统进行调度管理。线程是CPU调度的基本单位,由内核调度器负责。
进程切换:需要切换虚拟地址空间,寄存器,文件描述符等,开销大。
线程切换:仅需切换栈和寄存器,共享地址空间,开销小。
创建与销毁
创建进程需分配独立内存,初始化PCB,调用fork()系统调用;销毁进程需要回收全部资源,耗时较长。
创建线程仅需分配栈和线程控制块,调用pthread_create()库函数;销毁线程仅释放栈和线程控制块,资源回收简单。
通信机制对比
进程需通过IPC机制:管道,消息队列,共享内存,信号量来实现进程间通信,同步需求低(资源独立)。
线程直接通过共享内存(全局变量,堆)无需额外机制,同步需求高(需互斥锁,条件变量等避免数据竞争)。
进程,线程的通讯方式
进程间通信方式
- 管道(pipe)
匿名管道:基于内存的FIFO队列,仅限于父子进程或兄弟进程间单向通信。通过pipe()系统调用创建,读写端由不同进程分别关闭
命名管道:通过文件系统路径标识,允许无亲缘关系的进程通信。使用mkfifo()创建,支持双向通信但需自行管理同步
特定:简单高效,但容量有限且仅支持半双工,适合简单数据流传输- 消息队列(message queue)
结构:内核维护的链表结构,消息按FIFO或优先级排序存储,支持多进程异步读写。
使用场景:适合需要解耦生产者和消费者的场景(如任务分布系统),支持支持化消息和确认机制,但需处理消息丢失或重复问题
如System V的msgget/msgsnd/msgrcv或POSIX的mq_open/mq_send/mq_receive- 共享内存(shared memory)
原理:多个进程映射同一个物理内存区域,直接读写数据。
同步机制:
信号量:通过PV操作控制访问权限,如System V的semp或POSIX信号量
互斥锁/条件变量:在共享内存中嵌入锁结构,配合线程同步
优势:速度最快,适合大数据量高频通信(如视频处理);缺点是需要显示同步,编程复杂度高。- 信号(signal)
用途:异步通知进程特定事件(如SIGKILL终止进程)。通过kill()或signal()发送和处理
局限性:信号编号少,不适合复杂数据传递,多用于进程控制- 套接字(socket)
跨进程特性:支持本地和网络通信。通过socket()/bind()/listen()/accept()建立连接,实现全双工通信
应用场景:分布式系统,C/S架构,需要处理序列化和协议设计- 信号量(semaphores)
功能:计数器机制协调资源访问,解决互斥与同步问题。分为System V信号量集和POSIX信号量
如初始化信号量为1(互斥锁),进程访问共享资源前执行sem_wait(),是否后执行sem_post()。
线程共享进程资源,通信更侧重同步而非数据传输
- 共享变量与锁机制
互斥锁(Mutex):确保临界区原子性。使用pthread_mutex_init()初始化,lock()/unlock()包含资源
读写锁(Read-Write-Lock):允许多读单写,提高并发性。通过pthread_rwlock实现,适用于读多写少场景- 条件变量(Condition Variable)
协作机制:线程等待特定条件成立(如资源就绪)。需与互斥锁配合,使用pthread_cond_wait()挂其线程,pthread_cond_signal()唤醒- 信号量(Semaphores)
线程级同步:与进程间信号类似,但作用域限于同一进程。POSIX无名信号量通过sem_init()初始化。
信号量的互斥与同步作用
使用信号量实现进程间的互斥与同步主要通过以下步骤:
- 初始化信号量:创建一个信号量并初始化其值。对于互斥访问,信号量通常初始化为1。列如,使用sem_init函数初始化信号量,或使用sem_open创建命名信号量并初始化
- 进入临界区前执行P操作:进程在进入临界区前执行P操作(等待操作),即sem_wait或semop函数。如果信号量大于0,则将其减1并允许进程进入临界区;如果信号量值为0,则进程被阻塞,直到信号量值大于0
- 退出临界区后执行V操作:进程在提出临界区后执行V操作(信号操作),即sem_post或semp函数。这将信号量加1,并唤醒一个等待该信号量的进程
- 销毁信号量:在不再需要信号量时,使用sem_destory或sem_unlink函数销毁信号量,释放相关资源
通过上诉步骤,信号量可以确保多个进程对共享资源的互斥访问,避免数据竞争和不一致。
什么是锁
锁是一种用于同步并发访问共享资源的机制,通过限制对共享资源的访问顺序,确保在任意时刻只有一个线程能进入临界区。其本质是内存中的一个整形变量,通过状态(如0表示空闲,1表示锁定)实现资源占用的原子控制。在多线程环境下,锁的作用包括:
1. 互斥访问:防止多个线程同时修改空闲数据,导致不可预测的结果
2. 数据一致性:确保线程操作空闲资源时的中间状态对其它线程不可见
3. 执行顺序控制:通过信号量等机制协调线程的执行流程
在linux系统中,常见的锁类型包括互斥锁(mutex),读写锁(rwlock),自旋锁(spinlock),信号量(semaphore)。
互斥锁(Mutex)
定义:互斥锁是最基本且最常用的锁类型,用于保护共享资源,确保在任何时候只有一个线程或进程可以访问该资源
实现:通过pthread_mutex_t类型实现,可以通过静态或动态方式创建。静态创建使用PTHREAD_MUTEX_INITALIZER,动态创建使用pthread_mutex_init函数。
操作:主要操作包括pthread_mutex_lock(获取锁),pthread_mutex_unlock(释放锁)和pthread_mutex_destroy(销毁锁)读写锁(RWLock)
定义:读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。这样在读操作频繁而写操作较少的场景下可以显著提高并发性能
实现:通过pthread_rwlock_t类型实现,可以通过静态或动态方式创建。静态创建使用PTHREAD_RWLOCK_INITALIZER,动态创建使用pthread_rwlock_init函数。
操作:主页操作包括pthread_rwlock_rdlock(获取读锁),pthread_rwlock_wrlock(获取写锁),pthread_rwlock_unlock(释放锁)和pthread_rwlock_destroy(销毁锁)信号量(Semaphore):
定义:信号量用于控制多个进程或线程对共享资源的访问,实现更复杂的同步需求
实现:通过sem_t类型实现,可以通过静态或动态方式创建。静态创建使用SEM_FAILED,动态创建使用sem_init函数
操作:主要操作包括sem_wait(获取信号量),sem_post(释放信号量)和sem_destroy(销毁信号量)
为什么说锁可以使线程安全
线程安全是指在多线程环境下,多个线程同时访问共享资源时,能够正确的处理共享数据,保证数据的一致性和正确性,而不会导致不确定的结果或产生竞态条件。具体来说,线程安全的代码在多线程并发执行时,能够按照预期正确运行,不会因为共享资源的并发访问而引发错误。
线程安全的特点:
1.原子性:对共享资源的操作是原子的,要么完全执行,要么不执行,不能被中断或分割
2.可见性:一个线程对共享变量的修改,其他线程能够立即看到
3.有序性:程序执行的顺序按照代码的先后顺序进行,避免指令重排序导致的问题
常见的线程安全问题
1.竞态条件:多个线程同时修改同一变量,导致结果不确定
2.死锁:两个或多个线程在执行过程中,因争夺资源而造成的一种僵局
3.活锁:两个或多个线程在执行过程中,因争夺资源而不断重复尝试,但都无法取的进展
4.饥饿:某个线程无法获得必要的资源或条件,导致无法继续执行
锁通过以下机制解决多线程并发问题:
1. 强制互斥访问临界区:
锁确保同一时间仅有一个线程进入临界区,其它线程需等待锁释放。解决了原子性问题,避免多个线程同时修改共享资源(互斥锁通过原子操作和等待队列,确保临界区代码串行执行)
2. 内存屏障(?):
锁的获取和释放操作隐含内存屏障,强制刷新缓存并阻止指令重排,从而保证可见性和有序性
3. 防止竞态条件:
竞态条件源于多个线程对共享资源的非协调访问。锁通过强制同步,将并发操作转换为顺序执行,消除执行顺序的不确定性(若两个线程同时执行a++,未加锁可能因非原子操作导致结果错误;加锁后,操作变为原子的,结果正确)
加锁有什么副作用
性能开销:
锁的获取和释放操作本身需要消耗系统资源,可能成为程序性能瓶颈
- 原子操作与上下切换
原子指令开销:锁的底层依赖CPU的原子指令(如xchg, cmpxchg),这些指令会触发总线锁定或缓存同步,导致CPU流水线暂停,降低指令级并行性
上下文切换:互斥锁(Mutex)在锁竞争时会让线程进入睡眠状态(通过futex系统调用),触发线程调度和上下文切换。若锁竞争激烈,频繁的切换会显著增加CPU开销- 自旋锁的CPU浪费:
自旋锁(Spinlock)通过忙等待(Busy Waiting)获取锁,若锁持有时间较长,会持续占用CPU核心,浪费计算资源(单核CPU陷阱:在单核系统中,自旋锁可能导致死锁)- 缓存失效:
多个线程竞争同一锁时,锁变量的缓存行会在不同CPU核心之间频繁无效化,导致缓存一致性协议的额外开销
死锁
不合理的锁使用可能导致死锁,即多个线程互相等待对方释放锁,程序永久阻塞。死锁的四个必要条件:
1. 互斥访问:锁本身是独占资源
2. 持有并等待:线程持有锁的同时请求其它锁
3. 不可剥夺:锁只能由持有者释放
4. 循环等待:线程间形成环形等待链
活锁
活锁是死锁的一种变体:线程不断尝试获取锁失败,但未进入阻塞状态,导致CPU资源浪费(两个线程同时检测到对方持有锁,主动释放自己的锁并重试,形成无限循环)
其它副作用:
1. 资源泄漏:忘记释放锁导致其它线程永久阻塞
2. 错误处理复杂性:临界区内的代码若抛出异常或提取返回,需确保锁被释放(可通过RAII模式解决,如C++的std::lock_guard)
3. 锁与信号的交互:信号处理函数中不可使用非异步信号安全函数,否则可能引发死锁
总结
以上就是我总结的C++面试题,进程和线程方面(1)