Linux之线程互斥

线程简单封装 

试着用线程控制力介绍的一些系统调用, 将线程的创建、执行和等待等都封装起来. 我们在程序中指定一个函数Print, 让多个线程不断地执行该函数.

 myThread.hpp

#pragma once
#include <functional>
#include <pthread.h>
#include <string>//假定线程内部的函数类型是void(T)类型的
template<class T>
using func_t = std::function<void(T)>;template<class T>
class Thread
{
public:Thread(const std::string& name, func_t<T> func, T data):_tid(0),_name(name),_func(func),_flag(false),_data(data){}//由于pthr_create内函数是void*(void*)类型, 所以需要把_func封装//由于成员函数默认第一个参数是this, 所以设置为静态取消this, 但是需要把this作为参数传递进来static void* ThreadRoutine(void* arg){Thread* ts = static_cast<Thread*>(arg);ts->_func(ts->_data);return nullptr; }    //获取线程名称std::string getThreadName(){return _name;}bool isRunning(){return _flag;}//线程开始运行bool Start(){int n = pthread_create(&_tid, nullptr, ThreadRoutine, (void*)this);if(!n){_flag = true;return true;}elsereturn false;}//回收线程bool Join(){if(!_flag) return true;int n = pthread_join(_tid, nullptr);if(!n){_flag = true;return true;}elsereturn false;}
private:pthread_t _tid; //线程tidstd::string _name; //线程名称func_t<T> _func; //线程执行的函数bool _flag; //线程是否正在运行T _data; //_func的参数
};

test.cc 

#include <iostream>
#include <unistd.h>
#include <vector>
#include "myThread.hpp"std::string getThreadName()
{static int count = 1;char buffer[64];snprintf(buffer, sizeof(buffer), "thread-%d", count++);return buffer;
}void Print(int num)
{while (num){std::cout << "hello world: " << num-- << std::endl;sleep(1);}
}int main()
{const int num = 5;std::vector<Thread<int>> threads;for(int i = 0; i < num; i++){Thread<int> thread(getThreadName(), Print, 5);threads.push_back(thread);}for (auto &t : threads){std::cout << t.getThreadName() << ", is running: " << t.isRunning() << std::endl;t.Start();std::cout << t.getThreadName() << ", is running: " << t.isRunning() << std::endl;}for (auto &t : threads){t.Join();}return 0;
}

创建一个线程: 

创建多个线程: 


Linux线程互斥

首先模拟一段多线程抢票的例子观察现象:

#include <iostream>
#include <unistd.h>
#include <vector>
#include "myThread.hpp"int ticket = 1000;//全局共享资源std::string getThreadName()
{static int count = 1;char buffer[64];snprintf(buffer, sizeof(buffer), "thread-%d", count++);return buffer;
}void getTicket(std::string name)
{while (true){if (ticket > 0){usleep(1000); // 充当抢票时间printf("%s get a ticket: %d\n", name.c_str(), ticket);ticket--;}else{break;}}//... TODO
}int main()
{const int num = 5;std::vector<Thread<std::string>> threads;for (int i = 0; i < num; i++){std::string name = getThreadName();Thread<std::string> thread(name, getTicket, name);threads.push_back(thread);}for (auto &t : threads){t.Start();}for (auto &t : threads){t.Join();}return 0;
}

 运行之后会发现不合理的现象, 票出现了非正值

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

临界资源: 多线程执行流共享的资源就叫做临界资源.(ticket)
临界区: 每个线程内部, 访问临界资源的代码, 就叫做临界区.(对ticket做修改的代码块)
互斥: 任何时刻, 互斥保证有且只有一个执行流进入临界区, 访问临界资源, 通常对临界资源起保护作用. 互斥保证了此时临界区的资源是被串行地访问, 而不是并发地访问.
原子性(后面讨论如何实现): 不会被任何调度机制打断的操作, 该操作只有两态, 要么完成, 要么未完成. 

  • 原子性表述的一种: 对资源进行操作, 如果只用一条汇编就能完成, 那么就说该操作具有原子性.

初步解释原子性: 比如--操作就不是原子操作, 这条语句其实对应三条汇编指令:

load :将 a 从内存加载到寄存器中
update : 更新寄存器里面的值, 执行-1操作(inc)
store :将新值, 从寄存器写回 a 的内存地址

 在这三条汇编语句的执行过程中, 都有可能会因为线程切换而中断, 假如线程A对a要执行++操作, 刚执行完inc指令还没有把a写入内存就被切换了, 线程B的函数把 a 修改为了100, 并写回了内存, 线程A又被切换回来, 继续执行之前的指令, 把a写入内存, 这样B之前的工作就相当于白做了.

分析: 为什么会出现不合理的结果(票为负数)?

1. 假如当前CPU是单核的. 现在ticket的值为1, if (ticket > 0) 语句判断条件为真以后, 在 usleep 这个模拟漫长业务的过程中, 该线程时间片到了, 会并发的切换到其他线程, 并把上下文保存到PCB中. 但是内存中ticket的值没有被改变, 仍是 1, 所以切换到其它线程后, CPU也从内存中读到了 1 , if判断也成立了. 

2. 我们上面说过, --tickets需要三步才能完成, 包括 1.读取数据到寄存器, 2.寄存器数据减一, 3.将寄存器数据写回内存.

此时每一个线程都进入了if语句框, 线程thread1再次被CPU执行. 此时内存中的tickets为1, 寄存器ebx读取数据变为1. 此时执行--tickets, 寄存器内数据变为0, 最后将0写回到内存的tickets中.

thread2线程也被再次唤醒, 再次读取tickets为0, 减一得到-1, 再将-1写回内存中. 后面的thread3、thread4、thread5也是这样的流程, 最后内存中的tickets经过五次减一变成了-4, 这就出现了负数.

要解决这个问题, 就要对临界区的代码进行加锁!


互斥锁 

 要想解决多线程的数据不一致问题需要做到以下几点:

  • 代码必须有互斥行为, 当一个线程进入临界区执行代码时, 不允许其他线程进入该临界区.
  • 如果有多个线程同时请求执行临界区代码, 并且临界区没有线程在执行代码, 那么只允许一个线程进入该临界区
  • 如果线程不在临界区中执行代码,那么该线程也不能阻止其他线程进入临界区.

其实做到上面三点只需要一把互斥锁, 你可以将锁看作一个通行证, 持有锁的线程才能进入临界区中执行代码, 其他线程不持有锁, 无法进入该临界区.

加锁本质就是让共享资源临界资源化, 多个线程串行访问共享资源, 从而保护共享资源的安全.

互斥锁本质上就是一个类(class pthread_mutex_t),可以构造对象pthread_mutex_t mutex, mutex就是互斥锁对象。

互斥锁初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

如果是全局或static修饰的锁, 使用上面语句初始化锁

如果是局部的锁, 用下面两个函数初始化和销毁锁: 

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

头文件:pthread.h

功能:初始化互斥锁.

参数:pthread_mutex_t *restrict mutex表示需要被初始化的锁的地址, const pthread_mutexattr_t *restrict attr表示锁的属性, 一般都为nullptr

返回值:取消成功返回0,取消失败返回错误码。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

头文件:pthread.h

功能:销毁互斥锁。

参数:pthread_mutex_t *mutex表示需要被销毁的锁的地址。

返回值:销毁成功返回0,失败返回错误码。

 加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

头文件:pthread.h

功能:对lock到unlock的部分代码加锁(仅允许线程串行)

参数:mutex表示需要加锁的锁指针

返回值:加锁成功返回0,失败返回错误码。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

头文件:pthread.h

功能:标识走出lock到unlock的部分代码解锁(恢复并发)

参数:mutex表示需要解锁的锁指针

返回值:解锁成功返回0, 失败返回错误码。

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(lock);
//临界区
//...
pthread_mutex_unlock(lock);

先写一个全局锁的实现:

#include <iostream>
#include <unistd.h>
#include <vector>
#include "myThread.hpp"int ticket = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//有了锁, 定义了并被初始化, 锁也是全局的std::string getThreadName()
{static int count = 1;char buffer[64];snprintf(buffer, sizeof(buffer), "thread-%d", count++);return buffer;
}void getTicket(std::string name)
{while (true){// 非临界区代码!// 2. 加锁是由程序员自己保证的!规则是临界区都必须先申请锁// 3. 根据互斥的定义,任何时刻,只允许一个线程申请锁成功!多个线程申请锁失败,失败的线程怎么办?在mutex上进行阻塞,本质就是等待!pthread_mutex_lock(&mutex);//1. 申请锁本身是原子的,是安全的if (ticket > 0)// 4. 一个线程在临界区中访问临界资源的时候,可不可能发生切换?可能, 而且切换后其它进程也无法访问临界区, 因为没有解锁.{usleep(1000); // 充当抢票时间printf("%s get a ticket: %d\n", name.c_str(), ticket);ticket--;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}//... TODO
}int main()
{const int num = 5;std::vector<Thread<std::string>> threads;for (int i = 0; i < num; i++){std::string name = getThreadName();Thread<std::string> thread(name, getTicket, name);threads.push_back(thread);}for (auto &t : threads){t.Start();}for (auto &t : threads){t.Join();}return 0;
}

 比如我们的那段 getTicket 临界区其实是 if(ticket > 0) 对应的代码块,  所以在执行临界区代码之前先加锁, 执行完之后再解锁, 注意在else里也要写一次解锁, 给ticket为0时解锁.

运行之后可以发现ticket不再出现非正数, 但是运行速度也明显变慢了, 因为临界区是串行执行.

 申请一个局部的锁, 由于我们getTicket函数里需要用到多个参数(局部锁和线程名字), 所以这里封装一个Thread_Data作为参数:

#include <iostream>
#include <unistd.h>
#include <vector>
#include "myThread.hpp"int ticket = 1000;class Thread_Data
{
public:Thread_Data(const std::string& name, pthread_mutex_t* mutex):_name(name),_pmutex(mutex){}public:std::string _name;pthread_mutex_t* _pmutex;
};std::string getThreadName()
{static int count = 1;char buffer[64];snprintf(buffer, sizeof(buffer), "thread-%d", count++);return buffer;
}void getTicket(Thread_Data* td)
{while (true){// 非临界区代码!// 2. 加锁是由程序员自己保证的!规则是临界区都必须先申请锁// 3. 根据互斥的定义,任何时刻,只允许一个线程申请锁成功!多个线程申请锁失败,失败的线程怎么办?在mutex上进行阻塞,本质就是等待!pthread_mutex_lock(td->_pmutex);//1. 申请锁本身是原子的,是安全的if (ticket > 0)// 4. 一个线程在临界区中访问临界资源的时候,可不可能发生切换?可能, 而且切换后其它进程也无法访问临界区, 因为没有解锁.{usleep(1000); // 充当抢票时间printf("%s get a ticket: %d\n", td->_name.c_str(), ticket);ticket--;pthread_mutex_unlock(td->_pmutex);}else{pthread_mutex_unlock(td->_pmutex);break;}}//... TODO
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);const int num = 5;std::vector<Thread<Thread_Data*>> threads;std::vector<Thread_Data*> tds;for (int i = 0; i < num; i++){std::string name = getThreadName();Thread_Data* td = new Thread_Data(name, &mutex);Thread<Thread_Data*> thread(name, getTicket, td);threads.push_back(thread);tds.push_back(td);}for (auto &t : threads){t.Start();}for (auto &t : threads){t.Join();}pthread_mutex_destroy(&mutex);for(auto& td : tds){delete td;}return 0;
}

效果是一样的: 

为了简化加锁解锁的过程, 我们可以自己封装一个锁Mutex和锁的守卫LockGuard :

#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 LockGuard
{
public:LockGuard(pthread_mutex_t *lock): _mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}
private:Mutex _mutex;
};

只需要在getTicket函数里需要加锁的地方定义一个LockGuard对象, 并用{}套起来, 这样lg的构造函数会为我们自动加锁, lg出代码块调用析构函数会自动解锁.

void getTicket(Thread_Data *td)
{while (true){{LockGuard lg(td->_pmutex);if (ticket > 0) {usleep(1000); // 充当抢票时间printf("%s get a ticket: %d\n", td->_name.c_str(), ticket);ticket--;}elsebreak;}//... TODO}}

 加锁原则: 

1. 我们要尽可能地给少的代码块加锁.

2.  一般加锁, 都是给临界区加锁

3. 谁加锁, 就由谁来解锁

加锁对线程的影响 

对上面的现象做进一步解释, 用pthread_mutex_lock函数加锁, 一个线程如果成功申请锁, 那么它就会继续向下执行, 如果申请不成功, 它就会在加锁处阻塞.

所以我们此时就能理解CPU排队处理线程和串行的关系了:

1. 当一个线程申请锁成功, 进入临界区访问临界资源, 其他线程要想进入临界区只能阻塞等待, 等待该进程将锁释放.
2. 即使线程切换了也没关系, 因为锁还在该线程的"手里", 其他线程仍然无法申请锁成功.

所以站在其他线程的角度, 临界区的代码只有两种状态, 被加锁和没被加锁, 不存在其它中间状态,  所以被加锁后的临界区是原子的.

补充: 如果使用pthread_mutex_trylock加锁,如果互斥量当前没有被其他线程锁定,pthread_mutex_trylock 会成功锁定它并立即返回. 但是, 如果互斥量已经被其他线程锁定, pthread_mutex_trylock 会立即返回错误, 而不是阻塞调用线程等待互斥量变得可用.


锁的本质

锁必须让所有线程都看到, 所以锁本身就是共享资源. 那谁来保护锁的安全呢?

锁是通过加锁和解锁操作的原子性来保证自身的安全的

为了实现互斥锁操作, 大多数体系结构都提供了swapexchange指令, 该指令的作用是把寄存器内存单元的数据相交换, 由于只有一条指令, 保证了原子性, 即使是多处理器平台,访问内存的 总线周期也有先后, 一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期.

lock和unlock的伪代码:

假设有多个线程, 每个线程中都有加锁的代码:

首先CPU开始执行线程thread1的代码, 首先 movb $0,%al, 把0写入al寄存器:

然后交换al寄存器和mutex在内存中的内容, 由于此时thread1是第一个加锁的线程, 所以mutex内存中是1. 由于寄存器的内容每个线程都有一份, 属于线程自己的上下文, 是线程的私有数据, 所以此时al寄存器为1就相当于thread1拿到了这把"锁":

al寄存器的内容>0, 返回0, 代表加锁成功: 

 

那么加锁和解锁操作的原子性体现在哪呢?

在上面那几条指令的任何一条指令处线程被切换, 都不会引起问题.

假如thread1 在echgb %al, mutex执行后就被切换了, thread2重新执行了一遍加锁的指令, 最后在al中存放的也只是0, 只能走到 else 挂起等待. 而因为thread1的上下文被自己保存,下一次线程切换时, thread1依旧拿着这把"锁"继续向下执行if语句, 加锁成功.

 

经过上面过程的描述,我们不难发现发现:

  • 锁只能被一个线程持有, 而且由于加锁是一条xchange汇编代码, 操作是原子性的, 也不需要担心线程切换的事情.
  • 一旦一个线程申请到锁, 因为即使该线程被切走, 锁还是在它的上下文数据中. 所以, 其他线程无法拿到锁,只能挂起等待, 只有等锁被释放时才能申请.
  • 锁的工作本质上就是锁类变量中的一个标志位1在不同进程间传递的过程, 只有申请到该标志位, 或者说持有锁的线程才能执行. 形象地说, 利用锁达到线程串行类似于很多人抢一张入场券.
  • 释放锁的过程对原子性的要求不高, 因为只有持有锁的线程才能释放锁, 未申请到锁的线程都在挂起.

可重入VS线程安全 

 可重入和不可重入对应的是函数的特征, 线程安全与否描述的是线程的特征.

重入 

重入:同一个函数被不同的执行流调用, 当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入.

一个函数在重入的情况下, 运行结果不会出现任何不同或者任何问题, 则该函数被称为可重入函数.否则, 是不可重入函数.

常见不可重入的情况

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

常见可重入的情况

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

线程安全 

线程安全: 多个线程并发同一段代码时, 不会出现不同的结果. 否则是线程不安全的.

常见的线程安全的情况

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

常见的线程不安全的情况:

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

可重入与线程安全的联系:

  • 函数可重入, 就是线程安全的. 这样的代码没有全局或静态变量, 不会产生数据不一致的问题
  • 函数不可重入, 如果多个线程并发, 就有可能引发线程安全问题. 对不可重入函数的全局变量需要加锁保护.
  • 如果一个函数中有不加锁保护的全局变量或静态变量, 那这个函数既不可重入, 多线程并发也不能保证线程安全.

可重入与线程安全的区别:

  • 可重入说的是函数的中性属性, 而线程安全说的是线程并发是否会出问题
  • 可重入函数是线程安全函数的一种, 因为不存在全局或者静态变量
  • 线程安全不一定保证函数可重入的, 而可重入函数又一定是线程安全的. 因为线程安全的情况可能是对全局变量等进行了加锁.
  • 由于线程安全可以通过加锁实现, 所以线程安全的情况比可重入要多

情况不需要记忆, 只需要知道结论: 

1. 多执行流并发访问代码块不出现问题(程序崩溃, 数据不一致), 是线程安全的.

2. 线程调用可重入函数一定是线程安全的


死锁

死锁的概念和必要条件

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

死锁形成的四个必要条件:

互斥条件:一个资源每次只能被一个执行流使用. (只要用到锁就必定有互斥).
请求与保持条件: 一个执行流因请求资源而阻塞时, 对已获得的资源保持不放(一个执行流申请其他锁时, 不释放自己已经持有的锁)
不剥夺条件: 一个执行流已获得的资源, 在末使用完之前, 不能强行剥夺.(已经持有锁的执行流, 在它不主动释放锁前, 其它线程不能强行剥夺它的锁.) 比如线程A有pthread_mutex_lock(&mutex1), 线程B不能强行解锁pthread_mutex_unlock(&mutex1).
循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系

假设线程A当前有一把锁lock1, 线程B有一把锁lock2, 但是线程A和线程B都需要同时拥有两把锁才能继续向下执行. 在上面的四个条件下, B线程申请lock1失败, 在申请lock1时阻塞; A线程申请lock2失败, 在申请lock2时阻塞. 此时A线程和B线程都在对方的锁处等待, 所以两个线程都不会向下运行了.

思考, 一把锁也会产生死锁吗?

会, 如果在解锁的时候误把unlock写成lock, 就是一个线程申请两次加锁, 不仅自己的线程会被阻塞, 其它线程也会阻塞, 造成死锁.

避免死锁

解决或者避免死锁的方法就是至少破坏4个死锁必要条件里的1个:

一、破坏条件1互斥条件就是直接不用锁, 但是锁是为了保护资源才使用的, 所以在不得不用锁的前提下, 只能破坏后3个条件.

二、

1.请求与保持条件--一个执行流因请求资源而阻塞时, 对已获得的资源保持不放. 

 破坏此条件就是要把将自己的资源释放掉, 如果线程曾经申请成功了一把锁, 在申请另一把锁失败时, 把自己的锁全部释放掉, 回退到没有申请锁的阶段. 假设A要申请lock2, 申请失败, 那就把lock1释放掉. 

2. 不剥夺条件--个执行流已获得的资源, 在末使用完之前, 不能强行剥夺.

在一个线程申请锁失败时, 解锁并强行加锁.

3. 循环等待条件--若干执行流之间形成一种头尾相接的循环等待资源的关系

建议按照同样的次序申请锁. 线程A按照lock1和lock2的顺序申请锁, 线程B也按照这个次序. 而不是图中那样.

总结: 尽量把锁资源, 按照顺序一次性申请给线程. 


避免死锁算法

  • 死锁检测算法(了解)
  • 银行家算法(了解) 

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

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

相关文章

centos7下卸载MySQL,Oracle数据库

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 操作系统版本为CentOS 7 使⽤ MySQ…

HiveMetastore

HiveMetastore 背后的存储 select * from DBS; select * from TBLS; select * from TABLE_PARAMS; 查找出没有 totalSize stats 的table SELECT DBS.NAME,t.TBL_NAME from DBS inner join (select DB_ID,TBL_NAME from TBLS where TBLS.TBL_ID not in(select TBL_ID from T…

JavaEE:Servlet创建和使用及生命周期介绍

目录 ▐ Servlet概述 ▐ Servlet的创建和使用 ▐ Servlet中方法介绍 ▐ Servlet的生命周期 ▐ Servlet概述 • Servlet是Server Applet的简称&#xff0c;意思是 用Java编写的服务器端的程序&#xff0c;Servlet被部署在服务器中&#xff0c;而服务器负责管理并调用Servle…

Leetcode:最长公共前缀

题目链接&#xff1a;14. 最长公共前缀 - 力扣&#xff08;LeetCode&#xff09; 普通版本&#xff08;横向扫描&#xff09; 主旨&#xff1a;用第一个字符串与后续的每个字符串进行比较&#xff0c;先获取S1和S2的最长公共前缀&#xff0c;然后将该次比较获得的最长公共前缀…

自媒体利器-如何实现自媒体文章分发操作,自媒体怎么赚钱

大家好&#xff0c;我是网创有方的站长&#xff0c;由于最近在做自媒体&#xff0c;发现一个非常让人苦恼的问题&#xff0c;就是多平台分发操作&#xff0c;市面上有那种多平台一键发布的工具&#xff0c;但通常都要收费才能用&#xff0c;所以特别自制了一个免费使用的小工具…

巨详细Linux安装MySQL

巨详细Linux安装MySQL 1、查看是否有自带数据库或残留数据库信息1.1检查残留mysql1.2检查并删除残留mysql依赖1.3检查是否自带mariadb库 2、下载所需MySQL版本&#xff0c;上传至系统指定位置2.1创建目录2.2下载MySQL压缩包 3、安装MySQL3.1创建目录3.2解压mysql压缩包3.3安装解…

数据结构练习题——Java实现

20240531-时间复杂度 1、消失的数字 方法一&#xff1a;位运算 两个数字一样的数组&#xff0c;其中一个数组中少了一个数字&#xff0c;定义一个变量分别异或两个数组&#xff0c;结果即为缺少的数字 class Solution {public int missingNumber(int[] nums) {int xor 0;int…

ChatGPT 宕机部分用户访问报错 api key开发应用不影响

就在今日4号下午&#xff0c;有部分用户反映ChatGPT访问报错&#xff0c;不幸的是&#xff0c;ChatGPT 目前对某些用户不可用 - 该问题已被发现&#xff0c;OpenAI 团队正在努力解决它 似乎就api 开发使用key的应用不受影响 以下是对接ChatGPT api key开发的应用正常对话

[数据集][目标检测]室内积水检测数据集VOC+YOLO格式761张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;761 标注数量(xml文件个数)&#xff1a;761 标注数量(txt文件个数)&#xff1a;761 标注类别…

【机器学习系列】掌握随机森林:从基础原理到参数优化的全面指南

目录 目录 一、随机森林简介 (一)随机森林模型的基本原理如下&#xff1a; (二)随机森林模型的优点包括&#xff1a; (三)森林中的树的生成规则如下&#xff1a; (四)在随机森林中&#xff0c;每棵树都使用不同的训练集进行训练&#xff0c;原因如下 随机森林的分类性能&…

全网唯一:触摸精灵iOS版纯离线本地文字识别插件

目的 触摸精灵iOS是一款可以模拟鼠标和键盘操作的自动化工具。它可以帮助用户自动完成一些重复的、繁琐的任务&#xff0c;节省大量人工操作的时间。但触摸精灵的图色功能比较单一&#xff0c;无法识别屏幕上的图像&#xff0c;根据图像的变化自动执行相应的操作。本篇文章主要…

2024年Python最新30道练手题(附详细答案),新手小白必备项目学习资源!

今天给大家分享2024年最新30道Python练习题&#xff0c;建议大家先独立思考一下解题思路&#xff0c;再查看答案。&#xff08;文末附python学习资料&#xff09; 1. 已知一个字符串为 “hello_world_yoyo”&#xff0c;如何得到一个队列 [“hello”,”world”,”yoyo”] &…

ChatGPT Mac客户端 下载安装教程(免费 不限次数使用 还支持语音聊天)

ChatGPT Mac客户端 下载安装教程&#xff08;免费 不限次数使用 还支持语音聊天&#xff09; 免费 不限次数使用 还支持语音聊天 系统要求&#xff1a; macOS 14 和 Apple Silicon&#xff08;M1 或更高版本&#xff09; 文章目录 ChatGPT Mac客户端 下载安装教程&#xff08;…

【scau大数据技术与原理2】综合性实验Spark集群的安装和使用——安装启动spark shell篇

实验内容简介&#xff1a; Spark是一个分布式计算框架&#xff0c;常用于大数据处理。本次实验中&#xff0c;首先设计一个包含主节点和从节点的Spark集群架构&#xff0c;并在CentOS的Linux环境下进行搭建。通过下载并解压Spark安装包&#xff0c;配置环境变量和集群参数&…

Kaggle平台进行Python版本降级

前言 最近在复现语音合成模型VITS&#xff0c;由于目前没有算力故去Kaggle白嫖运算资源。 VITS的运行环境要求如下 Cython0.29.21 librosa0.8.0 matplotlib3.3.1 numpy1.18.5 phonemizer2.2.1 scipy1.5.2 tensorboard2.3.0 torch1.6.0 torchvision0.7.0 Unidecode1.1.1截至2…

MYSQL数据库客户端常规指令使用

这里新开一章&#xff0c;对MYSQL进行更加底层的系统的一个学习 Mysql常用工具简介 emmmm这里的话就默认大家在linux系统上面都进行了MYSQL的安装了. 在mysql安装完成之后&#xff0c;一般在路径 /usr/bin 下的 我们对该路径进行一个文件的展示 这里是展示出来的辅助工具 …

SDL教程(二)——Qt+SDL播放器

前言 ​ 这篇文章主要是使用SDL来打开视频&#xff0c;显示视频。后续会再继续使用SDL来结合FFmpeg。来能够直接使用网上的demo进行学习。 正文 一、环境 Qt 5.15.2 MSVC2019 64bit Win11 二、Qt搭建SDL Qt搭建&#xff0c;我觉得相比用VS2019来说&#xff0c;更为方便&…

Pandas读取文本文件为多列

要使用Pandas将文本文件读取为多列数据&#xff0c;你可以使用pandas.read_csv()函数&#xff0c;并通过指定适当的分隔符来确保正确解析文件中的数据并将其分隔到多个列中。 假设你有一个以逗号分隔的文本文件&#xff08;CSV格式&#xff09;&#xff0c;每一行包含多个值&a…

Java进制转换

进制介绍 二进制&#xff1a;0B开头&#xff0c;0-1 八进制&#xff1a;0开头&#xff0c;0-7 十进制&#xff1a;0-9 十六进制&#xff1a;0x开头&#xff0c;0-9和A-F public class Binary{public static void main(String[] args){//二进制 10int n10B1010//十进制 1010int…

(二刷)代码随想录第15天|层序遍历 226.翻转二叉树 101.对称二叉树2

层序遍历 10 102. 二叉树的层序遍历 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 综合代码&#xff1a; class Solution{public List<List<Integer>> resList new ArrayList<List<Integer>>();public List<List<…