Linux 线程的同步与互斥

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:Linux初窥门径⏪

🚚代码仓库:Linux代码练习🚚

🌹关注我🫵带你学习更多Linux知识
  🔝 

 

 前言

1.资源共享问题 

2.进程线程间的互斥相关背景概念

3.锁 

3.1锁文档说明 

3.2 加锁 解锁操作

3.3 ARII风格锁 

3.4 深入理解锁 

3.5 毁🔒

3.6 可重入&&线程安全

 3.7 常见锁概念

3.7.1死锁 

4.条件变量

4.1同步操作相关函数 


 

 前言

由于线程之间存在竞争,就导致了多线程有的线程涝的涝死,饿的饿死,就需要让线程之间保持某种平衡,让它们被CPU雨露均沾。这就是所谓的同步。由于临界资源只有一份,线程之间同时共享临界资源。为了防止临界资源的安全,线程之间需要互斥。

1.资源共享问题 

在Linux 线程控制​​​​​​文章我们知道了一个进程中的所有线程,在地址空间中的代码区、未初始化区、什么堆区也好、栈区也好,还是共享区也好都是共享的。

就好比下面这个代码

#include <iostream>int n = 0;int main()
{n++;return 0;
}

n是属于main函数栈帧中,如果我们创建线程,那么所有线程都是可以看见它。如果两个线程同时对它++,那么很可能n的值会超过我们的预期。

#include <thread>
#include <iostream>using namespace std;
int n = 0;int main()
{thread t1([](){for (int i = 0; i < 100000; i++){n++;}});thread t2([](){for (int i = 0; i < 100000; i++){n++;}});t1.join();t2.join();cout << n << endl;return 0;
}

打印出来的结果确实超出我们预期这是为什么?

n++; 这句代码确实是一句代码,但是对于汇编而是3条指令

也就是说当t1对n进行++时,其实是分为3步,同理t2也是3步。这就导致了++不是原子操作

  t1在执行add这条汇编指令时,也就是说当t1进行++时,调度的时间片到了,没有执行第3条指令mov,而t1被切换走时,会带走自己下上文数据。而这时t2被调度。而这个期间t2一直被调度疯狂的++,且完整的执行完汇编语句,将寄存器的值拷贝到内存中,等t1在被调度回来。t1将自己的上下文数据写回寄存器中,它就会执行第3条mov汇编语句,而不是从新开始。而它的n值是1,拷贝会内存这就出事了。覆盖了t2对n++的值。

结论:多线程场景中对全局变量线程并发访问并不是安全的。 

2.进程线程间的互斥相关背景概念

前面的简单实验我们可以得出几个名词

对多线程来说:

n就是临界资源,n++就是临界区 ,两个线程不让同时访问临界资源,叫做互斥。n++这个操作,要么是一次性完成的,要么是未完成的(未开始)那就是原子操作。

总结:

  • 临界资源:多线程执行流共享的资源就叫做临界资源

  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完

3.锁 

既然多线程之间并发访问,会导致临界资源安全性的问题,互斥能让它们对临界资源起到保护作用。

那利用什么方式能让它们每个时刻只有一个线程访问临界资源?

我们先举个生活中例子 公共厕所。

厕所在社会中属于公共资源,每个人上厕所都会把们关上,且把门上的锁进行反锁。而这个过程就是独享这份"公共资源"。

那对于代码中,如何对临界资源上锁?

Linux中对于锁也是有接口的 我们先用指令了解接口说明文档

3.1锁文档说明 

指令:man 3 pthread_mutex_lock

 

pthread_mutex_lock(pthread_mutex_t *mutex)

  • 功能:锁定由 mutex 引用的互斥锁。
  • 行为:如果互斥锁已经被其他线程锁定,则调用线程将阻塞,直到互斥锁可用。
  • 返回状态:如果成功,函数返回0;互斥锁被锁定,调用线程成为其所有者。

互斥锁类型

  • PTHREAD_MUTEX_NORMAL:不提供死锁检测。重复锁定会导致死锁。解锁未锁定的互斥锁会导致未定义行为。
  • PTHREAD_MUTEX_ERRORCHECK:提供错误检查。重复锁定或解锁未锁定的互斥锁将返回错误。
  • PTHREAD_MUTEX_RECURSIVE:维护一个锁定计数。首次成功获取互斥锁时,锁定计数设置为1。每次重复锁定时,计数增加1;每次解锁时,计数减少1。计数归零时,互斥锁对其他线程可用。解锁未锁定的互斥锁将返回错误。
  • PTHREAD_MUTEX_DEFAULT:默认类型,尝试递归锁定会导致未定义行为。如果解锁的互斥锁不是由调用线程锁定的,或者没有被锁定,将导致未定义行为。

