【Linux/C++多线程篇(二) 】给线程装上“红绿灯”:通俗易懂的同步互斥机制讲解 & C++ 11下的多线程

【Linux/C++多线程篇(二) 】给线程装上“红绿灯”:通俗易懂的同步互斥机制讲解 & C++ 11下的多线程

⭐️在这个怀疑的年代,我们依然需要信仰

个人主页:YYYing.

⭐️Linux/C++进阶系列专栏:【从零开始的linux/c++进阶编程】

系列上期内容:【Linux/C++多线程篇(一) 】多线程编程入门

系列下期内容:【Linux/C++网络篇(一) 】网络编程入门


目录

前言:当多线程遇上“交通混乱”

线程的同步互斥机制

一、为什么需要同步互斥?

二、线程互斥之互斥锁

2.1、互斥锁的相关API函数接口

 📖 创建一个互斥锁

 📖 初始化互斥锁

📖 获取锁资源

📖 释放锁资源

📖 销毁互斥锁

2.2、互斥锁的小练习

三、线程同步之无名信号量

3.1、无名信号量的相关API函数接口

 📖 创建无名信号量

 📖 初始化无名信号量

 📖 申请无名信号量的资源(P操作)

 📖 释放无名信号量的资源(V操作)

 📖 销毁无名信号量

3.2、互斥锁的小练习

四、线程同步之条件变量

4.1、条件变量的API函数接口

 📖 创建一个条件变量

 📖 初始化条件变量

 📖 消费者线程进入等待队列

 📖 生产者线程唤醒休眠队列中的任务

 📖 销毁条件变量

3.2、条件变量的小练习

C++11中的多线程

一、线程相关常用操作

1.1、线程的创建

1.2、线程体函数种类

1.3、线程号获取

1.4、线程号回收

二、互斥锁的使用

2.1、常用函数

2.2、lock_guard的使用

2.3、代码演示

三、条件变量

2.1、常用函数

2.2、代码演示

结语

---⭐️封面自取⭐️---



前言:当多线程遇上“交通混乱”

        想象一下,你是一个繁忙路口的交警,需要同时指挥四面八方的车辆。如果没有红绿灯和交警,所有车辆都凭感觉开,结果必然是撞车、拥堵、混乱。在计算机世界里,多个线程同时访问共享数据时,也会出现类似的“交通事故”——数据错乱、程序崩溃、结果不可预测

        为了让线程们有序地工作,我们需要给它们装上“红绿灯”,也就是同步互斥机制。本文将从生活比喻出发,带你轻松理解这些看似复杂的并发工具。

线程的同步互斥机制

一、为什么需要同步互斥?

假设有一个火车票售票系统,剩余票数为 1。两个线程同时执行以下操作

if (tickets > 0) { tickets--; cout << "购票成功"; }

        如果两个线程同时检查 tickets > 0,发现都是 1,于是都执行 tickets--,结果票数变成 -1,但两人都以为自己买到了票。这就是典型的竞态条件——程序的结果依赖于线程执行的偶然顺序。

        这种问题源于线程的交错执行tickets-- 不是原子操作(也就是指不会被线程调度机制打断的操作。),它实际上分为三步:读取、减一、写回。如果两个线程的步骤交错,就会出错。

        要解决这个问题,我们需要确保同一时刻只有一个线程能操作票数,这就是互斥。同时,可能还需要让一个线程等待另一个线程完成某件事(比如等票补足再卖),这就是同步

        总的来说——由于同一个进程的多个线程会共享进程的资源,这些被共享的资源称为临界资源,多个线程对公共资源的抢占问题,访问临界资源的代码段称为临界区,多个线程抢占进程资源的现象称为竞态,为了解决竞态,我们引入了同步互斥机制

下面,我们就来看看系统为我们提供的几种“红绿灯”。


