目录
一:线程
1:定义
线程是系统调度和分派的基本单位。 属于同一进程的线程,堆是共享的,栈是私有的。 属于同一进程的所有线程都具有相同的地址空间。
2:多线程的优点
1:创建速度快,方便高效的数据共享。多线程间可以共享同一虚拟地址空间。
2:较轻的上下文切换:不用切换地址空间,不用更改寄存器。
3:提供非均质的服务:如果全都是计算任务,那可以有效降低简单任务被复杂任务压住的概率。
3:线程切换到底是切换的什么
线程切换指的是在操作系统中,当一个进程或者线程正在执行的时候,由于某种原因需要暂停当前进程或线程,并且将cpu的资源分配给其他进程或线程执行,这个过程就是上下文切换。上下文包括寄存器,栈指针,程序计数器等。
这里使用一下别人的图片:加载A的上下文->将CPU的控制权交给A->保存A的上下文->加载B的上下文->将CPU交给B。
我们可以看到上面切换上下文需要消耗一定的资源,如果频繁的切换上下文会降低系统性能和稳定性。因此我们应减少线程的切换,而是使用同步机制(锁和信号量)来避免竞争条件。
4:什么时候使用多线程
1:需要频繁创建和销毁的,因为线程消耗的资源小。比如:web服务器来个连接就建立,断了就销毁。
2:需要进行大量的计算:大量计算就是消耗很多cpu,切换比较频繁。
3:强相关的使用线程:比如一个消息处理包括消息解码和业务处理。这两个任务的相关性就比较强,就可以进行分线程设计。而收发是弱相关,那就可以使用多进程处理。
4:多核分布的用线程,多进程分布使用进程。
5:多线程中锁的使用(线程同步)
因为多线程共享同一块内存的数据,因此我们需要使用锁来保证线程同步。
互斥锁,读写锁,自旋锁,条件变量,信号量,递归锁,屏障等。具体锁的讲解在下面。
二:进程
1:定义
进程依赖于程序运行而存在,进程是动态的,程序是静态的;
进程是操作系统进行资源分配和调度的一个独立单位
2:多进程的优点
1:编程相对容易:通常不需要考虑锁和同步资源的问题。
2:更强的兼容性:一个进程崩溃了,不会影响其他进程。
3:内核保证的隔离:数据和错误隔离,多进程架构可以做到一定程度的自我恢复:一个master进程守护多个worker进程,挂掉将其重启。
3:多进程的应用场景
1:nginx:多进程模式,一个master管理进程,多个worker工作进程。
2:chrome浏览器:一个网页崩溃不会影响到其他网页,并且网页之间互相隔离,不必担心一个网页中的恶意代码会获取其他网页中的敏感信息。
3:redis在rdb和aof持久化的时候,会进行fork子进程。
4:进程间通信的方式
进程间通信(IPC)是指不同进程之间进行数据交换和共享资源的过程。在现代操作系统中,进程是基本的运行单元,不同进程之间需要进行通信来完成协作任务。常用的进程间通信方式有以下几种:
管道(Pipe):管道是一种半双工的通信方式,可以实现父子进程之间或者兄弟进程之间的通信。管道有两个端口,分别连接到两个进程中,在一个端口写入数据,在另一个端口读取数据。
命名管道(FIFO):命名管道也是一种半双工的通信方式,可以实现无关联进程之间的通信。与管道不同的是,命名管道在文件系统中存在一个特定名称,并且可以通过该名称访问。
信号量(Semaphore):信号量是一种锁机制,用于保护临界区资源和控制并发访问。多个进程可以通过使用相同的信号量来互斥地访问某些资源。
共享内存(Shared Memory):共享内存允许多个进程直接访问同一个物理地址空间上的内存区域,避免了复制数据和上下文切换等开销。
消息队列(Message Queue):消息队列是一种异步通信方式,允许进程将数据发送到一个队列中,并由另一个进程从该队列中读取数据。
套接字(Socket):套接字是一种网络编程的通信方式,可以实现不同计算机上的进程之间进行通信。
三:线程和进程哪个好?
我先讲一下进程:
多进程肯定不会将系统放入到一个进程中的,那么就必然会碰到进程间通信的问题(IPC),其中最推荐Socket。因为Socket是最支持分布式部署的,而且还较容易实现多种语言的混合,而且还不用加锁了。当然Socket的性能也是不用担心的,当两个进程在本机中的时候可以使用回环地址,性能不会太低的。而且使用Socket还可以抓包,快速定位问题的位置。
那还需要线程干嘛?
1:如果频繁的创建进程,系统会容易挂掉,比如Web服务器,用来连接客户端。如果太多,开启多个进程,容易死机。
2:如果在业务层面,使用多进程,那我们会面临大量的IPC代码,很麻烦。
所以我们不能一味的只追求哪个好,而是分情况:
1:需要频繁创建销毁的优先用线程
2:需要进行大量计算的优先使用线程
3:强相关的处理用线程,弱相关的处理用进程
4:可能要扩展到多机分布的用进程,多核分布的用线程
5:满足需求的情况下,用你最熟悉、最拿手的方式
四:各种锁的使用
1:互斥锁
互斥锁是一种常见的线程同步机制,用于保护共享资源在多线程环境下的互斥访问,使得在同一时刻只有一个线程能够访问到共享资源。
我们可以使用原子操作 和互斥变量来实现一个互斥锁:
1:原子操作:我们在锁内部使用原子变量维护一个标志位,表示锁的状态,加锁则修改标志位,表示为锁定,如果修改成功则表示获取成功,失败则重试。解锁就是修改为未锁定状态。
2:互斥变量:和原子操作差不多,使用一个互斥量来作为标志位。
2:读写锁
读写锁是一种多线程同步机制,用于在读操作和写操作之间提供更好的并发性。读写锁允许多个线程同时进行读操作,但是写操作需要单独访问。
读优先和写优先的实现:
1:读优先:我们在内部维护一个计数器来表示当前进行读操作的线程数量,当没有写操作的时候,读操作可以并发执行,计数器递增。当读操作全部结束后,即计数器为0,写操作才可以执行,并且是独占的。读写操作之间的互斥访问可以使用互斥锁来实现。
2:写优先:在内部维护一个标志位表示当前是否有写操作。没有写操作的时候,可以进行并发读操作。当要写操作的时候,我们等待读操作结束,然后修改标志位,表示要写操作了。读写操作之间的互斥访问可以使用互斥锁来实现。
3:自旋锁
自旋锁是一种多线程同步机制,用于保护临界区代码,以防止多个线程同时访问共享资源。与互斥锁不同,自旋锁不会使线程进入阻塞状态,而是在获取锁时(自旋)不断循环检查锁是否可用,直到获取到锁为止。
自选等待:当一个线程发现这个自旋锁已经被别人使用了,那么这个锁就会进入自旋状态,在自选期间,这个锁不断的进行检查是否可用,这里的是一种忙等待状态,线程会一直占用CPU进行检查,直到锁被释放。
自旋锁的优点就是避免了线程上下文的切换和进程阻塞,适合于代码执行短,线程竞争不激烈的情况下。缺点是在自旋期间会一直占用CPU资源。因此自旋锁适合多核CPU的多线程并发操作,其中线程等待锁的时间较短。
4:条件变量
条件变量(Condition Variable)是一种多线程同步机制,用于在多个线程之间进行通信和协调。它允许一个或多个线程等待某个条件满足后才继续执行,从而避免了线程的忙等待。
1:创建条件:需要搭配互斥锁来使用,其实是条件变量内部需要引入一个互斥锁。
2:等待条件:当我们发现当前执行条件不满足的时候,我们可以调用wait函数,让线程进入休眠状态,也就是阻塞状态,并且会释放掉之前的互斥锁。
3:唤醒条件:当条件满足的时候我们可以使用唤醒函数来唤醒阻塞等待的一个或多个线程,被唤醒的线程会重新竞争锁。
4:再次检查:当被唤醒获取锁后,会再次进行检查状态,如果不满足可能会继续等待。也就是我们的return,这个进行检查。
5:信号量
信号量是一种多线程同步机制,用于控制对共享资源的访问。它通过一个计数器和一组等待队列来实现对资源的控制。 它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
1:创建信号量:我们先创建一个信号量对象,并初始化计数器的初始值,计数器的数量代表当前可用资源的数量。(比如5,那么就表示当前可以进入五个线程来进行并发访问)
2:获取资源:当一个线程需要访问共享资源的时候,需要先获取信号量,当计数器大于零,代表有可用资源,等于0,那就阻塞等待。
3:释放资源:当一个线程使用完共享资源后,需要释放当前持有的信号量,以便其他线程可以使用。释放后计数器加一,并唤醒等待队列中的某个线程。
4:等待和唤醒:等待操作会将这个线程加入到信号量的等待队列当中去,并将这个线程设置为阻塞状态,当信号量的计数器增加的时候,等待队列中的线程可能会被唤醒,并竞争信号量。
需要注意的是,信号量并不限定只能有一个线程访问资源,可以通过适当设置计数器的初始值来控制并发访问的数量。
6:信号量和自旋锁的区别
信号量:信号量本质上是一个整数值,和PV函数一起使用保证临界区的原子性。在linux内核中用于互斥。信号量可以被休眠
自旋锁:不能被休眠(如中断处理程序),一个自旋锁是一个互斥设备,它只能是两个值,锁定和解锁。自旋锁通常比信号量性能更高,但当存在自旋锁时,等待执行忙循环的处理器做不了任何有用的工作。
自旋锁加锁后是忙等待,死循环,不会被阻塞,而信号量是可以被阻塞等待的。