pthread_mutex_trylock(pthread_mutex_t *mutex)

  • 功能:与 pthread_mutex_lock() 相似,但如果互斥锁已经被锁定(包括当前线程),调用将立即返回,而不是阻塞。

pthread_mutex_unlock(pthread_mutex_t *mutex)

  • 功能:释放由 mutex 引用的互斥锁。
  • 释放方式:依赖于互斥锁的类型属性。如果有线程因互斥锁变为可用而被阻塞,调度策略将决定哪个线程获得互斥锁。

信号处理

  • 如果等待互斥锁的线程接收到信号,从信号处理程序返回后,线程将继续等待互斥锁,就像没有被中断一样。

返回值

  • 如果成功,pthread_mutex_lock() 和 pthread_mutex_unlock() 函数返回0;否则,返回错误编号以指示错误。

指令:man 3 pthread_mutex_init

pthread_mutex_destroy()

  • 功能:销毁由 mutex 引用的互斥锁对象,使其变为未初始化状态。
  • 安全:只能销毁未被锁定的互斥锁。尝试销毁一个被锁定的互斥锁将导致未定义行为。
  • 重新初始化:销毁的互斥锁可以通过 pthread_mutex_init() 重新初始化。
  • 错误行为:销毁操作后引用互斥锁将导致未定义行为。

pthread_mutex_init()

  • 功能:使用 attr 指定的属性初始化 mutex 引用的互斥锁。如果 attr 是 NULL,则使用默认属性。
  • 状态:初始化成功后,互斥锁变为已初始化且未锁定状态。
  • 同步使用:只能使用 mutex 本身进行同步操作,不能使用其副本。
  • 重复初始化:尝试重复初始化已初始化的互斥锁将导致未定义行为。

PTHREAD_MUTEX_INITIALIZER

  • 用途:用于静态分配的互斥锁的初始化。效果等同于使用 NULL 作为属性参数调用 pthread_mutex_init(),但不会执行错误检查。

返回值

  • 成功时,pthread_mutex_destroy() 和 pthread_mutex_init() 返回0;失败时,返回错误编号以指示错误。

 文档介绍完毕,我们直接开始代码实操

代码示例 

先来一个没有加锁的模拟抢票的过程。

#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
class ThreadData
{
public:ThreadData(int number){_threadname = "thread-" + to_string(number);}string GetName(){return _threadname;}private:string _threadname;
};
#define NUM 5
int tickets = 1000; // 火车票
void *GetTickets(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->GetName().c_str();while (true){if (tickets > 0){usleep(1000);printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);tickets--;}elsebreak;printf("线程%s ... 退出\n", name);}return nullptr;
}
int main()
{// 创建多线程vector<pthread_t> tids;            // 数组放线程IDvector<ThreadData *> thread_datas; // 线程信息for (int i = 1; i <= NUM; i++){pthread_t tid;ThreadData *td = new ThreadData(i);thread_datas.emplace_back(td);pthread_create(&tid, nullptr, GetTickets, thread_datas[i-1]);tids.emplace_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}return 0;
}

和之前一样如果在实现中,我们这个代码就出问题了。乘客给钱了,但是没有票了。

3.2 加锁 解锁操作

#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
class ThreadData
{
public:ThreadData(int number){_threadname = "thread-" + to_string(number);}string GetName(){return _threadname;}private:string _threadname;
};
#define NUM 5
int tickets = 1000; // 火车票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //静态分配。
void *GetTickets(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->GetName().c_str();while (true){pthread_mutex_lock(&lock);if (tickets >= 0){usleep(1000);printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);tickets--;pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}printf("线程:%s ... 退出\n", name);}return nullptr;
}
int main()
{// 创建多线程vector<pthread_t> tids;            // 数组放线程IDvector<ThreadData *> thread_datas; // 线程信息for (int i = 1; i <= NUM; i++){pthread_t tid;ThreadData *td = new ThreadData(i);thread_datas.emplace_back(td);pthread_create(&tid, nullptr, GetTickets, thread_datas[i - 1]);tids.emplace_back(tid);}for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}return 0;
}

