Linux线程控制

443c51730ff34b22adc24222e1b63d49.jpeg

目录

一、线程的简单控制

1.多线程并行

2.线程结束

3.线程等待

(1)系统调用

(2)返回值

4.线程取消

5.线程分离

二、C++多线程小组件

三、线程库TCB

1.tid

2.局部储存


 

一、线程的简单控制

1.多线程并行

我们之前学过pthread_create可以创建线程,而且要使用原生线程库必须在编译时加上-lpthread。

我们用这个接口一次性创建五个进程。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;#define NUM 5void* start_routine(void* args)
{string s;s += "running->";s += (char*)args;while(1){cout << "running->" << s;sleep(1);}
}int main()
{for(int i = 0; i<NUM; ++i){pthread_t tid;char buffer[64];snprintf(buffer, sizeof(buffer), "new thread:%d\n",i+1);pthread_create(&tid, nullptr, start_routine, (void*)buffer);}while(1){cout << "main pthread running.\n";sleep(1);}return 0;
}

我们运行三次程序:

fdc90e7b38a2414a85e05f0faaafd9cd.png

我们可能会想,按照代码的逻辑,我们想要看到的结果是:

running->new thread:1

running->new thread:2

running->new thread:3

running->new thread:4

running->new thread:5

main pthread running.

可是为什么运行两次,main pthread running.打印的位置不断变化,而且后面的数字都是5。

首先解决第一个问题,main pthread running.打印的位置为什么不断变化?

这是因为线程的运行顺序是由调度器决定的,各个线程的执行进度不同,主线程不一定在最后才能打印main pthread running.

第二个问题,为什么后面的数字都是5?

这是因为,三次执行中主线程执行顺序靠前。主线程将5次创建线程的代码跑完了,每循环一次sprintf就会将上一次的buffer内容覆盖掉,循环5次,i+1变为5。此时,又因为包括主线程的6个线程共享同一个地址空间,所以它们同时能看到buffer。五个新线程通过参数传递的地址找到buffer并打印出来。

既然你说buffer作为缓冲区被覆盖掉了,那我们为每一个线程构建一个自己的缓冲区不就解决问题了?

这样的思想是没有问题的,我们可以使用一个pthread_data类管理线程,内部包含线程标识符tid和缓冲区buffer。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;while(1){cout << s;sleep(1);}
}int main()
{for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);}while(1){cout << "main pthread running.\n";sleep(1);}return 0;
}

虽然确实1到5都出现了,但是因为我们控制不了线程运行的顺序,所以还是不能保证按数字顺序打印。

6d9f12067d11497f990c8b96598a6ed9.png

2.线程结束

线程执行的函数有一个void*返回值,我们返回空指针就能终止该线程。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 5)return nullptr;}
}int main()
{for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)(pd->buffer));}while(1){cout << "main pthread running.\n";sleep(1);}return 0;
}

五个线程都能正常退出,最后只剩下主线程运行。

4f8fa1af7301499bae9b46f139f30061.png

POSIX线程库也提供了一个接口用于结束线程

void pthread_exit(void* retval);

头文件:pthread.h

功能:终止当前线程。

参数:void* retval是线程的结束信息,设置为空指针就可以了。

void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 5)pthread_exit(nullptr);}
}

将return nullptr替换成该函数也能实现线程退出。

3.线程等待

(1)系统调用

和进程一样,线程在执行完毕时如果task_struct结构体不回收,就会导致内存泄漏(类似未被回收的僵尸进程)。所以我们需要使用pthread_join函数将线程加入等待队列,加入等待队列的线程会被回收,但是回收的现象我们是看不到的。

int pthread_join(pthread_t thread, void** retval);

头文件:pthread.h

功能:将标识符为tid的线程加入等待队列。

参数:pthread_t thread是需要等待的线程标识符,void** retval是线程结束返回的信息,是一个输出型参数。

返回值:等待成功返回0,等待失败返回错误码。