二、线程互斥之互斥锁

        互斥锁的本质是一个特殊的临界资源,当该临界资源被某个线程所拥有后,其他线程就不能拥有该资源,直到,拥有该资源的线程释放掉互斥锁后,其他线程才能进行抢占(同一时刻,一个互斥锁只 能被一个线程所拥有),相当于一把获取资源的钥匙。

2.1、互斥锁的相关API函数接口

 📖 创建一个互斥锁

        只需定义一个pthread_mutex_t 类型的变量即创建了一个互斥锁

pthread_mutex_t mutex;

 📖 初始化互斥锁
函数原型

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;       //静态初始化

头文件iostream
功能初始化互斥锁变量
参数说明

参数1:互斥锁变量的地址,属于地址传递

参数2:互斥锁属性,一般填NULL,让系统自动设置互斥锁属性

返回值成功返回0,失败返回错误码

📖 获取锁资源
函数原型int pthread_mutex_lock(pthread_mutex_t *mutex);
头文件iostream
功能获取锁资源,如果要获取的互斥锁已经被其他线程锁定,那么该函数会阻塞,直到能够获取锁资源
参数说明互斥锁地址,属于地址传递
返回值成功返回0,失败返回错误码

📖 释放锁资源
函数原型int pthread_mutex_unlock(pthread_mutex_t *mutex);
头文件iostream
功能释放对互斥锁资源的拥有权
参数说明互斥锁变量的地址
返回值成功返回0,失败返回错误码

📖 销毁互斥锁
函数原型int pthread_mutex_destroy(pthread_mutex_t *mutex);
头文件iostream
功能销毁互斥锁
参数说明互斥锁变量的地址
返回值成功返回0,失败返回错误码

2.2、互斥锁的小练习

        可以看到我们线程1和线程2都是先抢占我们锁资源然后进行释放,这其中的机制依旧是时间片轮询上下文切换。

#include<iostream> #include<cstdio> #include<cstring> #include <unistd.h> using namespace std; //11、创建一个互斥锁 pthread_mutex_t mutex; //定义一个全局资源 int num = 520; //定义分支线程1 void *task1(void *arg){ while(1){ sleep(1); //临界资源 //33、获取锁资源 pthread_mutex_lock(&mutex); num -= 10; //线程1将临界资源减少10 printf("张三取了10,剩余%d\n", num); //44、释放锁资源 pthread_mutex_unlock(&mutex); } } //定义分支线程2 void *task2(void *arg){ while(1){ sleep(1); //33、获取锁资源 pthread_mutex_lock(&mutex); num -= 20; //线程1将临界资源减少10 printf("李四取了20,剩余%d\n", num); //44、释放锁资源 pthread_mutex_unlock(&mutex); } } /*****************************主线程****************************/ int main() { //22、初始化互斥锁,参数NULL表示让系统自动分配互斥锁属性 pthread_mutex_init(&mutex, NULL); //1、创建两个分支线程 pthread_t tid1,tid2; if(pthread_create(&tid1, NULL, task1, NULL) != 0){ printf("tid1 create error\n"); return -1; } if(pthread_create(&tid2, NULL, task2, NULL) != 0){ printf("tid2 create error\n"); return -1; } printf("主线程:tid1 = %#x, tid2 = %#x\n", tid1, tid2); //2、阻塞等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); //55、释放锁资源 pthread_mutex_destroy(&mutex); std::cout << "Hello, World!" << std::endl; return 0; }

三、线程同步之无名信号量

        线程同步:就是多个线程之间有先后顺序得执行,这样在访问临界资源时,就不会产生抢占现象了

        同步机制常用于生产者消费者模型:消费者任务要想执行,必须先执行生产者线程,多个任务有顺序执行

        无名信号量:本质上也是一个特殊的临界资源,内部维护了一个value值,当某个进行想要执行之前,先申请该无名信号量的value资源,如果value值大于0,则申请资源函数接触阻塞,继续执行后续操作。如果value值为0,则当前申请资源函数会处于阻塞状态,直到其他线程将该value值增加到大于0

