主页 > 开源代码  > 

C++面试题,进程和线程方面(1)

C++面试题,进程和线程方面(1)

文章目录 前言进程和线程有什么不同进程,线程的通讯方式什么是锁为什么说锁可以使线程安全加锁有什么副作用总结


前言

这是个人总结进程和线程方面的面试题。如果有错,欢迎佬们前来指导!!!


进程和线程有什么不同

进程是程序的动态执行的实例,每个进程都有独立的地址空间,资源(如文件描述符,内存地址空间)和系统状态。 独立性:进程之间资源隔离,一个进程崩溃不会直接影响到其它进程 唯一标识:通过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)

标签:

C++面试题,进程和线程方面(1)由讯客互联开源代码栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“C++面试题,进程和线程方面(1)