实际上,这个加入等待队列和我们之前的进程等待现象差不多,我们写一段代码,让五个新线程运行3秒终止,主线程负责回收五个新线程,观察一下结果。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 3)return nullptr;}
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){pthread_join(e->tid, nullptr);printf("wait success:%d\n", e->tid);}return 0;
}

运行结果:

1b82015e855a4b78b4695847e1711fd8.png

主线程在执行至线程等待代码时,主线程并不会继续往下执行。这也证明了,线程等待是阻塞式等待。

(2)返回值

在之前我们写的start_routine线程函数有一个void*的返回值,它可以返回线程退出相关的信息。

比如说,下图的最后一行就可以以void*的格式返回1。

5d48364d895947d1b89062791473cb59.png

不过这里有个问题,它虽然能返回结束的信息,但是这个变量要怎么让主线程接收到呢?

pthread_join函数有一个输出型参数void** retval,我们在主线程内定义一个void*类型的ret指针变量。当一个线程被回收的时候,将ret传参,它的返回值就会被放进这个ret里。

又因为返回值类型为void*,如果只将ret传参,那么只会将ret的临时拷贝改变。所以参数必须为void**,这也是使用二级指针的原因。

我让每一个线程都返回1,运行代码。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 3)break;}return (void*)1;
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new  pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){void* ret = nullptr;pthread_join(e->tid, &ret);printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);}return 0;
}

运行结果:

ed8c2c87bac8407aaa89e3fd093a1fb7.png

把return换成只有一个参数void* retval的pthread_exit,它也可以将结果输出到变量中。

04afb8e6dfb74c579c227feef585a3e7.png

结果与上面的一致。

如果想要让每一个线程都返回各自的错误码,我们可以在pthread_data类中增加一个储存返回值的变量。

我下面就修改代码让每一个线程返回自己的编号。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5class pthread_data
{
public:pthread_t tid;char buffer[64];int num;
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += "running->";s += p->buffer;int j = 1;while(1){cout << s;sleep(1);if(j++ == 3)break;}//return (void*)1;pthread_exit((void*)(p->num));
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pd->num = i+1;pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){void* ret = nullptr;pthread_join(e->tid, &ret);printf("wait success:%d,exit code:%d\n", e->tid, (long long)ret);}return 0;
}

运行结果:

ec3268040b0d4c7fae4a2aeecd4371e5.png

4.线程取消

线程取消也是终止线程的一种方式,可使用下面的系统调用。

int pthread_cancel(pthread_t thread);

头文件:pthread.h

功能:取消标识符为thread的线程。

参数:pthread_t thread是需要取消的线程标识符。

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

我们创建10个线程,在中途取消前五个线程,查看具体的现象。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 10class pthread_data
{
public:pthread_t tid;char buffer[64];int num;
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;int j = 1;while(1){sleep(1);if(j++ == 3)break;}//return (void*)1;pthread_exit((void*)(p->num));
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "new thread:%d\n",i+1);pd->num = i+1;pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(int i = 0; i<NUM; ++i){printf("new thread:%d,address:%p\n", i+1, vpd[i]);}for(int i = 0; i<vpd.size()/2; ++i){pthread_cancel(vpd[i]->tid);}for(auto& e : vpd){void* ret = nullptr;pthread_join(e->tid, &ret);printf("join success:%d,exit code:%d\n", e->tid, (long long)ret);}return 0;
}

运行结果:

ec5c5145ddc94211a995cc2ad0ea4521.png

对于取消的前五个线程,等待会直接成功,返回值是-1。

未被取消的后五个线程,仍然阻塞等待,等待成功后返回的是设定的线程编号。

如果一个线程是被取消结束的,退出码就是-1,是一个宏PTHREAD_CANCELED。

5.线程分离

默认情况下,在线程退出后,需要使用pthread_join将它加入等待队列,否则就会造成内存泄漏。

但是主线程只能阻塞式线程,阻塞时主线程只能干等着。而且我们有时根本不关心线程的返回值,那阻塞式等待就是巨大的负担。