3.1、无名信号量的相关API函数接口

 📖 创建无名信号量

        只需定义一个sem_t 类型的变量即可

sem_t sem;

 📖 初始化无名信号量
函数原型

int sem_init(sem_t *sem, int pshared, unsigned int value);

头文件semaphore.h
功能初始化无名信号量,最主要是初始化value值
参数说明

参数1:无名信号量的地址

参数2:判断进程还是线程的同步

               0:表示线程间同步

               非0:表示进程间同步,需要创建在共享内存段中

参数3:无名信号量的初始值

返回值成功返回0,失败返回-1并置位错误码

 📖 申请无名信号量的资源(P操作)
函数原型int sem_wait(sem_t *sem);
头文件semaphore.h
功能阻塞申请无名信号量中的资源,成功申请后,会将无名信号量的value进行减1操作,如果当前无名信号量的value为0,则阻塞
参数说明无名信号量的地址
返回值成功返回0,失败返回-1并置位错误码

 📖 释放无名信号量的资源(V操作)
函数原型int sem_post(sem_t *sem);
头文件semaphore.h
功能将无名信号量的value值增加1操作
参数说明无名信号量的地址
返回值成功返回0,失败返回-1并置位错误码

 📖 销毁无名信号量
函数原型int sem_destroy(sem_t *sem);
头文件semaphore.h
功能销毁无名信号量
参数说明无名信号量的地址
返回值成功返回0,失败返回-1并置位错误码

3.2、互斥锁的小练习

        我们现在再来看看同步在生产者消费者模型中的应用,当 进程2 想要执行之前,先申请无名信号量的value资源,如果value值大于0,则申请资源函数解除阻塞,并继续执行后续操作。如果value值为0,则当前申请资源函数会处于阻塞状态,直到 线程1 将该value值增加到大于0

#include<iostream> #include<cstdio> #include<cstring> #include<unistd.h> #include<semaphore.h> #include<pthread.h> sem_t sem; //创建生产者线程 void *task1(void *arg){ int num = 5; while(num--){ sleep(1); printf("我生产了一辆特斯拉\n"); //44、释放无名信号量资源 sem_post(&sem); } //退出线程 pthread_exit(NULL); } //创建消费者线程 void *task2(void *arg){ int num = 5; while(num--){ //33、申请无名信号量的资源 sem_wait(&sem); printf("我消费了一辆特斯拉,很开心\n"); } //退出线程 pthread_exit(NULL); } /*******************************主程序*************************/ int main() { //22、初始化无名信号量,第一个0表示用于线程间通信,第二个0表示初始值为0 sem_init(&sem, 0, 0); //1、创建两个分支线程 pthread_t tid1,tid2; if(pthread_create(&tid1, NULL, task1, NULL) != 0){ printf("tid1 create error\n"); return -1; } if(pthread_create(&tid2, NULL, task2, NULL) != 0){ printf("tid2 create error\n"); return -1; } printf("主线程:tid1 = %#x, tid2 = %#x\n", tid1, tid2); //2、阻塞等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); //55、销毁无名信号量 sem_destroy(&sem); return 0; }

四、线程同步之条件变量

        我们不难发现,如果我们只用互斥锁,那么我们的消费者和生产者只能是一对一的关系,但我们要想让一个生产者对应多个消费者用互斥锁就不行了,所以我们就要用到我们的条件变量了。

        条件变量本质上也是一个临界资源,他维护了一个队列,当消费者线程想要执行时,先进入队列中等待生产者的唤醒。执行完生产者,再由生产者唤醒在队列中的消费者,这样就完成了生产者和消费者之间的同步关系。

        但是,多个消费者在进入休眠队列的过程是互斥的,所以,在消费者准备进入休眠队列时,我们需要使用互斥锁来进行互斥操作。

4.1、条件变量的API函数接口

 📖 创建一个条件变量

        只需定义一个pthread_cond_t类型的全局变量即可

