C/C++基础知识点——linux多线程编程

线程、进程的区别

线程:CPU调度的最小单元
进程:进行中的程序,资源分配的最小单元

线程和进程的区别及联系:

  1. 一个进程可以有多个线程,但至少要有一个进程
  2. 多个进程共享一个进程中的所有资源,而多个进程之间是没有联系的
  3. 一个进程中的某一线程中断,会造成整个进程的退出或者崩溃,而对于多个进程某一进程的中断不会影响其它进程的执行
  4. 多进程的开销要比多线程的开销大一倍

并行和并发的区别

  1. 并行是同时处理多个任务,而并发是可处理多个任务,但不一定同时
  2. 并行多用于多核处理,而并发多用于单核处理

多线程和多进程的区别以及应用场景

多进程:ngnix(master主进程管理多个work子进程)
多线程:redis 6.0(多个ID线程处理数据结构)

实现多线程有那些方式

在C++11未对多线程进行合并之前,Linux采用pthread_create(threadID, threadType, 指针函数,函数参数)

在C++11之后统一采用thread,调用join和detach两个接口

QT中多线程的实现方式,有以下两种方式:

  1. 继承Qthread类,重写run方法
  2. 直接使用moveToThread()接口将QObject子类移至线程中,内部的所有信号的槽函数均在线程中执行
  3. 使用QTConcurrent::run()类

sleep方法有什么作用,一般用来做什么

sleep是线程类Thread的方法,作用是使当前线程按时睡眠,可以放到任何位置

线程有哪几种状态,是如何转换的

运行期、挂起、死亡、正常退出及线程阻塞

线程同步,线程安全

线程安全:指当多个线程同时访问某一共享数据时,加一把锁,防止多个线程同时访问,当前时间只有一个线程访问,其他线程必须等待。

线程同步方式:事件、信号量、互斥量及临界区

死锁及死锁发生的条件

死锁:指两个或两个以上的线程在执行过程中,因抢占资源而造成一种相互等待的现象

死锁产生的原因:系统资源不足、线程推进顺序不当、资源分配不当

死锁的形成场景:忘记释放锁、单线程重复释放锁、多线程多锁申请、环形锁的申请

死锁的条件:互斥条件、请求和保持条件、不可抢占、循环等待

死锁的防止:

  1. 尽量避免同时只对一个互斥锁进行上锁;
  2. 互斥锁保护区域不要使用操作其它互斥锁的代码;
  3. 给锁定义顺序

死锁问题排查

  1. pstrack + 进程号
  2. 利用 GDB 工具进行排查

读写锁、自旋锁及互斥锁的区别

自旋锁与互斥锁的区别:

  1. 互斥锁是一种独占锁,互斥锁加锁失败后,线程会释放 CPU,给其他线程,而自旋锁加锁失败后,线程会忙等待,直到它拿到锁;

  2. 对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行;

  3. 互斥锁加锁失败时,会从用户态陷入到内核态,存在一定的性能开销成本,而自旋锁是通过 CPU 提供的 CAS 函数在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。

读写锁:

  1. 它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。
  2. 写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有;
读写锁可以分为「读优先锁」和「写优先锁」:

读优先锁:读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取写锁。

写优先锁:
写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。

上面两种情况,无论哪种方式都会造成【饥饿】现象,我了避免这种现象出现,提出公平读写锁(将读写锁加入队列中
进行排队)

线程间通信方式

事件、信号量、互斥量及临界区

事件

是win32提供的一种运行线程间通信的方式。事件可以是激活状态也可以是未激活状态

事件根据其状态的变迁可以分为:手动重置事件和自动重置事件

手动重置事件

手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而一直保持为激发状态,直到程序重新把它设置为未激发状态

自动重置事件

自动重置事件被设置为激发状态后,会唤醒一个等待的线程,然后自动恢复为未激发状态

注意:自动重置事件调用 setEvent 和 pulseEvent 有可能引起死锁,必须小心

信号量

信号量是解决生产者和消费者问题的关键因素,是维护0到指定最大值之间的同步对象。信号量计数大于0表示有信号量、等于0表示无信号量;用于有限量共享资源的访问。

信号量用于解决生产者-消费者问题(通过互斥锁和条件变量来实现)。

互斥量

一个线程占用某一共享资源,其他线程必须等待,直到该资源得到释放,其他资源才能访问

条件变量

与互斥锁不同的是,条件变量是用来等待而不是上锁的,条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用(为了防止发生竞争条件),避免大量加锁造成的资源浪费。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  1. 一个线程等待“条件变量的条件成立”而挂起
  2. 另一个线程使“条件成立”(给出条件成立信号)

如何正确的使用条件变量

  1. 创建和销毁条件变量
    创建条件变量:pthread_cond_init
    销毁条件变量:pthread_cond_destroy

  2. 等待条件的发生
    条件不满足时,使用 pthread_cond_wait() / pthread_cond_timewait() // 可以指定超时时间 函数,来让线程进入休眠。当函数正常返回时,返回值为0。

  3. 条件触发(唤醒其他线程)
    pthread_cond_signal()
    pthread_cond_broadcast()

使用条件变量需要注意的地方

  1. 要考虑解锁和唤醒的顺序
    由于 pthread_cond_signal() 和 pthread_cond_broadcast() 函数的调用都不需要加锁,所以它们放到 pthread_mutex_unlock() 之前或者之后执行都是可以的。但在实际使用中,需要根据具体情况考虑它们的顺序,来使得程序高效运行。

  2. 要使用 while 而不是 if,避免虚假唤醒
    这是由于 wait 函数被唤醒时,存在虚假唤醒等情况,导致唤醒后发现,条件依旧不成立。因此需要使用 while 语句来循环地进行等待,直到条件成立为止。

  3. timewait 是 absolute time
    pthread_cond_timedwait() 函数的 abstime 指的是超时的绝对时间,而不是相对现在的时间间隔。这点经常会有人误会。

  4. pthread_cond_timedwait() 不一定会准时返回
    如果 pthread_cond_timedwait() 超时到了,但是这个时候 mutex 锁被其他线程持有,导致本线程不能锁定 mutex,无法进入临界区,那么 pthread_cond_timedwait() 就无法立即返回

互斥量和信号量的区别

  1. 互斥量用于线程的互斥,信号量用于线程的同步
  2. 互斥量的值只能为 0 或者 1 ,信号量的值为非负整数
  3. 互斥量的加锁和解锁必须由同一个线程执行,信号量可以由一个线程释放,另一个线程得到

sleep和wait区别

  1. sleep睡眠时,保持对象锁,仍然占有该锁,其他线程无法访问,休眠后自行恢复运行,无需其他线程唤醒
  2. wait睡眠时,释放对象锁,其他线程可以访问,休眠后需要其他线程进行唤醒

进程间通信方式

共享内存、管道、信号量、消息队列、socket套接字

一个进程可以创建多个线程,和什么有关?

由可用可用虚拟空间线程的栈的大小共同决定。

posted @ 2023-08-14 15:13  suntl  阅读(110)  评论(0编辑  收藏  举报