那么,能不能模仿之前的轮询检测让主线程也继续干活吗?

可以,我们可以将需要释放的线程变为分离状态。我们将一个进程的所有线程做成一个组,如果将一个进程移出这个组,我们就说该线程处于分离状态。此时,主线程不用再关心该线程的状态,它会由系统自动释放。

我在这里也在说明一下,只要是让线程加入等待队列,那就必须要阻塞式等待;只要是分离状态,就不能再加入等待队列。所以说,可加入阻塞队列和分离状态是相互矛盾的,这也解释了为什么不能轮询非阻塞等待。

int pthread_detach(pthread_t thread);

头文件:pthread.h

功能:设置标识符为thread的线程分离状态。

参数:pthread_t thread是需要分离的线程标识符。

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

这个函数既可以分离线程组内的其他线程,也可以分离自己,但分离自己就需要用到自己的线程标识符tid,线程自己的tid可以由下面的函数获取。

pthread_t pthread_self(void);

头文件:pthread.h

功能:获取线程自己的tid。

返回值:返回自己的tid。

我们创建一个新线程,让新进程在第一步就分离执行,观察主线程能否回收它。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;#define NUM 10void* start_routine(void* args)
{pthread_detach(pthread_self());string s = static_cast<const char*>(args);for(int i = 0; i<5; ++i){cout << s;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;int n = pthread_join(tid, nullptr);cout << "error: " << n << ":" << strerror(n) << endl;return 0;
}

线程只要分离,主线程就管不了它了,而且我们发现确实不能回收该分离的线程了,与预期效果一致。

09190419c3034f939c202bdeeab3b90a.png

我们把start_routine的前两个语句调换位置。让线程先去构造string对象,然后将其分离。

e0e2659fddf34973be4e0386dd3909c2.png

此时运行观察,我们发现主线程把新线程回收了。这又是怎么回事呢?

4e7c935cfe9c413ab20fa4c926fdc571.png

这是因为主线程优先运行,在新线程开始运行时,主线程已经在阻塞等待新线程了,这样设置分离也就没有意义了。

既然在新线程中分离线程并不保险,我们就将分离操作全部放在主线程中。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;#define NUM 10void* start_routine(void* args)
{string s = static_cast<const char*>(args);  for(int i = 0; i<5; ++i){cout << s;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, start_routine, (void*)"new thread running\n");//在主线程分离新线程pthread_detach(tid);cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;int n = pthread_join(tid, nullptr);cout << "error: " << n << ":" << strerror(n) << endl;return 0;
}

最终结果就不会出错了。

15b63b2798bb4e42a652c67eee091933.png

二、C++多线程小组件

既然我们也学习了一些系统调用了,我们可以试着写一个小组件。这个小组件将线程的创建、执行和等待等都封装起来。我们在程序中指定一个函数,让多个线程不断地执行该函数。

thread_handler.h

#include<pthread.h>
#include<assert.h>
#define NUM 64
class Thread;//前置声明class Context
{
public:Context():_this(nullptr),_args(nullptr){}Thread* _this;//线程this指针void* _args;//pthread_create需要传递的args
};class Thread
{typedef std::function<void*(void*)> func_t;
public://构造函数创建线程Thread(func_t func, void* args, int number = 0):_func(func),_args(args){//对线程进行规范化命名char buffer[NUM];snprintf(buffer, sizeof(buffer), "thread-%d", number);_name = buffer;//将线程信息保存到Context变量Context* p = new Context;p->_this = this;p->_args = args;int n = pthread_create(&_tid, nullptr, start_routine, (void*)p);}//运行函数void* run(void* args){return _func(args);}//线程执行start_routine,但是它如果是普通成员函数,则参数多了一个this指针//将其变为static成员函数就能消除this指针//但同时我们又需要执行线程内部成员_func函数,此时既不能访问this指针的内容,之前创建线程传递的void* args也传递不到//我们再次构建一个类Context,将这些内容包含进去,将它们通过一个类指针传过去就好了static void* start_routine(void* args){Context* cp = static_cast<Context*>(args);void* ret = cp->_this->run(cp->_args);}//将加入等待队列void join(){int n = pthread_join(this->_tid, nullptr);assert(n == 0);}private:std::string _name;pthread_t _tid;func_t _func; void* _args;
};

test.cc

#include<iostream>
#include<memory>
#include<unistd.h>
#include"thread_handler.h"
using namespace std;void* handler(void* args)
{string s = "new thread:";s += static_cast<const char*>(args);s += '\n';while(1){cout << s;sleep(1);}
}int main()
{unique_ptr<Thread> t1(new Thread(handler, (void*)"thread1", 1));unique_ptr<Thread> t2(new Thread(handler, (void*)"thread2", 2));unique_ptr<Thread> t3(new Thread(handler, (void*)"thread3", 3));t1->join();t2->join();t3->join();return 0;
}

可以看到三个线程在不停地执行我们的函数。

三、线程库TCB

1.tid

我们编写代码让新线程打印自己的 tid,主线程打印自己和新线程的tid。

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;void* start_routine(void* args)
{string s = (char*)args;while(1){cout << s << " tid:0x" << pthread_self() << endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");assert(n == 0);while(1){printf("main thread tid:0x%p,new thread tid:0x%p\n", pthread_self(), tid);sleep(1);}return 0;
}

我们发现主线程和新线程打印的新线程tid相同,而且它们是一个地址。

db4bff194bab4e5a941cbdd4c7188084.png

我们一开始就强调,Linux内核中没有线程概念,只有轻量级进程的PCB,更没有TCB这样的数据结构。我们宏观看到的线程是系统的通过clone创造出的轻量级进程,只是这些轻量级进程共用了地址空间等资源。然后这些轻量级进程再通过POSIX原生线程库模拟出我们看到的线程。

既然原生线程库可以保证大量的线程同时工作,那么原生线程库中就必定有能管理这些线程的数据结构,换句话说,TCB结构一定在线程库中。

由于存储线程属性的TCB不在内核中,所以Linux的TCB也叫做用户级线程。

结论:Linux内核只负责调度执行流,用户关心的线程及其属性都由原生线程库维护。Linux的用户级线程和内核轻量级进程都保存着线程的属性,二者基本做到一一对应。

我们将视线转向地址空间:

原生线程库加载到内存后,页表会对虚拟地址空间和其内存物理地址建立映射关系。根据地址空间的分区,线程库会映射在虚拟地址空间中的共享区中,其中TCB等结构和数据当然也在共享区。

看下面的图片

3b7b3217cda041a2b5d83fe3e6a90125.png

我这里严谨一点:

由于局部储存和栈结构空间都只是在TCB存储指针,虽然这些都是TCB拥有的数据,但它们的内容肯定不会直接保存在TCB里,所以我就把它们和TCB分开了。

你要是认为全部的数据就是TCB也可以接受。

我们能得到以下结论:

  • 共享区内的每个线程都有自己的TCB、局部储存和独立栈,它们由载入内存的原生线程库维护。
  • 主线程的栈就是地址空间的栈区,而新线程的独立栈结构都在共享区,所以线程间才能有独立栈结构。
  • tid是指针类型,TCB在地址空间中的虚拟地址就是tid的值。

2.局部储存

我们知道同一进程的线程相互共用地址空间和页表,所以它们都可以使用一个全局变量。

可如果新线程仍然想用这个变量名,但又不想影响其他线程。这时需要让这个全局变量在每个进程中独立使用,此时就可以使用线程的局部存储属性了。

不使用局部储存6e5f86e6d09b493e96db80a75f3bdb92.png

 主线程和新线程都用了同一个g_val变量。

ccb69a3de0594cbbb61ea5f1107bdb3b.png

在int g_val = 0;前面加上__thread就能将该变量独立出去。

#include<iostream>
#include<pthread.h>
#include<assert.h>
#include<unistd.h>
using namespace std;__thread int g_val = 0;void* start_routine(void* args)
{string s = (char*)args;while(1){++g_val;printf("new thread g_val:%d,address:%p\n", g_val, &g_val);sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, start_routine, (void*)"new pthread");assert(n == 0);while(1){++g_val;printf("main thread g_val:%d,address:%p\n", g_val, &g_val);sleep(1);}return 0;
}

主线程和新线程使用的不再是同一个g_val变量,互相之间也不会受到影响。

10f71944cba14102a43dddc99536b0cf.png

结论:在全局变量或static变量前添加 __thread,可以让每个线程的TCB中都有一份独立的变量,不会互相影响。

 

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

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

相关文章

Windows SQLYog连接不上VMbox Ubuntu2204 的Mysql解决方法

Windows SQLYog连接不上VMbox Ubuntu2204 的Mysql解决方法 解决方法&#xff1a; 1、先检查以下mysql的端口状态 netstat -anp|grep mysql如果显示127.0.0.1:3306 则说明需要修改&#xff0c;若为: : :3306&#xff0c;则不用。 在**/etc/mysql/mysql.conf.d/mysqld.cnf**&am…

MySQL内置函数

文章目录 MySQL内置函数1. 日期函数1.1 用法演示(1) 获得年月日 - current_date()(2) 获得时分秒 - current_time()(3) 获得时间戳 - current_timestamp()(4) 获得当前时间- now()(5) 获取datetime参数的日期部分 - date(datetime)(6) 在日期的基础上加时间 - date_add(date, i…

JSX底层渲染机制

JSX底层渲染机制 一,.步骤 1.把我们写的jsx语法编译为虚拟DOM【virtualDOM】 虚拟DOM对象&#xff1a;框架自己内部构建的一套对象体系&#xff08;对象的相关成员都是React内部绑定的&#xff09;&#xff0c;基于这些属性描述出我们所构建视图中的DOM接的相关特征 1基于ba…

Linux学习之逻辑卷LVM用途和创建

理论基础 Linux文件系统建立在逻辑卷上&#xff0c;逻辑卷建立在物理卷上。 物理卷处于LVM中的最底层&#xff0c;可以将其理解为物理硬盘、硬盘分区或者RAID磁盘阵列&#xff0c;这都可以。卷组建立在物理卷之上&#xff0c;一个卷组可以包含多个物理卷&#xff0c;而且在卷组…

CSS中如何实现元素的渐变背景(Gradient Background)效果?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ CSS 渐变背景效果⭐ 线性渐变背景⭐ 径向渐变背景⭐ 添加到元素的样式⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&…

安全基础 --- https详解(02)、cookie和session、同源和跨域

https详解&#xff08;02&#xff09;--- 数据包扩展 Request --- 请求数据包Response --- 返回数据包 若出现代理则如下图&#xff1a; Proxy --- 代理服务器 &#xff08;1&#xff09;http和https的区别 http明文传输&#xff0c;数据未加密&#xff1b;http页面响应速度…

FreeSWITCH 1.10.10 简单图形化界面3 - 阿里云NAT设置

FreeSWITCH 1.10.10 简单图形化界面3 - 阿里云NAT设置 0、 界面预览1、 查看IP地址2、 修改协议配置3、 开放阿里云安全组4、 设置ACL5、 设置协议中ACL&#xff0c;让PBX匹配内外网6、 重新加载SIP模块7、 查看状态8、 测试一下 0、 界面预览 http://myfs.f3322.net:8020/ 用…

2023年腾讯云优惠券(代金券)领取方法整理汇总

腾讯云优惠券是腾讯云为了吸引用户而推出的一种优惠凭证&#xff0c;领券之后新购、续费、升级腾讯云的相关产品可以享受优惠&#xff0c;从而节省一点的费用&#xff0c;下面给大家分享腾讯云优惠券领取的几种方法。 一、腾讯云官网领券页面领取 腾讯云官网经常推出各种优惠活…

软件测试/测试开发丨Selenium 高级定位 Xpath

点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接&#xff1a;https://ceshiren.com/t/topic/27036 一、xpath 基本概念 XPATH是一门在XML文档中查找信息的语言 XPATH使用路径表达式在XML文档中进行导航 XPATH的应用非常广泛&#xff0c;可以用于UI自…

Unity3D 连接 SQLite 作为数据库基础功能【详细图文教程】

一、简单介绍一下SQLite的优势&#xff08;来自ChatGPT&#xff09; 轻量级: SQLite是一个嵌入式数据库引擎&#xff0c;它的库文件非常小巧&#xff0c;没有独立的服务器进程&#xff0c;适用于嵌入到其他应用程序中&#xff0c;对于轻量级的项目或移动应用程序非常适用。零配…

基于java swing和mysql实现的电影票购票管理系统(源码+数据库+运行指导视频)

一、项目简介 本项目是一套基于java swing和mysql实现的电影票购票管理系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都…

金融帝国实验室(Capitalism Lab)官方正版游戏『2023秋季特卖』

「金融帝国实验室」&#xff08;Capitalism Lab&#xff09;Enlight 官方正版游戏「2023秋季特卖」 ■时间&#xff1a;2023.09.01&#xff5e;2023.10.15 ■游戏开发商&#xff1a;Enlight Software Ltd. 请您认准以下官方正版游戏购买链接&#xff1a;支持“支付宝&a…

环境安装:rpm安装jdk上线项目

Tomcat安装 解析域名 购买域名并配置 安装Docker yum 卸载以前装过的docker

Orchestrator介绍三 命令行工具

Orchestrator-client orchestrator 支持两种方式通过命令行操作&#xff1a; 一种是 通过命令 orchestrator&#xff1a; 需要在服务器上安装 orchestrator&#xff0c;但是可以不作为服务启动。 需要配置orchestrator的文件&#xff0c;以便能够连接后端数据库 一种是通过…

Navicat使用HTTP通道服务器进行连接mysql数据库(超简单三分钟完成),centos安装nginx和php,docker安装nginx+php合并版

序言 因为数据库服务器在外网是不能直接连接访问的&#xff0c;但是可以访问网站&#xff0c;网站后台就能访问数据库&#xff0c;所以在此之前&#xff0c;访问数据库的数据是一件非常麻烦的事情&#xff0c;在平时和运维的交流中发现&#xff0c;他们会使用ssh通道进行连接访…

C++的基类和派生类构造函数

基类的成员函数可以被继承&#xff0c;可以通过派生类的对象访问&#xff0c;但这仅仅指的是普通的成员函数&#xff0c;类的构造函数不能被继承。构造函数不能被继承是有道理的&#xff0c;因为即使继承了&#xff0c;它的名字和派生类的名字也不一样&#xff0c;不能成为派生…

C#,数值计算——Midinf的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public class Midinf : Midpnt { public new double func(double x) { return funk.funk(1.0 / x) / (x * x); } public Midinf(UniVarRealValueFun funcc, double aa,…

查询优化器内核剖析第一篇

SQL Server 的查询优化器是一个基于成本的优化器。它为一个给定的查询分析出很多的候 选的查询计划&#xff0c;并且估算每个候选计划的成本&#xff0c;从而选择一个成本最低的计划进行执行。实际上&#xff0c; 因为查询优化器不可能对每一个产生的候选计划进行优化&#xff…

IDEA集成Git相关操作知识(pull、push、clone)

一&#xff1a;集成git 1&#xff1a;初始化git&#xff08;新版本默认初始化&#xff09; 老版本若没有&#xff0c;点击VCS&#xff0c;选中import into Version Controller中的Create git Repository(创建git仓库)&#xff0c;同理即可出现git符号。 也可查看源文件夹有没有…

【力扣每日一题】2023.8.30 到家的最少跳跃次数

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一只跳蚤&#xff0c;我们可以操控它前跳 a 格或是后跳 b 格&#xff0c;不能跳到小于0的位置&#xff0c;有一些被禁止的点不…