我使用的是静态的锁,其实也就是个宏 

静态分配 的优点在于 无需手动初始化和手动销毁,锁的生命周期伴随程序,缺点就是定义的 互斥锁 必须为 全局互斥锁 

当然我们也可以使用动态分配,动态分配需要我们手动初始化。

pthread_mutex_t lock; //动态要初始化
pthread_mutex_init(&lock,nullptr);

3.3 ARII风格锁 

 但是使用锁的方式很容易造成死锁的问题,就比如上面的代码需要二次解锁。我们可以利用ARII的思想

#pragma once
#include <pthread.h>
class Mutex
{
public:Mutex(pthread_mutex_t *lock): _lock(lock){}void lock(){pthread_mutex_lock(_lock);}void unlock(){pthread_mutex_unlock(_lock);}~Mutex() {}private:pthread_mutex_t *_lock;
};
class lockGudard
{
public:lockGudard(pthread_mutex_t *lock) //: _mutex(lock){_mutex.lock();}~lockGudard(){_mutex.unlock();}
private:Mutex _mutex;
};
void *GetTickets(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->GetName().c_str();while (true){//pthread_mutex_lock(&lock);lockGudard lockguard(&lock); //ARIIif (tickets >= 0){usleep(1000);printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);tickets--;// pthread_mutex_unlock(&lock);}else{//pthread_mutex_unlock(&lock);break;}printf("线程:%s ... 退出\n", name);}return nullptr;
}

ARII思想把资源的生命周期交给对象,利用C++类的特性。具体详情请看C++ 智能指针

3.4 深入理解锁 

 从结果来看,加锁之后临界资源确实被保护了,但是对锁来讲这里还有许多细节,我们一一来扣。

细节1:加锁的位置?

 每一个线程访问临界资源前都是要加锁的,本质是对临界区加锁,所以在临界区的代码,有些代码是不涉及临界资源的,例如上图在循环代码之前加锁,也就是说线程要拿到锁才能进入循环。

如果不在循环之前加锁而是在if之前加锁,那么所有线程都能进入循环。如果还有其他不涉及临界的代码。这个线程没有拿到锁是不是就可以执行其他代码?

 

建议加锁时,粒度要尽可能的细,因为加锁后区域的代码是串行化执行的,代码量少一些可以提高多线程并发时的效率 

细节2:多线程之间访问同一个临界资源可以不是同一把锁? 

当然不行,多线程之间访问临界资源,如果一个线程自己带锁,那么它就不会阻塞等待。那它就起飞了,没有人管了。只有多线程之间看到同一把互斥锁,才能让它们互斥。

细节3:互斥锁既能是全局的,又能是局部。那它不也是临界资源?它如何保证自己的安全?

加锁 是为了保护 临界资源 的安全,但  本身也是 临界资源,这就像是一个 先有鸡还是先有蛋的问题 的设计者也考虑到了这个问题,于是对于  这种 临界资源 进行了特殊化处理:加锁 和 解锁 操作都是原子的,不存在中间状态,也就不需要保护了 

 我们先来看一段互斥锁伪汇编

lock:movb $0, %alxchgb %al, mutexif(al寄存器里的内容 > 0){return 0;} else挂起等待;goto lock;

 假设线程a先拿到锁

 

  1. 将 %al 寄存器的值设置为0。
  2. 使用 xchgb 指令尝试将 %al 与 mutex 原子地交换值。假设mutex 原本为1,%al 将包含旧的 mutex 值(也是1),并且 mutex 现在被设置为0。
  3. 如果 %al 为1,表示当前线程a成功获取了锁,可以继续执行临界区代码。
  4. 线程b申请锁
  5. 检查 %al 寄存器的值。如果它大于0,表示其他线程a已经持有锁,当前线程b应该返回0(通常表示错误或失败)。
  6. 如果锁已被其他线程持有,当前线程将挂起等待,直到锁变为可用状态。
  7. 一旦锁可用,线程再次尝试获取锁,跳转回 lock 标签处继续执行。

 

unlock:movb $1, mutex唤醒等待 [锁资源] 的线程;return

 假设现在线程a解锁:

1. 将mutex的值设置为1

2. 将线程b唤醒,线程b再执行lock那一套逻辑。

3. 从解锁函数返回执行后面代码。

注意:1.xchgb原子操作不会被任何调度打断,要么完成,要么未完成。

           2. 寄存器的值不等于线程上下文的值。

 细节4:一个线程可以一直拿着锁吗?

理论上是可行的,如果一个线程是拿着锁的,如果它的线程调度时间片到了,也是有可能连锁一起带走。为什么这么说

 如果现在这间vip室室免费且小明先拿到🔑进来,但是小明又想出去玩一会,然后小明出门把🔑放在🔒,准备走的时候,发现有很多人,小明心里又不想失去这间vip室。所以小明又重新拿着🔑进入vip室,其他人都没有抢过小明,因为小明离🔑最近。

对于线程来说,那就是拿到🔒意味就能访问临界区。其他线程只能阻塞等待,而解锁的过程就是线程离开临界区,其他线程访问临界区。

3.5 毁🔒

当进程退出我们也是需要对锁资源进行清理,销毁互斥锁可以释放与之关联的系统资源。 

pthread_mutex_destroy(&lock);

就是这么简单的一句。

3.6 可重入&&线程安全

概念:

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

常见的线程不安全的情况 

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

 常见的线程安全的情况

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

常见不可重入的情况 

我们所学的大部分函数都是不可重入的

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

反之其他的函数都是可以重入的

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

可重入与线程安全区别

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

 3.7 常见锁概念

3.7.1死锁 

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

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

必要条件只要有一个不成立,都不会出现死锁问题。

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

担心死锁问题的小伙伴,不用怕,直接无脑ARII锁。 当然也有常见的避免 死锁 问题的算法:死锁检测算法、银行家算法

4.条件变量

 从这个结果来看,绝大部分都是线程1在抢票,我的运行结果都没有5号线程和2号线程。

 导致这两个线程饥饿,这并不是我们想要的,能否按照一定的顺序有序的访问。造成这样的原因还是因为线程之间的竞争导致的。

这时我们又要重新说到vip的房间了。小明拿到🔑,下次再申请🔑时候,小明最近。所以其他人是抢不到的,这时管理员看不下去了,对小明说你不能这样干,管理员强行对小明进行限制。

对于线程来说也是同理,我们需要对线程进行一定条件的限制。让其他线程雨露均沾。

我们又要引入一个新的概念条件变量。

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

可以把 条件变量 看作一个结构体,其中包含一个 队列 结构,用来存储正在排队等候的线程信息,当条件满足时,就会取 队头 线程进行操作,操作完成后重新进入 队尾 

 

 同步概念与竞态条件

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

 

4.1同步操作相关函数 

PTHREAD_COND_INITIALIZER 是一个宏,用于在编译时初始化条件变量对象。 

优缺点和PTHREAD_MUTEX_INITIALIZER 一样

条件变量函数 初始化 

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL

销毁  

int pthread_cond_destroy(pthread_cond_t *cond) 

等待条件满足  

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量

唤醒等待 


int pthread_cond_broadcast(pthread_cond_t *cond);
//这个函数的作用是唤醒所有等待指定条件变量 cond 的线程。
int pthread_cond_signal(pthread_cond_t *cond);
//这个函数用于唤醒等待指定条件变量 cond 的一个线程。

 

#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
class ThreadData
{
public:ThreadData(int number){_threadname = "thread-" + to_string(number);}string GetName(){return _threadname;}private:string _threadname;
};
#define NUM 5
int tickets = 1000;                               // 火车票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 静态分配。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;   // 条件变量静态全局变量
void *GetTickets(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);const char *name = td->GetName().c_str();while (true){pthread_mutex_lock(&lock);pthread_cond_wait(&cond, &lock);if (tickets > 0){usleep(1000);tickets--;printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}printf("线程:%s ... 退出\n", name);return nullptr;
}
int main()
{// 创建多线程vector<pthread_t> tids;            // 数组放线程IDvector<ThreadData *> thread_datas; // 线程信息for (int i = 1; i <= NUM; i++){pthread_t tid;ThreadData *td = new ThreadData(i);thread_datas.emplace_back(td);pthread_create(&tid, nullptr, GetTickets, thread_datas[i - 1]);tids.emplace_back(tid);}while (tickets > 0){usleep(1000);pthread_cond_signal(&cond);cout << " 主线程唤醒新线程..." << endl;}pthread_cond_broadcast(&cond); //这里需要唤醒所有等待的线程for (auto thread : tids){pthread_join(thread, nullptr);}for (auto td : thread_datas){delete td;}return 0;
}

 

 问题1:为什么条件变量的等待函数要在锁的后面?

条件不满足时,要去等待队列阻塞等待被唤醒。

一个线程拿到了锁,不解锁去等待,后面的线程不就拿不到锁,不就死锁了吗?

wait函数调用时会自动释放锁,这也是为什么第二个参数是锁。

问题2:我们怎么知道我们要让一个线程去休眠了那?

首先临界资源也是有状态的,要么就绪,要么不就绪。不就绪条件不满足。所以线程回去休眠

问题3:你怎么知道临界资源是就绪还是不就绪的?

很简单 我们 if 这里进行判断不就是访问临界资源吗?这也是为什么判断会在加锁之后。

总结:线程同步与互斥 主要讲解了 条件变量 函数接口使用。包括互斥锁的概念、操作、原理,以及多线程与互斥锁的封装;最后简单学习了线程同步相关内容,重点在于对条件变量的理解及使用。至于互斥锁+条件变量的实战:生产者消费者模型将会在下一篇文章中完成

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/31611.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言程序设计-7 数组

在程序设计中&#xff0c;为了处理方便&#xff0c;把具有相同类型的若干变量按有序的形式组织起来。这些按序排列的同类数据元素的集合称为数组。在&#xff23;语言中&#xff0c;数组属于构造数据类型。一个数 组可以分解为多个数组元素&#xff0c;这些数组元素可以是基本数…

晶体振荡电路中的负性阻抗是什么?-晶发电子

在理想的振荡电路中&#xff0c;为了保持振荡的稳定性和强度&#xff0c;需要一种机制来补偿晶振振动过程中的能量损耗。在实际应用中&#xff0c;这种能量损耗是不可避免的&#xff0c;它可能导致振荡逐渐衰减直至停止。为了解决这个问题&#xff0c;振荡电路设计者采用了一种…

公司怎么管理文档外发泄密?强化企业文档安全用迅软加密软件就行了!

一、文档加密软件原理 迅软DSE加密软件对各类需要加密的文件&#xff08;如&#xff1a;技术资料、商业数据、红头文件、会议纪要、机要文件、图纸、财务报表等&#xff09;进行加密。 使用加密算法对文件自动加密&#xff0c;只有拥有正确的解密密钥或密码的人才能打开文件&…

语言模型测试系列【10】

一个巧合&#xff0c;又测到了新的区别&#xff0c;以下是关于python代码生成的测试效果。 语言模型 文心一言讯飞星火通义千问2.5豆包360智脑百小应腾讯元宝KimiC知道商量智谱清言 这次的测试问题来源于**智谱AI开放平台**的介绍&#xff0c;正好有个python生成的效果说明…

韩顺平0基础学java——第28天

p569-591 坦克大战&#xff01;&#xff08;绘图监听事件线程文件处理&#xff09; 绘图 绘图原理 Component类提供了两个和绘图相关最重要的方法: 1. paint(Graphics g)绘制组件的外观 2. repaint()刷新组件的外观。 当组件第一次在屏幕显示的时候,程序会自动的调用paint()…

智能网联汽车实训教学“好帮手”——渡众机器人自动驾驶履带车

智能网联汽车实训教学“好帮手”——渡众机器人自动驾驶履带车 人工智能技术的兴起&#xff0c;为传统汽车行业注入了强有力的变革基因&#xff0c;以AI技术为驱动的无人驾驶成为汽车产业的未来&#xff0c;同样也面临诸多机遇和挑战。 一方面智能网联汽车的发展&#xff0c;为…

GD32学习

参考视频13.立创开发板GD32教程&#xff1a;串口配置_哔哩哔哩_bilibili 固件库跟用户手册基本上差不多&#xff0c;只不过用用户手册编写程序的话会更加的底层&#xff0c;固件库的话就是把一些函数封装起来&#xff0c;用的时候拿过来即可&#xff0c;目前我还没有找到固件库…

【数据库备份完整版】物理备份、逻辑备份,mysqldump、mysqlbinlog的备份方法

【数据库备份完整版】物理备份、逻辑备份&#xff0c;mysqldump、mysqlbinlog的备份方法 一、物理备份二、逻辑备份1.mysqldump和binlog备份的方式&#xff1a;2.mysqldump完整备份与恢复数据2.1 mysqldump概念2.2 mysqldump备份2.3 数据恢复2.4 **使用 Cron 自动执行备份**2.5…

linux下编译安装和使用cURL库(含有openssl)

cURL是一个利用URL语法指定各种协议(如HTTP、HTTPS、FTP等)进行数据传输的工具,它以客户端的身份,允许用户通过命令行与服务器交互。cURL库(libcurl)是cURL的编程接口(API),提供了一套丰富的函数,供开发者在自己的应用程序中实现网络传输功能 cURL库是网络编程中不可…

搭建一个简单的xxljob

数据库表结构&#xff1a; YyJobInfo&#xff1a; public class YyJobInfo {//定时任务idprivate int id;//该定时任务所属的执行器的idprivate int jobGroup;//定时任务描述private String jobDesc;//定时任务添加的时间private Date addTime;//定时任务的更新时间private D…

服务器权限管理

我们linux服务器上有严格的权限等级&#xff0c;如果权限过高导致误操作会增加服务器的风险。所以对于了解linux系统中的各种权限及要给用户&#xff0c;服务等分配合理的权限十分重要。&#xff08;权限越大&#xff0c;责任越大&#xff09; 1.基本权限 U--user用户,G-group…

智谱API调用

一、智谱API 文心一言api 千帆大模型平台 申请和使用 智谱AI开放平台 登录智谱AI开放平台&#xff0c;点击右上角的开发者工作台&#xff0c;然后查看自己的API glm-4 接口 conda create -n zhipuai python3.10 -y 二、如何使用 这边的介绍是根据官方文档的接口文档来进行介绍…

深度学习之绘图基础

文章目录 1.实验目的2. 需求3.代码结果图片 1.实验目的 熟练绘制各种图像&#xff0c;为深度学习打基础 2. 需求 给定一个函数&#xff0c;需要你画出原图像以及这个函数在某点切线图像 3.代码 # File: python绘制函数图像以及倒数图像.py # Author: chen_song # Time: 20…

绽放光彩的小程序 UI 风格

绽放光彩的小程序 UI 风格

vivado WIRE

WIRE是用于在Xilinx部件上路由连接或网络的设备对象。一根电线 是单个瓦片内部的一条互连金属。PIP、系紧装置和 SITE_PINs。 提示&#xff1a;WIRE对象不应与设计的Verilog文件中的WIRE实体混淆。那些 电线在设计中与网络有关&#xff0c;而不是与定义的设备的路由资源有关 WI…

创业众筹网

摘 要 创业是社会经济发展的重要动力&#xff0c;其在任何经济发展时期任何国家都最具活力与桃战性。然而创业的资金却是90%创业者面临的首要问题。包括积蓄不足、无不动产、负债、不知如何向银行申贷,及无法预估所创行业之总资金、成本。部分创业者虽然有心创业&#xff0c;但…

AI创作音乐引发的深思

在最近一个月中&#xff0c;音乐大模型的迅速崛起让素人生产音乐的门槛降到了最低。这一变革引发了关于AI能否彻底颠覆音乐行业的广泛讨论。在初期的兴奋过后&#xff0c;人们开始更加理性地审视AI在音乐领域的应用&#xff0c;从版权归属、原创性、创作质量、道德层面以及法律…

【读论文】Learning perturbations to explain time series predictions

文章目录 Abstract1. Introduction2. Background Work3. Method4. Experiments4.1 Hidden Markov model experiment4.2 MIMIC-III experiment 5. ConclusionReferences 论文地址&#xff1a;Learning Perturbations to Explain Time Series Predictions代码地址&#xff1a;htt…

深度学习Week17——优化器对比实验

文章目录 深度学习Week17——优化器对比实验 一、前言 二、我的环境 三、前期工作 1、配置环境 2、导入数据 2.1 加载数据 2.2 检查数据 2.3 配置数据集 2.4 数据可视化 四、构建模型 五、训练模型 1、将其嵌入model中 2、在Dataset数据集中进行数据增强 六、模型评估 1、Accur…

鸿蒙 登录界面示例

1.b站登录界面 我的b站教学视频&#xff1a;https://www.bilibili.com/video/BV1LQgQexEGm/?spm_id_from333.999.0.0&vd_sourced0ea58f1127eed138a4ba5421c577eb1 最终实现效果&#xff1a; 需要准备2张图片&#xff0c;分别是向下和向右边的图标 代码&#xff1a; En…