pthread_cond_t cond;

 📖 初始化条件变量
函数原型

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;     //静态初始化

头文件iostream
功能初始化条件变量
参数说明

参数1:条件变量的起始地址

参数2:条件变量的属性,一般填NULL

返回值

成功返回0,失败返回一个错误码


 📖 消费者线程进入等待队列
函数原型

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

头文件iostream
功能将线程放入休眠等待队列,等待其他线程的唤醒
参数说明

参数1:条件变量的地址

参数2:互斥锁,由于多个消费者线程进入等待队列时会产生竞态,为了解决竞态,需要使用一个互斥锁

返回值

成功返回0,失败返回错误码


 📖 生产者线程唤醒休眠队列中的任务
函数原型int pthread_cond_broadcast(pthread_cond_t *cond);int pthread_cond_signal(pthread_cond_t *cond);
头文件iostreamiostream
功能唤醒条件变量维护的队列中的所有消费者线程唤醒条件变量维护的队列中的第一个进入队列的消费者线程
参数说明条件变量的地址条件变量的地址
返回值

成功返回0,失败返回错误码

成功返回0,失败返回错误码

 📖 销毁条件变量
函数原型int pthread_cond_destroy(pthread_cond_t *cond);
头文件iostream
功能销毁一个条件变量
参数说明条件变量的地址
返回值

成功返回0,失败返回错误码


3.2、条件变量的小练习

        我们再以生产者消费者模型来看看我们的用法,我们既可以一个一个唤醒,也可以直接一次性将消费者全部唤醒。

#include<iostream> #include<unistd.h> #include<pthread.h> //11、定义一个条件变量 pthread_cond_t cond; //111、定义一个互斥锁 pthread_mutex_t mutex; //创建生产者线程 void *task1(void *arg) { /* int num = 3; while(num--) { sleep(1); printf("%#x:生产了一辆特斯拉\n", pthread_self()); //44、唤醒一个消费者进行消费 pthread_cond_signal(&cond); } */ sleep(3); printf("我生产了3辆特斯拉\n"); //44、唤醒所有消费者线程 pthread_cond_broadcast(&cond); //退出线程 pthread_exit(NULL); } //创建消费者线程 void *task2(void *arg) { //333、获取锁资源 pthread_mutex_lock(&mutex); //33、进入休眠队列,等待生产者的唤醒 pthread_cond_wait(&cond, &mutex); printf("%#x:消费了一辆特斯拉,很开心\n", pthread_self()); //444、释放锁资源 pthread_mutex_unlock(&mutex); //退出线程 pthread_exit(NULL); } int main() { //22、初始化条件变量 pthread_cond_init(&cond, NULL); //222、初始化互斥锁 pthread_mutex_init(&mutex, NULL); //1、创建两个分支线程 pthread_t tid1,tid2,tid3,tid4; if(pthread_create(&tid1, NULL, task1, NULL) != 0){ printf("tid1 create error\n"); return -1; } if(pthread_create(&tid2, NULL, task2, NULL) != 0){ printf("tid2 create error\n"); return -1; } if(pthread_create(&tid3, NULL, task2, NULL) != 0){ printf("tid3 create error\n"); return -1; } if(pthread_create(&tid4, NULL, task2, NULL) != 0){ printf("tid4 create error\n"); return -1; } printf("主线程:tid1 = %#x, tid2 = %#x, tid3 = %#x, tid4 = %#x\n", tid1, tid2, tid3, tid4); //2、阻塞等待线程结束 pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_join(tid3, NULL); pthread_join(tid4, NULL); //55、销毁条件变量 pthread_cond_destroy(&cond); ///555、销毁互斥锁 pthread_mutex_destroy(&mutex); return 0; }

C++11中的多线程

        C++11之后就支持线程支持库了,也支持线程创建、互斥锁、条件变量,线程支持库需要引入头文件 #include<thread>

一、线程相关常用操作

1.1、线程的创建

        C++线程支持库,本质是是面向对象的操作,可以使用构造函数完成。


1.2、线程体函数种类

  • 可以是任意类型的函数,不必要是 void * 类型参数也是void *类型
  • 可以是全局函数,也可以是类中成员函数当做线程体
  • 可以是仿函数当作线程体函数
  • 也可以是Lambda表达式当作线程体函数

1.3、线程号获取

this_thread::get_id()

        下述代码为我们展示了4种不同的线程体函数。

#include<iostream> #include <thread> using namespace std; /*****************第一个测试线程体*******************/ void ThreadFun_1(){ cout<<"ThreadFun_1 tid = "<< this_thread::get_id()<<endl; cout << "ThreadFun_1 test"<<endl; } /*****************第二个线程体测试**********************/ void ThreadFun_2(int num, string str){ //有参无返回值函数 cout<<"ThreadFun_2 tid = "<< this_thread::get_id()<<endl; cout<<"num = " << num << " str = " << str <<endl; } /*******************第三个线程体测试*********************/ class ThreadClass{ public: string name; int age; void ThreadClassFun(){ //类中成员函数作为线程体函数 cout<<"ThreadFun_3 tid = "<< this_thread::get_id()<<endl; cout << "name = "<<this->name<<" age = "<<age<<endl; } }; /**********************主程序**********************/ int main(int argc, const char *argv[]){ //主程序就是主线程 //创建第一个分支线程,使用无参函数完成线程体为无参无返回值函数 thread th1(ThreadFun_1); //创建第二个分支线程,并向线程体中传递数据 string name = "zpp"; thread th2(ThreadFun_2, 520, name); //此时就创建了一个分支线程,线程体函数 //可以直接向线程体传递参数,有多少可以传多少,无需使用结构体完成 //创建第三个分支线程,向线程体中传递一个类的成员函数 ThreadClass test; test.name = "zhangsan"; test.age = 18; thread th3(&ThreadClass::ThreadClassFun, &test); /************lambda表达式当作线程体:开发过程中用的比较多的************/ //创建第四个分支线程,将lambda表达式当作线程体函数 thread th4([](int key){ cout<<"ThreadFun_4 tid = "<< this_thread::get_id()<<endl; cout<<"key = "<<key<<endl; }, 999); //阻塞回收分支线程 th1.join(); th2.join(); th3.join(); th4.join(); return 0; }

1.4、线程号回收

  • 阻塞方式回收线程
th1.join();
  • 非阻塞方式回收线程
th1.detach();

        此处的阻塞非阻塞回收与我们上一篇讲的并无二异,说到底,这些类的实现本质也是在用我们上节课c语言的那些函数所做出来的。

#include<iostream> #include<thread> using namespace std; /*****************第一个测试线程体*******************/ void ThreadFun_1(){ // 无参无返回值 for (int i = 0; i < 10; i++){ cout << "ThreadFun_1 tid = " << this_thread::get_id() << endl; cout << "ThreadFun_1 test" << endl; //延时函数 this_thread::sleep_for(1s); //等待1秒时间 } } /**********************主程序**********************/ int main(int argc, const char *argv[]){ // 阻塞回收分支线程 //th1.join(); // 主程序就是主线程 // 创建第一个分支线程,使用无参函数完成线程体 thread th1(ThreadFun_1); // 此时就创建了一个分支线程,线程体函数为无参无返回值函数 th1.detach(); //将线程设置成分离态:主线程可以继续做自己其他事情 this_thread::sleep_for(20s); return 0; }

二、互斥锁的使用

        互斥锁本质上是完成将多个线程使用临界资源时,防止竞态

        我们需要在c++编程中需要引入头文件 #include<mutex>

2.1、常用函数

1、构造函数:创建一个互斥锁对象 2、 lock():上锁 3、 unlock():释放锁资源

