【Linux】线程同步与互斥

news/2024/9/28 21:28:41/

一、线程间互斥

         1 .进程线程间的互斥相关概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
临界资源和临界区
  • 多个线程之间能够看到的同一份资源被称作临界资源,而专门用来访问临界资源的代码段被称为临界区。保护临界资源本质就是想办法把访问临界资源的代码保护起来。
  • 因为多线程的大部分资源都是共享的,因此线程之间进行通信不需再去创建第三方资源。
互斥和原子性
  • 如果多个线程同时对临界资源进行操作,就可能导致数据不一致的问题,这些问题可以通过互斥解决。互斥能够保证在任何时刻都只能有一个线程(执行流)进入临界区对临界资源进行访问。
  • 原子性指的是不会被任何调度机制打断的操作,对于原子性来说,只有 完成 / 未完成 两种状态,要么不做,要么就做完,没有中间状态。要么执行要么就不执行

为什么需要互斥,看下面一段代码:

模拟抢票软件系统:

int tickets = 1000; void* get_ticket(void* args)
{std::string name = static_cast<const char*>(args);while (true){if (tickets > 0){usleep(1000);	// 模拟抢票花费的时间std::cout<<name<<" get a tickets :"<<tickets<<std::endl;--tickets;}else{break;}}    return nullptr;
}
int NUM=4;
int main()
{pthread_t  threads[NUM];for(int i=0;i<NUM;i++){char* name=new char[128];snprintf(name,128,"thread - %d",i+1);pthread_create(threads+i,nullptr,get_ticket,(void*)name);}for(auto tid:threads){pthread_join(tid,nullptr);}return 0;
}

执行结果:

可以看见,总共10000张票,最后却抢到了负号票。

问题:为什么票抢到了负数?

因为线程是OS调度的基本单位。

  • if 语句在判断条件为真 (即 tickets > 0)时,此时代码可以并发的切换到其他线程执行该任务。
  • usleep 用于模拟抢票的过程,在这个过程中,可能有很多个线程会进入该代码段。
  • --tickets和 if (tickets > 0)本身不是一个原子的操作。

假设此时 tickets 的值为 1,线程 A 刚把 g_tickets 从内存拷贝到寄存器中,正准备判断时,线程 A 时间片到了。必须得切换到其他线程,此时线程 A 就只好带着自己得寄存器中存着的 tickets 离开,此时线程 A 的寄存器中存着的 tickets 的值是 1。

 
切换到线程 B 之后,线程 B 也要把 g_tickets 从内存拷贝到寄存器中,线程 B 通过 if 判断出 tickets 的值 > 0,然后线程 B 就会将 tickets 在内存中的的值减 1,此时 tickets 的值已经变成 0 了。

 
等到线程 A 切换回来后,线程 A 的寄存器中 g_tickets 的值还是 1,线程 A 以为 tickets 的值依旧是 1 ,能够通过 if 判断,但不知道线程 B 已经将 tickets 减到 0 了,线程 A 再对 tickets 在内存中的值减 1 时就会将 g_tickets 减到 - 1 去。
然而对 g_tickets 执行判断和自减操作都需要再次将 g_tickets 从内存读取到寄存器中。
在多线程访问时,这种将票数干到负数的情况被称为数据不一致

问题:让 g_tickets 进行自减为什么不是原子的操作?

OS在对变量进行 -- 操作时,从汇编层面上看,其实有 3 条指令:

  1. load :将共享变量ticket从内存加载到寄存器中
  2. update : 更新寄存器里面的值,执行-1操作
  3. store :将新值,从寄存器写回共享变量ticket的内存地址
解决以上问题,需要做到三点:
  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临 界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。 ​​​​​​​

         2 .互斥量

为了搞定这个问题,就需要一把锁,在有线程访问临界区时,用这把锁将临界区锁起来,访问完之后再解锁,这把锁被称为互斥量 mutex

         3 . 互斥量的接口

初始化互斥量
初始化互斥量有两种方法:
  • 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 方法2,动态分配:
#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);	/* 要销毁的互斥量  */
销毁互斥量需要注意:
  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁和解锁
#include <pthread.h>int pthread_mutex_lock(				/* 上锁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex);	/* 需要上锁的互斥量  */

注意:

  1. 加锁的范围,颗粒的一定要小。尽可能的给少的代码块加锁,如果给一大段的代码加锁,线程之间就变成串行执行了,多线程就没意义了。
  2. 一般来说,都是给临界区加锁,只需要保护会访问到临界资源的那部分代码即可。
  3. 谁加的锁,就让谁解锁,最好不要出现线程 A 加锁却让线程 B 解锁的情况。
为互斥量上锁会遇到的情况情况
  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
互斥量的解锁
#include <pthread.h>int pthread_mutex_unlock(			/* 解锁成功时返回 0,失败时返回错误码 */pthread_mutex_t *mutex);	/* 需要解锁的互斥量 (锁) */

改进上面抢票系统:我们加一把全局互斥锁进去

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//全局互斥量
int tickets = 1000; void* get_ticket(void* args)
{std::string name = static_cast<const char*>(args);while (true){pthread_mutex_lock(&mutex);//锁上if (tickets > 0){usleep(1000);	// 模拟抢票花费的时间std::cout<<name<<" get a tickets :"<<tickets<<std::endl;--tickets;pthread_mutex_unlock(&mutex);//解锁}else{pthread_mutex_unlock(&mutex);//解锁break;}}    return nullptr;
}int NUM=4;
int main()
{pthread_t  threads[NUM];for(int i=0;i<NUM;i++){char* name=new char[128];snprintf(name,128,"thread - %d",i+1);pthread_create(threads+i,nullptr,get_ticket,(void*)name);}for(auto tid:threads){pthread_join(tid,nullptr);}return 0;
}

执行结果:

可以发现,抢票不会抢到负号票了。但是出现了新的问题,就是一个线程抢非常多票,这显然也是不合理的。

         4 . 互斥量的实现原理

临界区中的线程也能被线程切换走

即使是在临界区中的线程,也是能够进行线程切换的。但即使该线程被切换走了,其他线程也无法进入临界区。因为该线程被切走时,它是锁着走的,锁没有没解开就意味着其他线程是无法申请到锁,从而进入临界区的。
其他线程想要进入临界区,只能等待正在访问临界区的线程将锁解开,然后其他线程去竞争这把锁,争到锁的线程才能访问临界区。

 锁也是需要被保护的共享资源

多线程之间需要竞争锁才能访问临界区,这说明了锁本身也是一种临界资源。

既然锁也是临界资源,那么就需要被保护起来,实际上,锁只要保证申请锁的过程是原子的就能保护好自己。

 如何保证申请锁的过程是原子的

为了实现互斥锁的操作,大多数的体系结构都提供了 swap 或 exchange 指令,其作用是将寄存器和内存单元的数据互换。
由于从汇编层面上看,只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。

查看一下 lock 和 unlock 的伪代码: 

线程 lock 争锁的本质

内存中设 mutex 的初始值为 1,al 是线程独立拥有的一组寄存器中的的一个,每个线程都有这样的一组寄存器(寄存器硬件只有一组,这里指的是数据),当线程申请锁时,需要执行以下步骤:

  1. 使用 movb 指令将 al 寄存器中的值清零,多个线程可同时执行该动作。
  2. 使用 xchgb 指令将 al 寄存器和 mutex 中的值互换。
  3. 判断 al 寄存器中的值是否 > 0,若 > 0 则申请锁成功,此时就能进入临界区访问临界资源了。申请锁失败的线程会被挂起等待,直到锁被释放后再次去竞争申请锁。

因此,线程之间争锁的本质就是在争夺 mutex 中的那个 1,看哪个线程自己的寄存器能抢到这个 1,就说明哪个线程争到了这把锁
线程只要想争锁,就必须从第 1 步开始。即使前面的线程中途被切走,这个线程也是拿着这个 1 走的,mutex 中的值还是 0,其余线程没法争锁。

结论:把数据从内存移动到CPU寄存器中,本质是把数据从共享,变成线程私有。

线程 unlock 解锁的本质

  1. 使用 movb 指令将内存中的 mutex 的值重置回 1,让下一个申请锁的线程在执行 xchgb 交换指令后能够得到这个 1。
  2. 唤醒所有因为申请锁失败而被挂起等待 mutex 的线程,让它们继续去争锁。

二、可重入VS线程安全

         1 .概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。

         2 .常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

         3 . 常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性​​​​​​​

         4 . 常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

         5 .常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

         6 .可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

         7 .可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生
  • 死锁,因此是不可重入的。

三、常见锁概念

         1 .死锁

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态

当两个线程都需要同时持有两把锁才能开始干活,但是此时各自持有一把锁,都在期望得到对方的锁,且都不会释放自己持有的锁,那么这两个线程就陷入了永久等待对方的持有的资源使得任务都无法推进的一种死锁状态。

当然死锁并不是多线程才能产生,当单线程已经获得锁并且没有释放的时候再次去申请该锁,也会形成等待锁的释放而阻塞的状态。进而形成死锁。

         2 .死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

         3 . 避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配 ​​​​​​​

四、线程同步

         1 .同步概念与竞态条件

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

问题:什么是饥饿问题?如何解决?

在某些操作系统中,线程之间可能不是等概率的竞争到锁,而可能是某些线程本身的争锁能力特别厉害,导致任务全让这个线程干完了,其他线程竞争不到锁就会产生饥饿问题。
但如果线程在申请锁之后什么也不做,就一直在申请锁和释放锁,纯纯的浪费资源。

解决:

线程同步能够有效解决饥饿问题,而条件变量则是实现线程同步的一种机制。同步规定,上次持有锁的线程短期内不能再持有锁,就能很好的解决饥饿问题。同步就需要用到条件变量。

条件变量
条件变量(Condition Variable)是一种用于线程同步的机制,通常与互斥锁(Mutex)一起使用。条件变量提供了一种线程间的通信机制,允许一个线程等待另一个线程满足某个条件后再继续执行。
  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

故事说明

现在小明在一张桌子上放一个苹果,而旁边有一群蒙着眼睛的人,因为他们的眼睛被蒙着,他们如果想拿到这个苹果,就会时不时来桌子前摸一摸看看桌子是否有苹果,并且谁来桌子前摸苹果是无序的,这时的场面就很混乱,小明一看不行,于是小明就桌子上放了个铃铛,并且组织需要苹果的人排好队,有苹果小明就会摇响铃铛,排在第一个的人就拿走苹果,然后如果还想要苹果就到队尾排队等待。此时混乱的场面就显得井然有序了。在本故事中,小明就是操作系统,苹果就是临界资源,一群蒙着眼睛都人就是多线程,铃铛就是条件变量,排队就是实现同步,摇响铃铛就是唤醒线程

为什么需要使用条件变量

使用条件变量主要是因为它们提供了在多线程编程中一种有效的同步机制。当多个线程需要等待某个特定条件成立才能继续执行时,条件变量就显得尤为重要。通过条件变量,线程可以安全地进入等待状态,直到被其他线程显式地唤醒或满足等待的条件。这有助于避免线程的无谓轮询或忙等待,提高了系统的响应能力和效率。

注意:在使用条件变量时,必须确保与互斥锁一起使用,以避免竞态条件的发生

 条件变量就是一种数据类型

struct cond
{int flag;	// 1. 判断条件变量是否就绪tcb_queue;	// 2. 维护一个线程队列,线程就是在这个队列里排队
};

         2 .条件变量函数

pthread_cond_t
  • pthread_cond_t 是 POSIX 线程库(Pthreads)中用于表示条件变量的数据类型。
  • 可以使用 pthread_cond_t类型来定义一个条件变量。

  • 所有的条件变量函数的返回值都是调用函数成功时返回 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 锁的竞争,线程被唤醒 + 重新持有锁两者加起来线程才真正被唤醒。

pthread_cond_wait 的行为如下:

  1. 解锁互斥锁:调用 pthread_cond_wait 的线程首先会释放(解锁)它当前持有的互斥锁。这一步是必要的,因为条件变量通常与互斥锁一起使用,以确保对共享数据的访问是同步的。解锁互斥锁允许其他线程获取该锁,从而可以安全地修改共享数据。
  2. 加入等待队列:在解锁互斥锁之后,调用 pthread_cond_wait 的线程会将自己添加到与该条件变量相关联的等待队列中。此时,线程进入阻塞状态,等待被唤醒。
  3. 阻塞并等待信号:线程在等待队列中保持阻塞状态,直到它收到一个针对该条件变量的信号(通过 pthread_cond_signal 或 pthread_cond_broadcast 发出)。需要注意的是,仅仅因为线程在等待队列中并不意味着它会立即收到信号;它必须等待直到有其他线程显式地发出信号
  4. 重新获取互斥锁:当线程收到信号并准备从 pthread_cond_wait 返回时,它首先会尝试重新获取之前释放的互斥锁。如果此时锁被其他线程持有,那么该线程会阻塞在互斥锁的等待队列中,直到获得锁为止。这一步确保了线程在继续执行之前能够重新获得对共享数据的独占访问权。
  5. 检查条件:一旦线程成功获取到互斥锁,它会再次检查导致它调用 pthread_cond_wait 的条件是否现在满足。虽然通常认为在收到信号时条件已经满足,但这是一个编程错误的常见来源。正确的做法是在每次从 pthread_cond_wait 返回后都重新检查条件,因为可能有多个线程在等待相同的条件,或者条件可能在信号发出和线程被唤醒之间发生变化。
  6. 返回并继续执行:如果条件满足,线程会从 pthread_cond_wait 返回,并继续执行后续的代码。如果条件仍然不满足,线程可以选择再次调用 pthread_cond_wait 进入等待状态,或者执行其他操作。

唤醒在条件变量处等待的线程

  • 唤醒条件变量的方式有 2 种,分别是唤醒全部线程以及唤醒首个线程
#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond);	// 唤醒在 cond 条件变量队列处等待的 所有 线程
int pthread_cond_signal(pthread_cond_t *cond);		// 唤醒在 cond 条件变量队列处等待的 首个 线程
  • 虽然该函数说是唤醒了线程,但是其实只是一种伪唤醒,只有当线程被唤醒 + 重新持有锁才是真唤醒。
  • 只有被真唤醒的线程才会继续去执行后续代码。

问题:在调用pthread_cond_wait前如果已经提前收到唤醒通知会怎么样?

如果在调用pthread_cond_wait之前线程已经收到了条件变量的唤醒通知(通过pthread_cond_signal或pthread_cond_broadcast),那么该通知实际上会被“记住”,直到线程真正进入pthread_cond_wait并准备返回。这是因为条件变量的实现通常包含一个等待队列,用于存储那些正在等待条件变量的线程。当调用pthread_cond_signal或pthread_cond_broadcast时,会唤醒等待队列中的一个或多个线程,但如果没有线程实际在pthread_cond_wait中等待,那么这个通知就会被保留,直到有线程调用pthread_cond_wait

问题:为什么 pthread_cond_wait 需要互斥量 ?
  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

 

  • 按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
  • 由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
  • int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。 ​​​​​​​

         3 .条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
  • 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

http://www.ppmy.cn/news/1531626.html

相关文章

多路复用和事件轮询机制

多路复用&#xff1a;Nio 服务端只有一个线程处理多个连接 事件轮询机制&#xff1a;select 底层用了 epoll。 select open 调用了 epoll 通过3个方法来实现事件轮询 1.epoll.create 创建epoll 多个集合 2.epoll.ctl 如果有事件会把事件挪到就绪事件列表。 3.epoll.wait 会监听…

使用python获取百度一下,热搜TOP数据详情

一、查找对应链接 # 警告&#xff1a;以下代码仅供学习和交流使用&#xff0c;严禁用于任何违法活动。 # 本代码旨在帮助理解和学习编程概念&#xff0c;不得用于侵犯他人权益或违反法律法规的行为。 1、打开百度页面 百度一下&#xff0c;你就知道 2、点击F12 或 右键鼠标…

vue3中使用iframe不成功的问题

再做大屏的时候&#xff0c;引用了一个html的页面&#xff0c;但是vue3编码&#xff0c;所以需要用到iframe&#xff0c;但是一直报错&#xff0c;故将解决方法做一个备份&#xff1a; <template><div class"screen-bg"><iframe src"/static/in…

Linux集群部署RabbitMQ

目录 一、准备三台虚拟机&#xff0c;配置相同 1、所有主机都需要hosts文件解析 2、所有主机安装erLang和rabbitmq 3、修改配置文件 4、导入rabbitmq 的管理界面 5、查看节点状态 6、设置erlang运行节点 7、rabitmq2和rabbitmq3重启服务 8、查看各个节点状态 二、添加…

ProgrammerAI—AI辅助编程学习指南

前言 随着AIGC&#xff08;AI生成内容&#xff09;技术的快速发展&#xff0c;诸如ChatGPT、MidJourney和Claude等大语言模型相继涌现&#xff0c;AI辅助编程工具正逐步改变程序员的工作方式。这些工具不仅可以加速代码编写、调试和优化过程&#xff0c;还能帮助解决复杂的编程…

教授(优青)团队一站式指导:专业实验设计、数据分析、SCI论文辅助。基因表达分析、转录因子、组蛋白、染色质、DNA等高通量检测及基因功能分析

可高通量检测组蛋白不同修饰在基因组上的位点; 可用于模式物种和非模式物种的研究&#xff0c;无需特异性抗体; 完整的DAP-seq解决方案。 DAP-seq可高通量检测转录因子或DNA结合蛋白在基因组上的结合位点; 可用于模式物种和非模式物种的研究&#xff0c;无需特异性抗体; 完整的…

Acwing 约数

1.试除法 思路分析&#xff1a;利用试除法求一个数的所有约数&#xff0c;思路和判断和求质数的判定类似 一个数N有一个约数d&#xff0c;那么N/d也必然是其约数 约数都是成对出现的&#xff0c;只需要枚举1到 n \sqrt{n} n ​即可&#xff0c;注意不要让一个约数加入两次! …

Go版数据结构 -【1.1 数据结构的分类与基本概念】

1.1 数据结构的分类与基本概念 本节我们将对数据结构的基本概念以及分类进行讲解&#xff0c;数据结构有很多种&#xff0c;但是主要也就是集中于几种类型&#xff0c;本节我们将进行逐一讲解。 什么是数据结构&#xff1f; 按照标准说法&#xff0c;数据结构指的是在计算机…