您的位置  > 互联网

游戏中的线程和用户态和内核操作系统的区别

在谈论同步和互斥锁之前,您需要熟悉一些与计算机进程和线程相关的基本计算机概念

每个应用程序(例如游戏)都作为进程执行。但是,游戏需要图形渲染,需要网络,需要响应用户的动作,而这些行为不能相互阻挡,必须同时完成,所以被设计成线程化。现代CPU以多线程为主,下面内容以多线程为主,我们多说的就是线程的调度。

计算机的资源

我们经常谈论操作系统分配资源的需要,最重要的三个资源是:计算资源(CPU)、内存资源和文件资源。计算机的CPU是有限的,往往有多个任务需要执行,CPU不能同时执行所有任务,而只能一个一个地执行,这相当于将计算资源分配给任务(线程),获得计算资源的任务就可以执行。文件资源(如存储在计算机上的文件)是文件资源。

归根结底,无论是计算机的组成还是

操作系统,它面临着资源有限,而且任务(用户)有很多使用资源,如何合理安排资源,以便任务能够更好的执行,无论是线程和进程的设计,还是计算机的三级存储结构,都是在权衡利弊和成本的基础上设计的, 以便更好地规划资源。

操作系统的内核

用户模式和内核模式

内核线程和用户线程

在设计线程时,它们被称为轻量级进程,因为只分配计算资源 (CPU)。它的分配方式是操作系统调度线程。操作系统创建进程后,将进程的入口程序分配给主线程执行,这样看起来操作系统在调度进程,但实际上是调度进程中的一个线程。

这种直接由操作系统调度的线程,我们也变成了内核级的线程。此外,在某些编程语言或应用程序中,用户(程序员)也自己实现线程。它相当于操作系统调度主线程,主线程的程序用一种算法实现子线程,我们称之为用户级线程。Linux API 是用户级线程,API 是内核级线程。

哪里的进程开销大于线程的开销?

以 Linux 为例:在 Linux 中创建一个进程,自然会创建一个线程,这个线程就是主线程。创建一个进程,需要为该进程分配一个完整的内存空间,并且有很多初始化操作,例如对内存进行分段(堆栈、正文区域等)。创建线程要简单得多,因为您可以确定 PC 指针和寄存器的值,并为线程分配一个堆栈来执行程序,从而允许在同一进程的多个线程之间重用堆栈。因此,创建进程比创建线程慢,并且进程的内存开销更大。线程的同步和互斥 什么是

同步,什么是互斥?同步:线程同步

并不意味着线程同时运行,而是线程按照一定的顺序执行,以便可以同步最终数据。想想这样的场景,教室里有很多学生,每个学生都在说闲话,有很多困惑。如果这些学生一个接一个地说话,或者作为一个小组说话,就会显得井然有序,每个人都能听到他们在说什么。同样,在多线程的情况下,如果不加以控制,多个线程一起添加或修改数据,数据会异常混合,就像咿咿呀呀的声音一样。编写程序的目的无非是处理数据,处理数据的输入和输出,如果最终输出不在你的控制之下,这个程序有什么用呢?