2.2、lock_guard的使用

        但在这其中,我们的mutex互斥锁经常会与lock_guard进行一起使用,用于在其构造时自动获取锁,在析构时自动释放锁。使用 std::lock_guard 的好处是,当 std::lock_guard 对象离开其作用域时,会自动调用析构函数,该析构函数会释放锁。这确保了在任何情况下(包括由于异常等原因导致的提前退出),锁都会被正确释放,从而避免了忘记手动释放锁而导致的死锁问题

std::mutex myMutex; std::lock_guard<std::mutex> lock(myMutex);

2.3、代码演示

#include<iostream> #include<thread> #include<mutex> using namespace std; mutex mux; //实例化一个互斥锁 /*****************第一个测试线程体*******************/ void ThreadFun_1(){ // 无参无返回值 mux.lock(); //获取锁资源 cout<<"======================================"<<endl; cout<<"tid = "<<this_thread::get_id()<<endl; this_thread::sleep_for(1s); cout<<"**************************************"<<endl; mux.unlock(); //释放锁资源 } /**********************主程序**********************/ int main(int argc, const char *argv[]){ for(int i=0; i<10; i++){ thread th(ThreadFun_1); th.detach(); } //线程分离 this_thread::sleep_for(20s); std::cout << "Hello, World!" << std::endl; this_thread::sleep_for(20s); return 0; }

三、条件变量

        实现一个生产者对应多个消费者问题,需要引入头文件:#include<condition_variable>

2.1、常用函数

1、 构造函数:创建并初始化一个条件变量 2、 wait():将消费者线程放入等待队列中 3、 唤醒线程: cv.notify_one(); 唤醒一个线程 cv.notify_all(); 唤醒所有线程

2.2、代码演示

        不难看出与我们刚才所讲的同步机制中的条件变量逻辑是非常相似的。

#include<iostream> #include<thread> #include<mutex> #include<condition_variable> using namespace std; //定义一个条件变量 condition_variable cv; mutex mux; //条件变量头文件 //线程支持库头文件 //互斥锁 //用于防止竞态的互斥锁 //定义生产者线程 void ThreadWrite(){ for(int i=0; i<5; i++){ this_thread::sleep_for(2s); cout<<"我生产了一辆特斯拉"<<endl; cv.notify_one(); //通知一个线程可以消费了 //通知所有线程 //cv.notify_all(); } } //定义消费者线程 void ThreadRead(){ //提前先进入消费者队列 unique_lock<mutex> lock(mux); cv.wait(lock); cout<<"我消费了一辆特斯拉"<<endl; lock.unlock(); // 解锁 } int main(int argc, const char *argv[]) { //创建生产者线程 thread th1(ThreadWrite); //每隔两秒时间生产一辆特斯拉 //创建多个消费之 for(int i=0; i<5; i++){ thread th2(ThreadRead); th2.detach(); } th1.join(); //阻塞回收线程 return 0; }

结语

        当你掌握了“红绿灯”和“独木桥”的原理,你就已经跨过了多线程编程最危险的那道门槛。接下来,就是去享受多核 CPU 带来的速度激情吧!

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!


---⭐️封面自取⭐️---

Read more

利用 Python 爬虫进行跨境电商数据采集

利用 Python 爬虫进行跨境电商数据采集

* 1 引言 * 2 代理IP的优势 * 3 获取代理IP账号 * 4 爬取实战案例---(某电商网站爬取) * 4.1 网站分析 * 4.2 编写代码 * 4.3 优化代码 * 5 总结 1 引言 在数字化时代,数据作为核心资源蕴含重要价值,网络爬虫成为企业洞察市场趋势、学术研究探索未知领域的重要技术手段。然而爬虫实践中常面临技术挑战,例如某电商企业通过爬虫获取竞品数据时,因高频请求触发目标平台 IP 封锁机制导致采集中断。IP 代理在网络爬虫中发挥关键作用:通过分布式请求分散访问压力,可规避单 IP 高频访问限制并突破地域内容获取限制;同时能隐藏真实 IP 地址降低法律风险,模拟多用户行为特征优化反爬虫策略,有效平衡数据获取需求与网络访问规则。这种技术工具通过突破技术限制、提升采集效率、保障数据安全等多维价值,成为网络爬虫体系中的重要组成部分。本文将介绍代理IP在网络爬虫中的重要性,并结合实际应用。 2 代理IP的优势

By Ne0inhk
解密链表环的起点:LeetCode 142 题

解密链表环的起点:LeetCode 142 题

解密链表环的起点:LeetCode 142 题 * 视频地址 * 🌟 引言 * 🔍 问题描述 * 🧠 解题思路回顾 * 快慢指针算法 * 数学原理 * 💻 C++代码实现 * 🛠 代码解析 * 数据结构定义 * 算法实现细节 * 🚀 性能分析 * 🐞 常见问题与调试 * 常见错误 * 调试技巧 * 📊 复杂度对比表 * 🌈 总结 视频地址 因为想更好的为大佬服务,制作了同步视频,这是Bilibili的视频地址 🌟 引言 链表环检测问题在C++中同样是一个经典面试题。本文将用C++实现LeetCode 142题"环形链表II"的解决方案,深入讲解快慢指针算法的原理和实现细节。 🔍 问题描述 给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 nullptr。 🧠 解题思路回顾 快慢指针算法 1. 使用两个指针:slow每次走一步,fast每次走两步 2.

By Ne0inhk

Python GUI开发革命:CustomTkinter完整指南

Python GUI开发革命:CustomTkinter完整指南 【免费下载链接】CustomTkinterA modern and customizable python UI-library based on Tkinter 项目地址: https://gitcode.com/gh_mirrors/cu/CustomTkinter CustomTkinter是一个基于Python Tkinter的现代化UI库,为传统Tkinter注入了全新的生命力。它提供了一系列美观、现代化且完全可定制的组件,支持自动适配系统外观模式和高DPI缩放,让Python桌面应用开发变得简单而优雅。 为什么选择CustomTkinter? 在Python GUI开发领域,Tkinter虽然易用但界面陈旧,而PyQt等库学习曲线陡峭。CustomTkinter完美解决了这一痛点——它保留了Tkinter的简单语法,同时提供了媲美现代桌面应用的视觉效果。无论你是初学者还是经验丰富的开发者,都能在几分钟内创建出专业级的界面。 惊艳界面效果展示 CustomTkinter能够创建出令人惊叹的现代化

By Ne0inhk
Python调用PubMed API实战:构建医学文献搜索系统【附完整代码】

Python调用PubMed API实战:构建医学文献搜索系统【附完整代码】

🎯 背景与需求 作为医疗健康领域的开发者,我们经常需要从PubMed检索大量医学文献。手动搜索效率低下,而构建自动化的文献检索系统成为刚需。 典型应用场景: * 🏥 临床决策支持系统需要快速检索相关文献 * 📊 科研数据分析需要批量获取文献元数据 * 📝 医学知识库构建需要持续更新文献信息 * 🤖 AI医疗助手需要实时检索最新研究进展 核心技术挑战: 1. PubMed API的调用规范和限流策略(3 req/s vs 10 req/s) 2. XML/JSON数据格式的解析和结构化存储 3. 批量检索时的性能优化和错误处理 4. 医学术语的标准化和中英文映射 💡 技术方案选型 在调用PubMed API时,我们有三种主流技术方案: 方案对比 方案技术栈优点缺点适用场景方案1:原生HTTP请求requests + XML解析轻量灵活,完全自主控制需手动处理XML,限流逻辑复杂学习研究、定制化需求方案2:Biopython库Bio.Entrez模块封装完善,自动限流依赖较重,更新较慢生物信息学项目方案3:集成服务第三方API(如supp

By Ne0inhk