【Are u OKay?——协程、线程、进程】 https://www.bilibili.com/video/BV1Wr4y1A7DS/?share_source=copy_web&vd_source=1e4d767755c593476743c8e4f64e18db
高并发:线程池,不要无休止的创建线程。--> task很小很快,thread需要切换(中断)-->协程
上下文保存(保存当前进程状态): 当操作系统决定切换到另一个进程时,首先需要保存当前进程的上下文信息,包括寄存器的状态、程序计数器、内存页表等。
切换到内核模式: 进程切换通常涉及从用户模式切换到内核模式,以便操作系统能够执行敏感的指令并更改进程的状态。
调度新进程: 操作系统选择要切换到的新进程,将其上下文信息加载到 CPU 寄存器和内存管理单元中。
切换到用户模式: 切换到新进程的用户模式,允许其执行。
在任务切换的过程中,通常会涉及到系统调用。系统调用是用户程序与操作系统之间进行通信的一种方式,用于请求操作系统提供服务。在任务切换中,以下情况可能触发系统调用:
上下文保存: 将当前进程的上下文信息保存到内存中,可能涉及到对内存的写操作,这可能需要调用操作系统提供的服务。
切换到内核模式: 用户程序切换到内核模式通常需要通过中断或异常来触发,这会涉及到一些与特权级别相关的系统调用,例如进入内核模式的中断服务例程。
调度新进程: 选择和调度新进程也可能涉及到系统调用,例如获取进程列表、更新进程状态等。
进程是应用程序/程序的执行副本,进程要管理硬件资源、CPU资源、文件资源、内存分页等,执行只需要CPU和内存。进程来回切换消耗资源,所以抽象出一个更小的单位线程,当一个程序启动以后(进程),会产生一个主线程,操作系统将计算资源不给进程,而是直接给主线程。进程是资源管理的单位,线程是程序执行的单位,线程只需要执行程序。操作系统的调度线程的执行,一个一个线程排队执行,每个线程执行时都有一个时间片,当时间片执行完了以后,切换下一个线程执行。程序执行产生一个进程,这个进程都会有一个主线程。但是操作系统调度的是自己的线程,自己的线程表,程序员在用户空间创建线程,但是真正执行的是操作系统的线程。操作系统的线程和用户线程是映射关系,也就是说,操作系统的线程才是真正的线程,程序员并不能直接执行直接的线程,必须要挂靠到操作系统的线程执行。高并发的场景下,会有很多task,为每个task创建一个线程(线程对象)很占用内存空间,每个线程都对应一个操作系统的线程(内核线程),这样操作系统忙不过来。所以设计了线程池技术,溢出线程池怎么办,让task排队(线程会回收到线程池)。
内核中的链表: 消息队列通常在内核中维护一个链表,用于存储进程发送的消息。每个消息都是链表中的一个节点,包含了消息内容、发送者、接收者等信息。这样的设计使得内核能够高效地管理和调度消息。
系统调用: 在Linux系统中,消息队列的相关操作是通过系统调用来实现的。
基于虚拟内存实现,不需要走系统调用。往往配合信号量
pv操作
POSIX信号量:可用于进程同步,也可用于线程同步。
POSIX互斥锁 + 条件变量:只能用于线程同步。
互斥锁(Mutex):
信号量(Semaphore):
允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define N 100
typedef int semaphore;
semaphore mutex = 1; // 互斥信号量,用于对临界区的访问进行互斥
semaphore empty = N; // 表示缓冲区中的空槽位数量
semaphore full = 0; // 表示缓冲区中的已有数据项数量
// 生产者函数
void producer() {
while (1) {
int item = produce_item(); // 生产一个数据项
down(&empty); // 等待缓冲区有空槽位可用
down(&mutex); // 进入临界区前先获取互斥锁
insert_item(item); // 将数据项插入缓冲区
up(&mutex); // 退出临界区,释放互斥锁
up(&full); // 增加已有数据项数量
}
}
// 消费者函数
void consumer() {
while (1) {
down(&full); // 等待缓冲区中有数据项可用
down(&mutex); // 进入临界区前先获取互斥锁
int item = remove_item(); // 从缓冲区中移除数据项
consume_item(item); // 消费数据项
up(&mutex); // 退出临界区,释放互斥锁
up(&empty); // 增加空槽位数量
}
}
条件变量(Condition Variable):
屏障(Barrier):
读写锁(Read-Write Lock):
自旋锁(Spin Lock):
线程执行顺序不确定
抢占式调度: 操作系统通常采用抢占式调度方式,即在任意时刻都可能中断当前执行的线程,切换到其他就绪状态的线程。这样的调度策略导致线程执行的时机和顺序不确定。
堆(Heap): 堆是由程序员分配和释放的内存区域,用于存储动态分配的数据。多个线程可以访问同一块堆内存。
全局变量和静态变量: 全局变量和静态变量存储在程序的全局数据区,它们在程序的整个生命周期内存在。多个线程可以访问和修改这些变量,因此需要考虑线程安全性。
指针和引用: 指针和引用可以指向堆上的内存或者栈上的内存。如果它们指向的是共享的堆内存,多个线程就可以访问和修改这些数据。但如果指向的是栈上的内存,那么每个线程将有自己独立的拷贝。
文件: 文件描述符是全局唯一的,因此多个线程可以共享对同一文件的访问。
栈(Stack): 每个线程都有自己的栈,用于存储局部变量和函数调用信息。栈是线程私有的,每个线程都有独立的栈空间,不同线程之间不会共享栈。
总体来说,多线程之间可以共享堆、全局变量、静态变量、指针和引用所指向的内存,以及对文件的访问。而栈是每个线程私有的,不同线程之间不会共享栈。在多线程编程中,需要注意对共享资源的访问进行同步,以避免竞态条件和数据不一致的问题。
32/64
单进程多线程:
多进程:
选择依据:
任务性质: 如果应用程序的任务可以被有效地分解为并行执行的子任务,并且需要共享大量数据,那么多线程可能是一个更好的选择。如果任务需要完全独立执行,且不需要共享大量数据,多进程可能更合适。
可维护性: 多线程相对来说更容易维护,因为它们共享相同的地址空间,数据共享相对简单。但要注意线程同步和共享数据的问题。多进程在某种程度上更易于隔离和维护。
性能需求: 多线程可以提供更好的性能,特别是在多核处理器上。但多进程在某些情况下也能提供良好的性能,例如可以在多台机器上分布执行。
虚拟存储器是一种计算机系统的存储管理技术,为每个进程提供了一个独立、连续的虚拟地址空间,这个虚拟地址空间被映射到物理内存或者辅助存储器上。通过虚拟存储器,操作系统为每个进程提供了独占系统地址空间的假象,使得每个进程感觉自己在独占地使用整个系统的地址空间。
资源隔离和地址空间:
切换成本:
并发性和资源共享:
守护进程(Daemon Process):
systemd
是一种常见的守护进程管理器。僵尸进程(Zombie Process):
wait()
或waitpid()
来获取其终止状态信息的进程。wait()
或waitpid()
来处理子进程的终止状态,释放其资源。孤儿进程(Orphan Process):
init
进程(通常是进程ID为1的进程)接管,并由init
进程成为新的父进程。这确保孤儿进程在终止时能够正常退出。在进程管理中,守护进程用于长期运行的系统服务,僵尸进程的管理涉及父进程对子进程状态的处理,孤儿进程的接管确保它们能够正常终止。这些概念帮助操作系统有效地管理和协调运行中的进程。
僵尸进程有什么危害?内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用CPU时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程。
1)子进程退出的时候,内核会向父进程发头SIGCHLD信号,如果父进程用signal(SIGCHLD,SIG_IGN)通知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。
2)父进程通过wait()/waitpid()等函数等待子进程结束,在子进程退出之前,父进程将被阻塞待。
3)如果父进程很忙,可以捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()。
在Linux中,异常和中断是两个不同的概念,但它们都涉及到处理由处理器生成的异步事件。下面是它们的区别和联系:
区别:
联系:
总体来说,异常更侧重于处理器内部的错误或不寻常条件,而中断更侧重于来自处理器外部的事件,例如硬件设备的信号。在操作系统中,它们都需要相应的处理机制,以确保系统的正确运行和响应外部事件。
多线程竞争共享资源可能导致数据错乱的原因主要有以下几点:
原子性问题: 多线程并发执行时,一个线程可能在读取共享数据的同时,另一个线程在修改这个数据,导致读取的数据不是一个完整的、一致的值。这种情况常称为原子性问题。
可见性问题: 多线程操作共享数据时,每个线程都有自己的本地缓存,当一个线程修改了共享数据,其他线程不一定能立即看到这个修改。这就是可见性问题,即一个线程对共享数据的修改对其他线程不可见。
有序性问题: 指令重排序可能导致线程执行的顺序与预期不符。编译器和处理器可能会对指令进行重排序以提高性能,但在多线程环境下,这可能导致共享数据的操作顺序不符合程序员的预期。
竞态条件: 当多个线程并发地访问共享资源,并且最终的结果取决于线程执行的时间顺序时,就可能发生竞态条件。竞态条件下,不同的线程得到的结果可能是不同的,从而导致数据错乱。
在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。
那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
如何排查死锁?
利用 top 命令查看进程的cpu占用率,然后看cpu占用率100%的进程的执行栈,然后等待的东西是同一个
排查死锁通常需要深入分析程序的执行状态和资源调度情况。以下是一些建议和步骤,供您在排查死锁时参考:
观察系统状态: 使用系统工具(如top
、htop
、ps
等)观察系统的整体状态,查看CPU和内存的使用情况。死锁可能导致程序无法正常执行,使得某个进程的CPU占用率达到100%。
查看进程栈信息: 对于CPU占用率较高的进程,使用调试工具(如GDB)查看其执行栈信息。定位到占用CPU的线程,查看其执行状态和调用链。
在GDB中可以使用 bt
命令查看线程的回溯信息。
使用死锁检测工具: 一些编程语言和工具提供了死锁检测功能,例如Java中的jstack
、C++中的线程检测工具等。这些工具可以帮助您定位死锁发生的位置。
分析日志和输出: 检查程序的日志输出,查找是否有关于死锁的异常信息。程序的输出可能包含关键的线程状态信息和死锁提示。
使用死锁预防策略: 在程序设计阶段,考虑使用死锁预防策略,例如避免使用多个锁、使用超时机制、使用事务等。
收发网络数据属于高权限操作,和硬件相关,数据到内存,先到内核空间,然后从内核拷到用户空间。
做一个内存映射,优化这个过程。
零拷贝(Zero-Copy)是一种优化技术,主要用于减少数据在系统内部的复制次数,从而提高数据传输的效率。在传统的数据传输中,数据通常需要经过多次拷贝操作,从一个缓冲区拷贝到另一个缓冲区,涉及多次内存访问和复制操作,导致性能损耗。零拷贝的目标是通过一些技术手段来最小化或消除这些数据拷贝操作。
内存映射: 使用内存映射技术,将文件映射到进程的地址空间,使得文件的内容可以直接在用户空间和内核空间之间共享,避免了数据在内核空间的中间复制。
文件传输优化: 在网络传输中,零拷贝可以通过直接操作内存映射的文件来避免数据从用户空间到内核空间的额外复制,提高文件传输效率。
多个读者可以同时进行读-->不涉及对数据的更改
写者必须互斥(只允许一个写者写,也不能读者写者同时进行)--> 不能同时更改
写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
一次只能一个线程拥有互斥锁,其他线程只有等待
互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责 线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以 ?互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线 ?程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁
互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个 ?线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足 ?时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变 ?量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互 ?斥的机制,条件变量则是同步机制。
import threading
mutex = threading.Lock()
condition = threading.Condition()
shared_resource = []
def producer():
global shared_resource
for i in range(5):
with mutex:
shared_resource.append(i)
print(f"Produced: {i}")
with condition:
condition.notify()
def consumer():
global shared_resource
for i in range(5):
with mutex: # 获取互斥锁
while not shared_resource:
with condition:
condition.wait() # 释放互斥锁并等待通知
item = shared_resource.pop()
print(f"Consumed: {item}")
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()
如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。 ?如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的 ?场景,这个时候效率比较高。
互斥锁(Mutex Lock):
pthread_mutex_lock
或 Mutex
类)来实现。当一个线程尝试获得互斥锁时,如果锁已经被其他线程占用,那么它将被阻塞,直到锁被释放。优点:
缺点:
自旋锁(Spin Lock):
优点:
缺点:
减少锁竞争:
使用线程池:
使用并发数据结构: