线程互斥同步
前言:
简单回顾一下上文所学,上文我们最重要核心的工作就是介绍了我们线程自己的LWP和tid究竟是个什么,总结一句话,就是tid是用户视角下所认为的概念,因为在Linux系统中,从来没有线程这一说法,有的就是LWP(轻量级进程)。正因如此,用户和内核所看待的线程是不一样的!所以我们就可以认为,这个tid就是作为用户所维护的线程,而据了解,这个tid其实就是在pthread库里面的一个地址,这个地址指向是真正维护线程的“线程控制块”的起始地址!
线程互斥
抢票现象:
临近新年,祝大家新年快乐,既然是新年,就拿枪火车票举个例子,下面我将创建5个线程,来一起抢火车票,这个车票我将定位全局变量,作为大家共享的资源。
#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <unistd.h>int tickets = 1000;void *Routine(void *args)
{std::string name = (const char *)args;while(true){if(tickets > 0){usleep(10000);std::cout << name << " got ticket, the rest of: " << tickets << std::endl;tickets--;}elsebreak;}return nullptr;
}int main()
{std::vector<pthread_t> threads(5);for (int i = 1; i <= 5; ++i){char *name = new char[128];snprintf(name, 128, "thread_%d", i);pthread_create(&threads[i], nullptr, Routine, (void *)name);}for (auto &t : threads){pthread_join(t, nullptr);}return 0;
}
最终5个线程会疯狂进行抢票,但是最终我们会发现票数变为了负数
不仅仅会发现票数出现负数,就连最终的打印结果也很混乱,其实我们之前测试线程所打印出来的数据多多少少都很混乱,那么接下来我们就来浅谈出现这些问题的原因。
分析抢票:
首先我们需要明确的一点,就是tickets是一个共享资源,所有线程都可以访问它。
其次就是我们所写的代码,将来都是会被翻译为汇编指令的,所以我们写的if_else还是tickets–,最终都会是一条条汇编语句,从C++的角度来看可能就一条语句,但是真实的汇编可就不只一条,而是会和寄存器挂钩出现很多条汇编语句。
if_else的内部逻辑:
tickets变量的值将从内存加载到一个寄存器中(通常是eax或r0,取决于架构)。
-
通过CMP(比较)指令与常量0进行比较。
-
根据比较结果,利用JMP类指令(如JLE、JG等)决定跳转到代码的不同部分。
-
源操作数(tickets):从内存加载到通用寄存器(如eax)。
-
目标操作数(0):直接用立即数参与比较。
MOV eax, [tickets] ; 将tickets值加载到寄存器eax
CMP eax, 0 ; 比较eax和0
JLE end_loop ; 如果tickets <= 0,跳转到结束
tickets–的内部逻辑:
对于后置减减的逻辑,可以简单理解为:我先存储减1之后的结果,但是我还是用原来的数据,等你这一行代码执行完了,我再把结果给还回来。
所以我们可以猜测汇编语句是这么写的:
mov eax, [tickets] ; 加载 tickets 的值到寄存器
mov temp, eax ; 保存旧值到 temp
sub eax, 1 ; 递减 eax
mov [tickets], eax ; 将减后的值写回 tickets
mov result, temp ; 返回旧值
总结负数原因:
如果从底层来看的话,还是能很好的说明情况。
- 假设票数tickets被抢到为1了,那么此时假设线程A进来了if语句中,它来判断票数是否大于1了,那么线程A就会把1放在if语句的寄存器中来进行判断。
- 假设线程A的时间到了,CPU会赶走线程A和它的寄存器,所以线程A就会带着它在寄存器里存放的1在别的地方呆着,同时也会记住自己刚刚所在的代码行,然后CPU立马切换线程B来执行,线程B同样走到了if语句中,把1放在了自己的寄存器中,然后一切没问题之后进行减减操作,所以票数tickets就变为了0。
- 线程B执行完后,轮到线程A了,线程A就重新回来,同样把寄存器里的值交给寄存器,然后去判断,发现寄存器里的值是1,那么就可以通过if语句。
既然通过了,那么后面线程A并不知道票数tickets发生改变了,所以线程A执行了减减操作,然后票数tickrts就从0变为了-1。
1、线程A判断 tickets == 1 时被挂起。
2、线程B修改了 tickets(从 1 减到 0)。
3、线程A恢复后基于过时的判断执行了递减操作,使得 tickets 从 0 变为 -1。
如何解决?
造成这种问题的主要原因,还是因为多个线程在互相争夺资源,所以导致每次访问资源时会出现多个线程。
因此最重要的解决方案无非就是保证任何时刻只允许一个线程进行资源访问,也就是互斥。
首先我们需要回顾一下之前在学习信号量那部分时,学到的一个专有名词——临界资源。
所谓临界资源就是需要被保护的共享资源。
而对临界资源进行保护,本质是对临界区代码进行保护,结合上面的例子来看,临界资源就是抢票的那个过程,我们需要保证一次只能有一个线程进入,这就达成了一种保护。
因此为了能达到这个保护措施,我们就需要引入pthread库提供的接口 —— 锁。
加锁保护
介绍锁
互斥锁:
-
互斥锁是一种同步机制,它允许多个线程在同一时刻最多只有一个线程访问共享资源。
-
互斥锁的设计是“锁”和“解锁”的机制,确保同一时刻只有一个线程能“持有”锁,从而保护临界区(即共享资源访问的代码块)。
pthread_mutex_t 类型:
-
在 Pthreads 中,互斥锁是通过 pthread_mutex_t 类型实现的。
-
一个互斥锁可以被初始化、上锁(加锁)、解锁以及销毁。
静态初始化
如果定义的是全局的锁,可以使用静态的方式初始化这把锁,也可以使用动态的方式初始化这把锁。使用静态的方法进行初始化可以不需要destroy
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化
如果定义的是一把局部的锁,则必须用动态的方式初始化这把锁。
#include <pthread.h>int pthread_mutex_init( /* 初始化成功时返回 0,失败时返回错误码 */pthread_mutex_t *restrict mutex, /* 需要初始化的互斥量 (锁) */const pthread_mutexattr_t *restrict attr); /* 互斥量 (锁) 的属性,一般设置为 空 即可 */
销毁锁
#include <pthread.h>int pthread_mutex_destroy( /* 销毁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex); /* 要销毁的互斥量 (锁) */
上锁
#include <pthread.h>int pthread_mutex_lock( /* 上锁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex); /* 需要上锁的互斥量 (锁) */
解锁
#include <pthread.h>int pthread_mutex_unlock( /* 解锁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex); /* 需要解锁的互斥量 (锁) */
注意事项
-
线程就是参与抢票的,所以都需要先申请锁!
-
所以线程申请锁,前提是所有线程都得看到这把锁,锁本身也是共享资源 == 加锁的过程,必须是原子的!(一会讲)
-
如果线程申请锁失败了,代表锁被其它线程拿走了,那该线程就要阻塞等待。
-
如果线程申请锁成功了,继续向后运行!
-
如果线程申请锁成功了,执行临界区的代码了,执行临界区代码期间,可以切换,但是其他线程依旧无法进入,因为锁还未释放。
-
多线程之间需要竞争锁才能访问临界区,这说明了锁本身也是一种临界资源。
既然锁也是临界资源,那么就需要被保护起来,实际上,锁只要保证申请锁的过程是原子的就能保护好自己。(一会讲)
总结:对于所有线程,要么我没有申请锁,要么我释放了锁,这样对其他线程才有意义!
何为原子性?
—— 要么不做,要么做,要做就直接做完。
举个例子,**上述抢票代码的if_else的判断就不是一个原子操作!**因为底层要不断的切换寄存器,这就导致了多个线程之间可以在此处发生切换,这也是引发竞态条件的主要原因。
改进代码:
// 定义并初始化全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *Routine(void *args)
{std::string name = (const char *)args;while(true){pthread_mutex_lock(&mutex); // 上锁// 临界资源if(tickets > 0){usleep(10000);std::cout << name << " got ticket, the rest of: " << tickets << std::endl;tickets--;pthread_mutex_unlock(&mutex); // 解锁}else{pthread_mutex_unlock(&mutex); // 解锁break;}}return nullptr;
}
最后很明显也不会再出现抢票抢到负数的情况了。
锁的底层:
大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
铺垫一下:
1、CPU的寄存器只有一套,被所有的线程共享。但是寄存器里面的数据,属于执行流的上下文,属于执行流私有的数据(即独属于线程)。
2、CPU在执行代码的时候,一定会有对应的执行载体,即线程&&进程
3、数据在内存中,被所有线程是共享的。
所以把数据从内存移动到CPU寄存器中,本质是把数据从共享,变成线程私有
那么我们再从底层原理出发来看:
因为我们定义锁肯定是在内存空间上定义的,所以我们不妨简单一点,我们认为在内存上存在一块空间记录锁的状态
根据提供出来的汇编代码,第一步就是将%al寄存器里面初始化个0。然后再与物理内存中的锁进行交换,交换完之后%al就变为了1,那么这就代表着上锁成功了。
因为交换的过程是原子的,这就可以避免出现线程切换,从而造成复杂的场面。
就算在%al寄存器与内存交换完后发生线程交换,那该线程就会带走%al寄存器里的数据在旁边等着,因为该数据是该线程的!
切换完后来的那个新线程,同样也会先把%al寄存器清0,但当他与内存中的锁发生交换后,仍然还是0,因为锁此时还没被释放!!!
那么新线程就会被判断发现<=0就会在阻塞等待,直到切换到上一个线程,然后释放锁了才会再去执行新线程!!!
而释放锁其实也是一种交换,那么对于锁的底层实现,我们也看到其特有的原子性,就能放心的使用锁了,因为锁也是一种被保护起来的临界资源。
线程同步
互斥 && 同步
因为我们两章的内容分别是线程互斥与线程同步,但其实我们应该真正介绍下互斥与同步的区别与关系,为什么我放在这里来讲而不是开头呢?
就是因为互斥比较好理解,在学习完线程互斥才能更好的理解线程同步。
「互斥」是为了解决资源分配的问题,确保某一时刻只允许一个线程进入执行
「同步」是为了解决执行顺序的问题,在互斥的基础上协调线程的执行顺序
- 互斥解决的是资源竞争问题(“不能同时做”)。
- 同步解决的是执行顺序问题(“必须等待某个条件”)。
假设有一天,有三个小伙子想去网吧上网,但是网吧目前只有一台电脑,互斥锁的出现,就是能保证每次都只会有一个人进去网吧上网
但是这会造成一种情况,一个人可以不断的进网吧和出网吧,而其他两个人就只能在旁边看着。这也是线程互斥带了的一个问题
其实最好的解决方法就是让三个小伙子排队等待,即:
这也是线程同步所解决的执行顺序的问题。
条件变量
在理解线程的「互斥」与「同步」之间的关系之后,我们就自然而然的需要来想办法解决「同步」所需要的执行顺序的问题了。
现在我们又需要换一种故事,来讲解条件变量:
现在我们假设网吧的电脑出现了问题,而这时候有一个人一直在疯狂的抢锁,然后进去网吧发现电脑故障用不了,就出来,但是他总觉得自己能修好,所以一直在进进出出。
可是,网吧老板知道了这件事情后,带着新电脑来以旧换新,只是网吧老板一直都抢不过这个小伙子,老板一直拿不到锁,那么老板就一直进不去,进不去就无法换新电脑,那这个网吧迟早会被这个小伙子干倒闭!!!
所以这个时候老板就会先给网吧贴一个告示!代表现在出问题了,那么其他用户看到告示后,就会跑到别的地方集合,等待老板撕下告示,这样就代表可以进入玩游戏了!这样老板就可以无限不用担心竞争不到锁了!!!
简单来说,条件变量就相当于是一个告示,为了方便理解,所以举了这么个例子,但其实每个用户都应当先解锁然后发现电脑坏了,然后再跑出来在等待地点(这个等待地点就是条件变量)进行等待,直到老板过来说“可以玩了!”,这样其他用户才会再次竞争锁然后访问资源。
接口
-
初始化条件变量
同初始化互斥锁一样,初始化条件变量也有静态初始化和动态初始化两种方式。
-
静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
-
动态分配
- 全局的条件变量可以使用 静态 / 动态 的方式初始化。
- 局部的条件变量必须使用 动态 的方式初始化。
#include <pthread.h>int pthread_cond_init(pthread_cond_t *restrict cond, /* 需要初始化的条件变量 */const pthread_condattr_t *restrict attr); /* 条件变量的属性,一般都设置为空 */
-
-
销毁条件变量
局部的条件变量必须销毁,全局的则不用
#include <pthread.h>int pthread_cond_destroy(pthread_cond_t *cond); // 销毁指定的 cond 条件变量
-
让线程去条件变量下等待
#include <pthread.h>int pthread_cond_wait( pthread_cond_t *restrict cond, /* 条件变量,指定线程需要去 cond 条件变量处等待 */pthread_mutex_t *restrict mutex); /* 互斥锁,需要释放当前线程所持有的互斥锁 */
哪个线程调用的该函数,就让哪个线程去指定的条件变量处等待,还要将这个线程持有的锁释放,让其他线程能够争夺这把锁。
线程在哪调用的这个函数,被唤醒之后就要从这个地方继续向下执行后续代码。
当线程被唤醒之后,线程是在临界区被唤醒的,线程要重新参与对 mutex 锁的竞争,线程被唤醒 + 重新持有锁两者加起来线程才真正被唤醒。 -
唤醒在条件变量处等待的线程
唤醒条件变量的方式有 2 种,分别是唤醒全部线程以及唤醒首个线程。
#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒在 cond 条件变量队列处等待的 所有 线程 int pthread_cond_signal(pthread_cond_t *cond); // 唤醒在 cond 条件变量队列处等待的 首个 线程
该函数说是唤醒了线程,其实只是一种伪唤醒,只有当线程被伪唤醒 + 重新持有锁才是真唤醒.
只有被真唤醒的线程才会继续去执行后续代码.
代码测试
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;void *Routine(void *args)
{std::string name = (const char *)args;while (true){pthread_mutex_lock(&gmutex);pthread_cond_wait(&gcond, &gmutex); // 等待被唤醒usleep(10000);std::cout << "Hi I am " << name << std::endl;pthread_mutex_unlock(&gmutex);sleep(1);}return nullptr;
}int main()
{std::vector<pthread_t> threads(5);// 创建5个线程for (int i = 0; i < 5; ++i){char *buffer = new char[1024];snprintf(buffer, 1024, "thread-%d", i + 1);std::cout << "create " << buffer << " but not to do sometings" << std::endl;pthread_create(&threads[i], nullptr, Routine, (void *)buffer);usleep(10000);}sleep(3);while (true){// 唤醒5个线程,一个一个的唤醒pthread_cond_signal(&gcond);std::cout << "唤醒一个线程" << std::endl;sleep(2);}// 等待回收5个线程for (const auto &t : threads)pthread_join(t, nullptr);return 0;
}
总结:
本文我们打通了线程之间的互斥与同步的关系,那我们的多线程部分也马上就要结束了,我们的Linux操作系统也就到达了尾声阶段,接下来我会给大家介绍生产消费者模型并动手实现,在实现完后就会引入信号量的概念,随后就是手搓一个线程池,紧接着我们就会开始我们的Liunx网络篇。
相关文章:

线程互斥同步
前言: 简单回顾一下上文所学,上文我们最重要核心的工作就是介绍了我们线程自己的LWP和tid究竟是个什么,总结一句话,就是tid是用户视角下所认为的概念,因为在Linux系统中,从来没有线程这一说法,…...
DeepSeek R1 AI 论文翻译
摘要 原文地址: DeepSeek R1 AI 论文翻译 我们介绍了我们的第一代推理模型,DeepSeek-R1-Zero 和 DeepSeek-R1。 DeepSeek-R1-Zero 是一个通过大规模强化学习(RL)训练的模型,且在此过程中未使用监督微调(…...

如何计算态势感知率?
态势感知率(Situational Awareness Rate)的计算通常需要结合具体应用场景和定义目标,通常涉及对感知、理解、预测三个层次的量化分析。不同领域(如网络安全、军事、工业控制等)可能有不同的量化方式。通用思路和常见方…...
二、CSS笔记
(一)css概述 1、定义 CSS是Cascading Style Sheets的简称,中文称为层叠样式表,用来控制网页数据的表现,可以使网页的表现与数据内容分离。 2、要点 怎么找到标签怎么操作标签对象(element) 3、css的四种引入方式 3.1 行内式 在标签的style属性中设定CSS样式。这种方…...

Alibaba开发规范_异常日志之日志规约:最佳实践与常见陷阱
文章目录 引言1. 使用SLF4J日志门面规则解释代码示例正例反例 2. 日志文件的保存时间规则解释 3. 日志文件的命名规范规则解释代码示例正例反例 4. 使用占位符进行日志拼接规则解释代码示例正例反例 5. 日志级别的开关判断规则解释代码示例正例反例 6. 避免重复打印日志规则解释…...
使用istio实现权重路由
istio概述 **概述:**Istio 是一个开源的 服务网格(Service Mesh)解决方案,主要用于管理、保护和监控微服务架构中的服务通信。它为微服务提供了基础设施层的控制功能,不需要更改应用程序的代码,从而解决服…...
M. Triangle Construction
题目链接:Problem - 1906M - Codeforces 题目大意:给一个 n 边形, 每一个边上有a[ i ] 个点, 在此多边形上求可以连的三角形有多少个, 每个点只能用一次。 输入: 第一行是一个整数 N ( 3 ≤ N ≤ 200000…...

每天学点小知识之设计模式的艺术-策略模式
行为型模式的名称、定义、学习难度和使用频率如下表所示: 1.如何理解模板方法模式 模板方法模式是结构最简单的行为型设计模式,在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基…...
机试题——到邻国目标城市的最短距离
题目描述 A国与B国是相邻的两个国家,每个国家都有很多城市。国家内部有很多连接城市的公路,国家之间也有很多跨国公路,连接两个国家的边界城市。两个国家一共有N个城市,编号从1到N,一共有M条公路,包括国内…...

Python + Tkinter + pyttsx3实现的桌面版英语学习工具
Python Tkinter pyttsx3实现的桌面版英语学习工具 在多行文本框输入英文句子,双击其中的英文单词,给出英文读音和中文含义和音标。 本程序查询本地词典数据。通过菜单栏"文件"->"打开词典编辑器"进入编辑界面。 词典数据存储…...
【Vite + Vue + Ts 项目三个 tsconfig 文件】
Vite Vue Ts 项目三个 tsconfig 文件 为什么 Vite Vue Ts 项目会有三个 tsconfig 文件?首先我们先了解什么是 tsconfig.json ? 为什么 Vite Vue Ts 项目会有三个 tsconfig 文件? 在使用 Vite 创建 vue-ts 模板的项目时,会发现除了 ts…...
AI时代IT行业职业方向规划大纲
一、引言 AI时代的颠覆性影响 ChatGPT、Midjourney等生成式AI对传统工作模式的冲击 案例:AI编程助手(GitHub Copilot)改变开发者工作流程 核心问题:IT从业者如何避免被AI替代,并找到新机遇? 二、AI时代…...
Mac M1 Comfyui 使用MMAudio遇到的问题解决?
问题1: AssertionError: Torch not compiled with CUDA enabled? 解决办法:修改代码以 CPU 运行 第一步:找到 /ComfyUI/custom_nodes/ComfyUI-MMAudio/mmaudio/ext/autoencoder/vae.py文件中的下面这两行代码 self.data_mean nn.Buffer(t…...

大语言模型深度研究功能:人类认知与创新的新范式
在人工智能迅猛发展的今天,大语言模型(LLM)的深度研究功能正在成为重塑人类认知方式的关键力量。这一突破性技术不仅带来了工具层面的革新,更深刻地触及了人类认知能力的本质。本文将从认知科学的角度出发,探讨LLM如何…...
[SAP ABAP] 性能优化
1.数据库编程OPEN SQL方面优化 1.避免使用SELECT *,只查询需要的字段即可 尽量使用SELECT f1 f2 ... (具体字段) 来代替 SELECT * 写法 2. 如果确定只查询一条数据时,使用 SELECT SINGLE... 或者是 SELECT ...UP TO 1 ROWS ... 使用语法 UP TO n ROWS 来…...
并行计算、分布式计算与云计算:概念剖析与对比研究(表格对比)
什么是并行计算?什么是分布计算?什么是云计算?我们如何更好理解这3个概念,我们采用概念之间的区别和联系的方式来理解,做到切实理解,深刻体会。 1、并行计算与分布式计算 并行计算、分布式计算都属于高性…...
ASP.NET Core Filter
目录 什么是Filter? Exception Filter 实现 注意 ActionFilter 注意 案例:自动启用事务的筛选器 事务的使用 TransactionScopeFilter的使用 什么是Filter? 切面编程机制,在ASP.NET Core特定的位置执行我们自定义的代码。…...
doris:删除操作概述
在 Apache Doris 中,删除操作(Delete)是一项关键功能,用于管理和清理数据,以满足用户在大规模数据分析场景中的灵活性需求。 Doris 提供了丰富多样的删除功能支持,包括:DELETE 语句、删除标记&…...

【思维导图】redis
学习计划:将目前已经学的知识点串成一个思维导图。在往后的学习过程中,不断往思维导图里补充,形成自己整个知识体系。对于思维导图里的每个技术知识,自己用简洁的话概括出来, 训练自己的表达能力。...
申博经验贴
1. 所谓申博,最重要的就是定制的海投 分成两个部分 1. 定制 要根据每个教授去写不同的,一定不要泛泛的去写,一定要非常非常的具体,要引起教授的兴趣。每个教授每天都会收到几十封邮件,所以要足够的引起教授的注意&a…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...
如何为服务器生成TLS证书
TLS(Transport Layer Security)证书是确保网络通信安全的重要手段,它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书,可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...