互斥:有一些资源,例如打印机。一次只能由一个线程访问,不允许多个线程同时访问它,这种资源称为关键资源。因此,当一个线程访问此资源时,其他线程无法访问它,这称为互斥锁。[关键区域:访问关键资源的代码称为关键区域。线程只有在首先进入关键区域时才能访问关键资源。】

一般来说,为了实现同步,首先要实现互斥性。例如,在上面同步引用的场景中,如果我们想让学生一个接一个地说话,我们必须首先确保一个学生说话时,其他学生不说话,这是一种相互排斥。

操作系统如何实现同步使用互

斥锁:使用互斥机制,只有具有互斥锁的线程才能访问公共资源。因为只有一个互斥锁,所以保证了公共资源不会同时被多个线程访问。例如,Java 中的关键字和各种锁都是这种机制。

使用 ():它允许多个线程同时访问同一资源,但需要控制同时访问该资源的最大线程数

什么是信号量?请看以下示例

考虑有两个函数,向上和向下。 向上增加 1 的锁,向下使锁减少 1。当 lock 为 0 时,如果它仍然处于向下功能,它将旋转。

其中 cas 是指 CPU 中的原子操作,其伪代码形式可以表示为:cas(&, , )。例如,如果我们想做 A++;假设 a 的初始值为 0,我们可以这样写 cas(&a, 0, 1),这是什么意思?如果 a 的值为 0,我会将 a 的值更新为 1,然后写回去,否则 a 的值不会更新。为什么运行 A++ 如此麻烦?为了防止其他线程在多线程并发的情况下更改 a 的值

up(&lock){
  while(!cas(&lock, lock, lock+1)) { }
}
down(&lock){
  while(!cas(&lock, lock, lock - 1) || lock == 0){}
}

在执行以下程序时考虑多线程

int lock = 2;
down(&lock);
// 临界区
up(&lock);

如果只有一个线程在临界区域,则锁等于 1,第二个线程可以进入。如果两个线程处于临界区域,则第三个线程在尝试关闭时将陷入自旋锁。当然,我们也可以通过其他方式替换自旋锁,例如休眠线程。

当 lock 的初始值为 1 时,模型实现互斥锁。如果锁大于 1,则允许多个线程同时进入临界区域。这种方法称为信号量()。

使用事件:例如 Wait/:通过通知操作使多个线程保持同步。操作系统实现互斥锁的方式:

单旗法:设置一个常用的整数变量:turn。例如,turn = 1 表示运行 1 的进程进入临界区域获取临界资源,不允许其他线程进入。

先双旗方法检查:每个线程在进入关键区域访问关键资源之前,先检查关键资源是否被访问,如果是,线程需要等待。如果未访问关键资源,则线程将进入关键区域以访问关键资源。我们可以设置一个 flags[], flag[i] = true 数组,这意味着 i 正在访问关键资源,而 flag[i] = false 表示线程没有访问关键资源。当线程 i 检测到没有其他线程正在访问关键资源时,它会进入关键区域并将 flag[i] 设置为 true。

可能的问题:两个线程同时进入临界区域。由于线程首先检查是否有其他线程访问关键资源,然后在没有其他线程访问关键资源时进入关键区域以访问关键资源,因此这两个步骤之间有一定的时间间隔。考虑两个线程,T1 和 T2。T1 首先检查是否有线程没有访问关键资源,检查结果显示没有其他线程正在访问关键资源,因此准备进入关键区域访问关键资源,T1 即将进入但尚未进入。T2 也开始检查是否有线程访问关键资源,检查结果发现没有线程访问关键资源,于是 T2 也开始准备进入关键区域访问关键资源。好吧,T1 和 T2 都检测到没有其他线程正在访问关键资源,并且都进入了关键区域,这显然违反了互斥锁。双标志

后检查:双标志检查优先方法在设置自己的标志之前检测关键资源的状态。对于准备访问关键资源的线程,情况正好相反,它首先将自己的标志设置为 true,然后检查其他线程的标志,如果它检测到另一个线程的标志为 true,则线程将等待,否则进入关键区域。

可能的问题:导致饥饿状态,没有一个线程可以进入临界区域。考虑两个线程,T1 和 T2。T1 准备访问关键资源,并将其标志设置为 true。在 T1 开始检测其他线程的状态之前,T2 即将访问关键资源,因此 T2 迅速将其标志设置为 true。那么,当这两个线程检查其他线程是否访问关键资源时,它们可以检查另一个线程的标志位是否为 true,并且两个线程处于死锁状态,并且它们都无法访问关键资源

('s) 算法:除了使用 flag[] 数组来指示线程是正在访问关键资源还是即将访问关键资源外,还设置了一个公共变量 turn。

伪代码如下:

考虑两个线程,I 和 J。

首先考虑 3 的情况。首先是 i 线程已准备好访问关键资源,因此它将其标志设置为 true。然后,i 线程假设 j 线程此刻正在访问关键区域,因此它设置为 j。此时,J 线程也即将访问关键资源,因此它将其标志设置为 true,并且 J 线程也做了一个假设,假设此刻是访问关键区域的 i 线程,因此它将 true 的值设置为 i。最终,true 的值变为 i。紧接着,i 线程开始判断 while 循环中的条件,flag[j] == true 为 true,但是 turn 的值已经被 j 线程设置为 i,所以 turn == j 不为真,所以循环条件不满足,i 线程不执行循环,i 线程成功进入临界区域, 避免了双旗法引起的饥饿问题。

中断屏蔽方法

当线程获取CPU的计算资源时,除非发生中断,否则不会进行线程切换。我们的计算机可以同时运行多个程序,并不是说这些程序同时获取CPU的计算资源,而是CPU不断切换线程或进程,比如先执行线程A,执行一段时间,然后向线程A发起中断请求,线程A被打断, 然后交出运行权限,然后进行线程切换。然后 CPU 抢先执行 B 线程。线程 B 执行一段时间并切换到其他线程。CPU速度非常快,可以在短时间内执行大量代码,线程切换速度非常快,因此产生了程序并行执行的现象。

当一个线程准备好访问关键资源时,它首先屏蔽了中断,这意味着线程不会被中断,这意味着该线程是 CPU 独占的,其他线程不会被执行。

硬件指令方法

在计算机中,有些指令是原子的,这意味着执行过程不会因为线程切换而中断。例如,TAS(测试和设置)、CAS(和交换)。回想一下我们什么时候使用软件来实现互斥方法,例如双标志检查,无论是先是还是后。存在被打断的问题。例如,T1 线程首先检测到并发现没有其他线程在使用关键资源,但就在 T1 即将进入且尚未进入时,T2 线程来捣乱并偷偷更改了一些标志。由于 T1 已经过测试,它认为一切正常并继续执行,但部分内容已被修改。如果我们设置 T1 来检测其他线程是否在使用中断,如果没有,则将我们的值设置为 true,否则,继续轮询这些操作并将它们封装到一个原子进程中,即当 T1 执行这些代码时,它是一次性执行的,不允许其他线程干预制造麻烦, 并且不会同时进入临界区或挨饿。

参考

《御道操作系统考研复习指南》。