当前位置: 首页 > news >正文

【linux】线程控制

线程控制

  • 1.创建线程
  • 2.线程终止
  • 3.线程等待
  • 4.线程分离
  • 5.对线程的简单封装

在这里插入图片描述

喜欢的点赞,收藏,关注一下把!在这里插入图片描述

进程概念上篇文章已经讲完了,下面我们就来说说线程控制。

我们使用的接口是pthread线程库,也叫做原生线程库给我们提供的,这个库遵守POSIX标准的,跟我们System V是相对应的一种标准。

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

1.创建线程

pthread_create 创建一个新的线程

在这里插入图片描述
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

这个接口线程概念哪里我们已经用过。创建了一个线程。
今天我们想创建多个线程,并给每个线程都写上编号。

#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstdio>using namespace std;void* start_rountine(void* args)
{//安全的进行强制类型转化string name=static_cast<const char*>(args);while(true){cout<<"new thread create success, name: "<<name<<endl;sleep(1);}
}int main()
{#define NUM 10for(int i=0;i<NUM;++i){pthread_t id;//pthread_create(&id,nullptr,start_rountine,(void*)"thread new"); char namebuffer[64];snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);pthread_create(&id,nullptr,start_rountine,namebuffer); }while(true){cout<<"new thread create success, name: main thread"<<endl;sleep(1);}return 0;
}

在这里插入图片描述

    for(int i=0;i<NUM;++i){pthread_t id;char namebuffer[64];snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);pthread_create(&id,nullptr,start_rountine,namebuffer); //这里sleep一秒,再看一下运行结果sleep(1);}

在这里插入图片描述
为什么不加sleep,线程编号都变成9了呢?
在这里插入图片描述
因为无法保证谁先运行,所以在运行时可能出现一些奇怪的情况。

就如上面我们创建线程时,有可能这个线程创建出来了,但是还没来得及执行后序代码,而优先跑的主线程,主线程上来之后直接对缓冲区写入,而上一个线程只是把缓冲区地址给过去了,但缓冲区本身被下一次格式化显示所覆盖了。所以最终到底线程编号都是9。

所以如果想创建一批线程这种写法不太对的。
正确写法如下:


#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
#include<cstdio>
#include<vector>using namespace std;struct ThreadDate
{pthread_t tid;char namebuffer[64];
};void* start_rountine(void* args)//这个是传值传参,形参是实参的拷贝
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=10;while(cnt){//该函数拿到之后,解引用就可以访问到了cout<<"new thread create success, name: "<<td->namebuffer<<" cnt: "<<cnt-- <<endl;sleep(1);}return nullptr;
}int main()  
{#define NUM 10for(int i=0;i<NUM;++i){//每一次都会新创建一个ThreadDate对象ThreadDate* td=new ThreadDate();snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);//每个线程都调用start_rouytine,都会把每次新创建对象的地址传给该函数pthread_create(&td->tid,nullptr,start_rountine,td); //new出现新对象每个都是独立的,不会互相影响的}while(true){cout<<"new thread create success, name: main thread"<<endl;sleep(1);}return 0;
}

在这里插入图片描述
为了后序方便处理,因此还可以添加一个vector对象

int main()  
{vector<ThreadDate*> threads;
#define NUM 10for(int i=0;i<NUM;++i){ThreadDate* td=new ThreadDate();snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);pthread_create(&td->tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了threads.push_back(td);}for(auto& iter:threads){cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;}while(true){cout<<"new thread create success, name: main thread"<<endl;sleep(1);}return 0;
}

在这里插入图片描述

那我现在就有几个问题了。
1.start_routine,现在是被几个线程执行的呢?
在这里插入图片描述
回答:10个
那这个函数现在是什么状态?
回答:重入,信号时谈到一个函数被多个执行流访问,这个函数就是可重入状态。

2.该函数是可重入函数吗?
回答:是的

3.start_routine内定义的变量,多个执行流进来,会不会互相影响?
在这里插入图片描述
回答:不会,
如何证明?

void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=10;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);}return nullptr;
}

在这里插入图片描述
由运行结果说明。

在函数内定义的变量,都叫做局部变量,具有临时性,这是语言上的概念今天依旧适用。在多线程情况下,也没有问题。通过这个概念我们也证明了一件事情:其实每一个线程都有自己独立的栈结构

在哪呢?下面说。

2.线程终止

进程终止有三种方式,我们先说两种,然后把线程等待说一说,下面再说第三种。

return

线程函数结束,return的时候,线程就算终止了,或者在任意位置return。
关于返回值void*,下个话题再说。

以前我们也用exit终止过进程,这里可以用来终止线程吗?

void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=10;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);exit(1);}return nullptr;
}

在这里插入图片描述
不能,因为exit是终止进程的!任何一个执行流调用exit都会让整个进程退出,

pthread_exit

那一个线程调用这个函数就终止哪一个线程。

在这里插入图片描述
参数先不管,等会说。

void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=10;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);pthread_exit(nullptr);}return nullptr;
}

在这里插入图片描述
由此说明每一个线程调用pthread_exit都把自己终止了,并不影响主线程

这个调用也可以放在return的位置,都是一样的。

void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=10;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);}pthread_exit(nullptr);
}

注意到不管是return,还是pthread_exit的参数,返回类型都是void*,有返回值,如果我想拿到这个返回值,该怎么办呢?

这里我们不得不先谈线程等待的问题

3.线程等待

线程也是要被等待的。
如果不等待会有什么问题呢?

以前说过僵尸进程,子进程退出了,你不wait等待,子进程是僵尸状态,从而造成内存泄漏。那么在多线程这里,每一个线程在退出时,它的PCB并没有被释放,如果不等待,会造成类型僵尸进程的问题-----内存泄漏

所以线程也必须要被等待,目的有两个
1.获取新线程的退出信息(可以不关心,但还是要等)
2.回收新线程对应的PCB等内核资源,防止内存泄漏(暂时无法查看)

pthread_join

等待线程结束
在这里插入图片描述
thread:线程ID(你等哪一个线程)
retval:void**?一个void*就很难理解了。这是什么意思呢?

返回值:成功返回0;失败返回错误码

我们先使用这个函数进行等待再说。

int main()  
{vector<ThreadDate*> threads;
#define NUM 10for(int i=0;i<NUM;++i){ThreadDate* td=new ThreadDate();snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);pthread_create(&td->tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了threads.push_back(td);}for(auto& iter:threads){cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;}for(auto& iter:threads){//阻塞式的等待每一个线程int n=pthread_join(iter->tid,nullptr);assert(n == 0);(void)n;cout<<"join : "<<iter->namebuffer<<" success"<<endl;delete iter;}//这里就可以看出主线程式阻塞式的等待,全部等待成功,才打印这句话cout<<"main thread quit!!"<<endl;return 0;
}

在这里插入图片描述
要等待的时候设置一下join,操作系统就会自动帮我收回曾经为线程所创建的轻量级进程相关的资源。

现在谈一谈线程退出返回值问题。
我们发现线程退出,用return返回时它的类型是void*,用pthread_exit需要我们传入一个线程的参数类型也是void*,而我们想获得线程执行完的结果,对我们来说也可以通过join获得它,当然也可以回收它。

我们看到join的第二个参数,void** 也和void有关,难道都是巧合吗?
在这里插入图片描述

我们主线程join新线程,第二个参数是void** ,到底是什么意思呢?

在这里插入图片描述
你想作为输出型参数把结果拿回去,而返回值可是void*,你想把结果带出去,就必须是void**

下面我们演示一下。

struct ThreadDate
{int number;pthread_t tid;char namebuffer[64];
};void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=5;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);}return (void*)td->number;//传值返回
}

这里返回可能会有warning,因为整数4个字节,而linux默认release编译,指针大小8字节,但并不影响。
那么请问,这个整数返回的时候,如果返回到void*里面,请问这个整数或者说这个变量在哪里呢?

其实上面的返回就相当于把这个数字写到指针变量里。4字节写到8字节里

void* ret=(void*)td->number;
    for(auto& iter:threads){void* ret=nullptr;//注意是void*//阻塞式的等待每一个线程int n=pthread_join(iter->tid,&ret);//&地址是void**  assert(n == 0);(void)n;cout<<"join : "<<iter->namebuffer<<" success"<<endl;delete iter;}

这个join函数内部就做了这样一件事情

void** retp=&ret;
*retp=return (void*)td->number //*retp解引用就是ret

这样就相对于把返回值写到ret变量里。

然后再打印出来。

for(auto& iter:threads){void* ret=nullptr;//注意是void*//阻塞式的等待每一个线程int n=pthread_join(iter->tid,&ret);//&地址是void**  assert(n == 0);(void)n;//注意这里如果用int打印,会有精度损失,8->4//所以用long long//cout<<"join : "<<iter->namebuffer<<" success , number: "<<(int)ret<<endl;cout<<"join : "<<iter->namebuffer<<" success , number: "<<(long long)ret<<endl;delete iter;}

在这里插入图片描述
确实拿到了返回结果。

这里想告诉大家的时,未来线程的返回值可以通过join第二个参数拿到。

如果这里还是不懂话,我们换个值。
在这里插入图片描述
我想让每个线程都能返回这个值。
在这里插入图片描述
这里可以理解成pthread库里面为了保存当前线程的返回值定义了一个变量类型是void*,return 这里可以认为把111强转成void*,我们把它当成指针来看,return就是把111写到void变量当中
在这里插入图片描述
接下来就是,我在自己的用户空间里定义了一个指针变量void
ret
在这里插入图片描述
而pthread_jion本质:是从库中获取执行线程的退出结果!
在这里插入图片描述
&ret,就是指向这块空间
在这里插入图片描述
然后join内部就相当于做
(&ret)就是我们的定义的ret,然后把111拷贝到ret里
在这里插入图片描述
因此,return就相当于把这个数字返回到库中了,而库为当前线程维护一个小的变量,它保存的是该线程退出时void
变量,直接把这个数字写入到这个变量中,而我们在用户空间定义一个void*ret变量,如果不用pthrad_join函数怎么接收这个返回值呢?假设返回值变量名称是下,是不是就可以直接ret=x了啊。但是我们又不能直接读取库里的内容,必须通过函数来调用,所以只能&ret,然后就如void** retp=&ret;retp=x;而retp就是ret,这样而能把x赋给ret了。

还有一种角度就是,形参是是实参的临时拷贝,如果就传ret,形参的改变并不会影响实参,这个值你就拿不回来。

同样pthread_exit也可以传返回值

void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=5;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);}pthread_exit((void*)111);}

在这里插入图片描述

既然假的地址,整数都能被外部拿到,那么如果返回的是堆空间的地址呢?对象的地址呢?

struct ThreadReturn
{int exit_code;int exit_result;
};void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=5;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);}//注意千万不能直接申请一个对象//ThreadReturn tr; //这是在栈上开辟空间,出了栈就销毁了//tr.exit_code//tr.exit_result//return (void*)&trThreadReturn *tr=new ThreadReturn();//堆开辟的空间tr->exit_code=1;tr->exit_result=111;return (void*)tr;}int main()  
{vector<ThreadDate*> threads;
#define NUM 10for(int i=0;i<NUM;++i){ThreadDate* td=new ThreadDate();td->number=i+1;snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);pthread_create(&td->tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了threads.push_back(td);}for(auto& iter:threads){cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;}for(auto& iter:threads){ThreadReturn* tr=nullptr;int n=pthread_join(iter->tid,(void**)&tr);//&地址是void**    assert(n == 0);(void)n;cout<<"join : "<<iter->namebuffer<<" success , exit_code: "<<tr->exit_code<<" exit_result: "<<tr->exit_result<<endl;delete iter;}//这里就可以看出主线程式阻塞式的等待,全部等待成功,才打印这句话cout<<"main thread quit!!"<<endl;return 0;
}

在这里插入图片描述

还有一个小问题,以前进程退出的时候,我们除了可以拿到退出码,还有退出信号
pthread_join为什么没有对应的参数可以接收线程异常的信号呢
在这里插入图片描述

为什么没有见到,线程退出的时候,对应的退出信号呢? 线程异常了怎么呢?

线程出异常,收到信号,整个进程都会退出!pthread_join连返回的机会都没有,pthread_join:默认就会认为函数会调用成功!不考虑异常问题,异常问题是你进程该考虑的问题。

上面说了线程终止有三种方式,接下来就说这第三种方式。

线程是可以被cancel取消的!
注意:线程要被取消,前提是这个线程已经跑起来了。

pthread_cancel

取消一个执行中的线程
在这里插入图片描述
thread:线程ID

void* start_rountine(void* args)
{//安全的进行强制类型转化ThreadDate* td=static_cast<ThreadDate*>(args);int cnt=5;while(cnt){cout<<"cnt: "<<cnt<<" &cnt"<<&cnt<<endl;cnt--;sleep(1);}//正常跑完返回的100,那被取消的线程返回的是什么呢?return (void*)100;
}int main()  
{vector<ThreadDate*> threads;
#define NUM 10for(int i=0;i<NUM;++i){ThreadDate* td=new ThreadDate();td->number=i+1;snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);pthread_create(&td->tid,nullptr,start_rountine,td); //这样不仅每个线程数据除了自己拿到了,主线程也全部拿到了threads.push_back(td);}for(auto& iter:threads){cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;}//线程取消sleep(5);//先让线程跑起来for(int i=0;i<threads.size()/2;++i){pthread_cancel(threads[i]->tid);cout<<"pthread_cancel: "<<threads[i]->namebuffer<<" success"<<endl;}for(auto& iter:threads){void* ret=nullptr;//注意是void*int n=pthread_join(iter->tid,&ret);//&地址是void**  assert(n == 0);(void)n;cout<<"join : "<<iter->namebuffer<<" success , number: "<<(long long)ret<<endl;delete iter;}//这里就可以看出主线程式阻塞式的等待,全部等待成功,才打印这句话cout<<"main thread quit!!"<<endl;return 0;
} 

在这里插入图片描述

一个线程如果是被取消的,退出码是-1。
一般都是主线程取消新线程的。

接下来重新认识我们的线程库(语言版)
C++也有多线程,使用要包含一个thread库文件

mythread:mythread.ccg++ -o $@ $^ -std=c++11  
.PHONY:clean
clean:rm -f mythread
#include<iostream>
#include<thread>
#include<unistd.h>using namespace std;
void* thread_run(void* args)
{while(true){cout<<"我是新线程..."<<endl;sleep(1);}
}int main()
{//创建,并执行对应的方法thread t1(thread_run);while(true){cout<<"我是主线程..."<<endl;sleep(1);}//回收t1.join();return 0;
}

我故意把makefile中pthread弄掉了,编译不能通过了。

在这里插入图片描述
我们明明没有用过pthread_create怎么会有这个呢?我用的可是C++的多线程,从来没有用过pthread库啊。

那把它加上去看看,就没有报错了。我用的是C++的多线程,但是我照样看的是轻量级进程。
在这里插入图片描述
任何语言,在Linux中如果要实现多线程,必定要用pthread库。
如何看到C++11中多线程库呢?
C++11的多线程库,在Linux环境中,本质是对pthread库的封装!

4.线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

也就是说线程是可以等待的,等待的时候,join的阻塞式的等待,如果我们不想等待呢?

pthread_detach

线程分离,线程结束后,自动释放线程资源。

在这里插入图片描述
thread:线程ID

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离自己。

如果是自己分离自己该如何得知自己的线程ID呢?

pthread_self

谁调用这个函数,就获得线程ID
在这里插入图片描述

#include<iostream>
#include<pthread.h>
#include<unistd.h>using namespace std;string changeId(const pthread_t &thread_id)
{char tid[128];snprintf(tid, sizeof(tid), "0x%x", thread_id);return tid;
}void* start_routine(void* args)
{string threadname=static_cast<const char*>(args);while(true){cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");cout<<"main thread running ... new thread id:"<<changeId(tid)<<endl;pthread_join(tid,nullptr);return 0;
}

在这里插入图片描述
由此证明pthread_self可以获取线程ID的

下面使用pthread_detach分离线程
在这里插入图片描述

void* start_routine(void* args)
{string threadname=static_cast<const char*>(args);//pthread_detach(pthread_self());//设置自己为分离状态while(true){cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");string main_id=changeId(pthread_self());cout<<"main thread running ... new thread id:"<<changeId(tid)<<"main thread id: "<<main_id<<endl;//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了int n=pthread_join(tid,nullptr);cout<<"result: "<<strerror(n)<<endl;return 0;
}

先不分离看看结果
在这里插入图片描述

void* start_routine(void* args)
{string threadname=static_cast<const char*>(args);pthread_detach(pthread_self());//设置自己为分离状态int cnt=5;while(cnt--){cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");string main_id=changeId(pthread_self());cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了int n=pthread_join(tid,nullptr);cout<<"result: "<<n<<" : "<<strerror(n)<<endl;return 0;
}

现在分离再看运行结果
在这里插入图片描述

为什么分离和不分离的运行结果都是一样的呢?

之前说过,主线程和新线程谁先跑是不确定的。
在这里插入图片描述
可能先跑的主线程,然后就先等待了,还是阻塞式等待,而新线程还没有执行到pthread_detach,等到新线程分离了,但是主线程不知道,所以等新线程退出了还是回收你。

接下来我们先让新进程分离,然后主进程在等待看一看运行结果。

int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");string main_id=changeId(pthread_self());cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;//先让新进程跑上2秒sleep(2);//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了int n=pthread_join(tid,nullptr);cout<<"result: "<<n<<" : "<<strerror(n)<<endl;return 0;
}

在这里插入图片描述

所以推荐分离应该在线程创建成功后,由主线程进行分离新线程

int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");pthread_detach(tid);//主线程对新线程进行分离string main_id=changeId(pthread_self());cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;//sleep(2);//一个线程默认是joinable的,如果设置了分离状态,不能够进行等待了int n=pthread_join(tid,nullptr);cout<<"result: "<<n<<" : "<<strerror(n)<<endl;return 0;
}

在这里插入图片描述
这样结果就更明显了。
因为新线程一跑起来就分离了,主线程在等待就报错了

如果主线程对新线程分离了,就不用再等待了,主线程做自己的事情就好了。

void* start_routine(void* args)
{string threadname=static_cast<const char*>(args);int cnt=5;while(cnt--){cout<<threadname<<" running ... : "<<changeId(pthread_self())<<endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");pthread_detach(tid);//主线程对新线程进行分离string main_id=changeId(pthread_self());while(true){cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<endl;sleep(1);}return 0;
}

在这里插入图片描述
其实线程控制到这里就结束了。

但是我们一直还有一些问题没有解决,
线程ID是什么?
每个线程都有独立的栈结构,可是虚拟地址空间的栈只有一个,那这个栈在哪?

下面我们就说一说。
在这里插入图片描述
Linux无法直接提供创建系统调用接口!而只能给我们提供创建轻量级进程的接口!
在这里插入图片描述
但是我们的程序员用户可不管这些轻量级进程,我只要线程。
没有办法,所以在操作系统和应用程序员之间设计了一个库,这就是我们一直在用的pthread库,我们称之为原生线程库。
在这里插入图片描述
当一个程序员想用线程的时候,是不是得有线程的ID,线程的状态,优先级等,独立栈这些属性。
但还要做一个事情,我们程序员要线程时,并不是直接向操作系统要的,而是从库中要。所以原生线程库,可能要存在多个线程。
也就是说你用库中的接口创建了线程,别人可以同时在用吗?当然是可以的。你在开发时用。别人在运行时用。

所以当前原生线程库要不要对线程进行管理呢?
当然要的!不然你怎么知道你的线程ID是多少,你怎么知道你当前线程的栈在哪里,栈大小,你怎么你的线程其他属性有哪些。

下面这就是我们线程的属性,它是联合体。
将来线程属性字段就会按照特定的要求填充到这个数组里。每一个字段都有自己的含义。
在这里插入图片描述

虽然要对线程做管理,但是不像进程那么多。
如何管理?
先描述,在组织!
只不过这个描述(线程的属性),比较少!

可以理解成每次创建线程,都需要在库中先创建一个结构体,这里每一个结构体都会对应一个轻量级进程,
在这里插入图片描述
Linux方案:用户级线程,用户关心的线程属性在库中,内核提供线程执行流的调度
Linux 用户级线程:内核级轻量级进程 = 1:1

库不关心线程如何调度,只关心线程ID,栈在哪里,栈的大小等属性这是由库维护的,相当于库也帮我们创建一个数据结构来描述这个线程。

下面在看一张图,两张图结合一起能让我们对这块知识有更清楚的认识。

虚拟地址空间有一块区域,叫做共享区,这个我们在动静态库的时候说过。
说到底pthread库是一个磁盘文件只不过是库文件罢了 。进程用了这个库,是一定要加载到内存中,然后映射到进程地址空间的共享区里面。

在这里插入图片描述

每一个线程当被创建时,除了要在内核中创建对应的轻量级线程,还要库中创建对应的描述线程属性的相关结构体
在这里插入图片描述
这里只是给出了重要的三条属性,第一个是线程ID,第二个线程的局部存储,第三个线程栈结构。
这就是库中描述的对象,那怎么组织的呢?
有一个线程,就创建一个线程的结构体,我们可以称之为TCB,这个线程控制块是放在一起的,你可以想象成看出是一个数组,有了数组之后,为了更好的找到某一个线程,我们只需要找到数组的起始地址,而这个数组的起始位置并没有采用下标,动态库加载到内存之后不是有起始地址吗,所以对应的每个TCB都有自己的起始地址。
在这里插入图片描述
所以实际上线程ID就是在共享区线程库中对应线程TCB的地址
根据这个地址就可以找到对应线程的相关属性了。
所以当一个线程结束时,库会把该进程的返回值填到该线程ID所对应的TCB中,主线程在进程join的时候,根据该线程的ID去对应TCB中拿返回值。

现在也可以回答之前的问题了,地址空间里只有一个栈,可是说每个线程都有自己独立的栈结构,那么这个栈在那呢?
这个栈在库中的对应TCB中,每个栈都有自己的私有栈。
在这里插入图片描述
换句话说,未来创建多个线程,主线程的栈在虚拟地址空间中,用的时主线程栈,而其他线程你想用对应的栈,这个栈在共享区的线程库中的对应的TCB结构体中,库帮我们维护好的。

库帮我们创建轻量级进程,调用的接口是clone,第一个参数指明回调函数,第二个指向线程的独立栈,当TCB创建好后,把栈空间的起始地址传递给child_stack,而未来每个新线程都用的是自己独立栈。不和主线程用同一个栈。
在这里插入图片描述

总结一下:
当你创建线程时,对应的线程实际上,它帮我们在线程库中创建对应的线程控制块(可以认为时TCB),线程控制块对应的地址就是线程控制块起始地址,这个TCB里面包含线程的ID,线程私有栈结构,线程的局部存储,创建好之后底层库继续帮我们调用clone,在底层创建轻量级进程,然后把我们创建好的TCB对应的回调方法,私有栈等参数传给clone,所以底层的线程一旦创建好了,它就时依赖我们的原生线程库,进而新线程在使用的临时变量就是压到了自己的child_statck所指的栈中,也就是在我们的库中直接保存了。

所以线程的ID就是我们传说中在共享区的线程库中对应线程TCB的地址。
这种解决方案的线程,我们称之为用户级线程!

线程的局部存储我们说一下。

我们知道线程大部分资源都是共享的,关于全局变量我们也演示过了。

int g_val=0;string changeId(const pthread_t &thread_id)
{char tid[128];snprintf(tid, sizeof(tid), "0x%x", thread_id);return tid;
}void* start_routine(void* args)
{string threadname=static_cast<const char*>(args);//pthread_detach(pthread_self());//设置自己为分离状态//int cnt=5;while(true){cout<<threadname<<" running ... : "<<changeId(pthread_self())<<\" g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;++g_val;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");pthread_detach(tid);//主线程对新线程进行分离string main_id=changeId(pthread_self());while(true){cout<<"main thread running ... new thread id:"<<changeId(tid)<<" main thread id: "<<main_id<<\" g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;sleep(1);}return 0;
}

在这里插入图片描述
上面运行结果符合预期

下面我们改一下代码,在运行一下看结果

__thread int g_val=0;

在这里插入图片描述
发现新线程值一直在++,而且地址也变得很大,而主线程值没有变,但是地址也变得很大,并且两个地址都不一样。这是为什么呢?

添加__thread,可以将一个内存类型设置为线程局部存储
这个变量依旧是全局变量,只不过在编译得时候,给每一个线程都来一份,每个线程都有一份,那么它们在访问这个变量时就互不影响了;
那为什么地址会有那么大变化呢?
原因在于刚开始定义得全局变量在已初始化数据段,而你一旦把它设置了局部存储,那么这个变量就被映射到了对应得线程TCB中得线程局部存储,而线程局部存储可是在共享区中,共享区可是接近栈了,比已初始化数据段可要高,而虚地址空间从低地址到高地址的。
在这里插入图片描述
如果未来想给线程定义一些私有属性,不想放在栈上,或者new或者malloc你就可以定义成这样,这种局部存储是介于全局变量和局部变量之间的线程特有的存储方案。

以上就是线程控制的全部东西!

既然学完了,那现在就用一下,我们尝试把线程接口进行封装,就像C++那样调用我们的线程!

5.对线程的简单封装

#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<functional>using namespace std;class Thread
{
public://这里也可以按照C的方法typedef void*(*func_t)(void*)typedef function<void*(void*)> func_t;private:string _name;func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程ID
};

以后我们写代码难免会遇见C和C++混编的情况,可能会觉得C++提供了很多方便的接口给我们用,但是不好意思,有些C++提供的接口,底层不认识。底层可能用纯C写的。

class Thread
{
public:typedef function<void*(void*)> func_t;const int num=1024;
public:Thread(func_t func,void* args=nullptr,int number=0):_func(func),_args(args){char namebuffer[num];snprintf(namebuffer,sizeof(namebuffer),"thread->%d",number);_name=namebuffer;//调用回调函数//报错int n=pthread_create(&_tid,nullptr,_func,_args);assert(n == 0);(void)n;}private:string _name;func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程ID
};

在这里插入图片描述
pthread_create接口不认识C++提供的这个东西。
解决方法有很多,换成C的,或者别的方法,下面提供一种方法

我写一个能pthread_create库能认识的回调函数方法。

    void* start_routine(void* args){return _func(_args);}Thread(func_t func,void* args=nullptr,int number=0):_func(func),_args(args){char namebuffer[num];snprintf(namebuffer,sizeof(namebuffer),"thread->%d",number);_name=namebuffer;//调用回调函数int n=pthread_create(&_tid,nullptr,start_routine,_args);assert(n == 0);(void)n;}

在这里插入图片描述
但这里还有错。考虑一下为什么。

原因在于

    void* start_routine(void* args)//类内函数,有缺省参数(this指针){return _func(_args);}

那我static一下,就变成静态函数了,没有this指针了

    static void* start_routine(void* args)//类内函数,有缺省参数(this指针){return _func(_args);}

在这里插入图片描述
但问题又出来了,这又是什么问题呢?

    static void* start_routine(void* args)//类内函数,有缺省参数(this指针){//静态函数只能调用静态成员和静态函数return _func(_args);}

那把这两个成员变量都变成静态成员变量可不可以?
不可以的,把这个两个成员变成静态的了,那这个两个变量就是所有线程共享的了。

这里我们再写一个类,把this指针保存起来,然后再去调用,顺便把剩下成员函数谢谢,简单的线程封装就写好了

#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<functional>
#include<cstdio>
#include<cassert>using namespace std;//声明
class Thread;class Context
{
public:Thread* _this;void* args_;Context():_this(nullptr),args_(nullptr){}
};class Thread
{
public:typedef function<void*(void*)> func_t;const int num=1024;
public:static void* start_routine(void* args)//类内函数,有缺省参数(this指针){Context* tr=static_cast<Context*>(args);tr->_this->_func(tr->args_);//静态函数只能调用静态成员和静态函数//return _func(_args);}Thread(func_t func,void* args=nullptr,int number=0):_func(func),_args(args){char namebuffer[num];snprintf(namebuffer,sizeof(namebuffer),"thread->%d",number);_name=namebuffer;Context* tr=new Context();tr->_this=this;tr->args_=_args;//调用回调函数int n=pthread_create(&_tid,nullptr,start_routine,tr);// int n=pthread_create(&_tid,nullptr,start_routine,_args);assert(n == 0);(void)n;}void join(){int n=pthread_join(_tid,nullptr);assert(n==0);(void)n;}~Thread(){}private:string _name;func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程ID
};

在这里插入图片描述

相关文章:

【linux】线程控制

线程控制 1.创建线程2.线程终止3.线程等待4.线程分离5.对线程的简单封装 喜欢的点赞&#xff0c;收藏&#xff0c;关注一下把&#xff01; 进程概念上篇文章已经讲完了&#xff0c;下面我们就来说说线程控制。 我们使用的接口是pthread线程库&#xff0c;也叫做原生线程库给我…...

Swift学习笔记第三节:Set类型

1、代码 import Foundationvar set1: Set<Int> [1, 2, 3, 4, 3] print("定义1: \(set1)") var set2 Set(1...4) print("定义2: \(set2)") print("长度: \(set2.count)") print("是否为空: \(set2.isEmpty)") set1.insert(99)…...

【前端】安装指定版本的nodejs

先安装curl sudo apt install curl以下是 Nodejs 18.x的安装&#xff0c;一行代码搞定 &&\ 的意思是前面的命令执行无误后&#xff0c;再执行后面代码 curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - &&\ sudo apt-get install -y node…...

多商户小程序开源商城源码 打造微信商城新零售网店

一款易于二次开发的商城小程序&#xff0c;使用php、thinkphp6、vue、element-ui和uniapp技术栈进行开发。 该商城小程序内置多商户商城和分销商城&#xff0c;包括小程序商城、H5商城、公众号商城、PC商城和App&#xff0c; 支持多种分销模式、拼团、砍价、秒杀、优惠券、活…...

云仓酒庄的品牌雷盛红酒LEESON分享干红是纯葡萄酿造的吗?

干红是一种葡萄酒的简称&#xff0c;全称是干型红葡萄酒。葡萄酒按含残糖量分为干型、半干型、半甜型和甜型。无论什么型的酒&#xff0c;只要是葡萄酒&#xff0c;那就是葡萄酿造的。 云仓酒庄的品牌雷盛红酒LEESON分享干红是葡萄酒的一种&#xff0c;而葡萄酒却不止干红一种…...

PHP函数学习总结

version_compare&#xff08;比较php版本&#xff09; 用法&#xff1a; version_compare(string $version1, string $version2, ?string $operator null): int|bool//示例 $result version_compare(PHP_VERSION, 8.0.0) > 0 ? ok : fail;echo $result;// 输出ok证明当…...

5G RedCap:轻量5G技术的新宠

嘿&#xff0c;大家好&#xff01;今天我们将深入了解一项引领5G轻量化时代的关键技术——5G RedCap。这项技术可谓是5G发展中的一把新利器&#xff0c;让我们看看它是如何在5G世界中展露头角的。 5G RedCap是什么&#xff1f;轻量化5G技术的精髓 5G RedCap的全名是5G Reduced…...

【LeetCode 热题 HOT 100】题解笔记 —— Day04

❤ 作者主页&#xff1a;欢迎来到我的技术博客&#x1f60e; ❀ 个人介绍&#xff1a;大家好&#xff0c;本人热衷于Java后端开发&#xff0c;欢迎来交流学习哦&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 如果文章对您有帮助&#xff0c;记得关注、点赞、收藏、…...

rust中的超时处理

rust中的超时处理 自从 tokio 1.0发布以来&#xff0c;rust的异步开发总算大势已定。尽管没达到标准库的速度&#xff0c;依然挡不住大家的热情。看编程排行榜&#xff0c;增加2倍的开发者。 既生瑜何生亮&#xff0c;感觉go就是小号的rust。 不废话了。背景&#xff1a;之前…...

DML语言(重点)———update

格式&#xff1a;update 要修改的对象 set 原来的值新值 -- 修改学员名字,带了简介 代码案例&#xff1a; -- 修改学员名字,带了简介 UPDATE student SET name清宸 WHERE id 1; -- 不指定条件情况下&#xff0c;会改动所有表&#xff01; 代码案例…...

Mybatis使用详解

简介 MyBatis是一款优秀的持久层框架&#xff0c;它支持普通SQL查询&#xff0c;存储过程和高级映射。MyBatis通过简单的XML或注解用于配置和原始映射&#xff0c;将接口和Java的POJOs&#xff08;Plain Ordinary Java Object&#xff0c;普通的Java对象&#xff09;映射成数据…...

云原生周刊:Karmada 成为 CNCF 孵化项目 | 2023.12.25

开源项目推荐 kubernetes-reflector Reflector 是一个 Kubernetes 的插件&#xff0c;旨在监视资源&#xff08;secrets 和 configmaps&#xff09;的变化&#xff0c;并将这些变化反映到同一命名空间或其他命名空间中的镜像资源中。 Lingo Lingo 是适用于 K8s 的 OpenAI 兼…...

【开源】基于JAVA的学校热点新闻推送系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 新闻类型模块2.2 新闻档案模块2.3 新闻留言模块2.4 新闻评论模块2.5 新闻收藏模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 新闻类型表3.2.2 新闻表3.2.3 新闻留言表3.2.4 新闻评论表3.2.5 新闻收藏表 四、系统展…...

Java基于TCP网络编程的群聊功能

服务端 import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List;public class Server2 {public static List<Socket> onlineList new ArrayList<>();public static void main(String[] args) throws Except…...

CentOS+ISCSI

九、配置iSCSI 添加1块大小为10G的虚拟硬盘; 安装iSCSI服务端targetcli; 使用新增加的硬盘创建卷组,名称为iscsivg,再创建iSCSI共享逻辑卷,逻辑 卷名称为iscsistore,大小为5G; 使用上述逻辑卷创建后端存储,名称为serverc.iscsistore; 定义iSCSI的IQN为iqn.2022-…...

RHCE9学习指南 第11章 网络配置

11.1 网络基础知识 一台主机需要配置必要的网络信息&#xff0c;才可以连接到互联网。需要的配置网络信息包括IP&#xff0c;子网掩码&#xff0c;网关和DNS。 11.1.1 IP地址 在计算机中对IP的标记使用的是32bit的二进制&#xff0c;例如&#xff0c; 11000000 10101000 00…...

Qt如何在控制台项目中使用opencv打开视频

Qt如何在控制台项目中使用opencv打开视频&#xff1f; 重要代码&#xff1a; 1、在pro文件中这样设置&#xff1a; QT - gui QT core widgets serialport 2、不要继承和使用&#xff1a;QCoreApplication #include pro文件&#xff1a; cpp QT - gui QT core widgets seria…...

Node.js 默认包管理器 npm 详解

目录 npm 概念 npm 命令 npm init npm install npm update npm uninstall npm search npm run other npm 安装 yarn npm 安装 yarn 和 npm 安装项目依赖 websocket 本质区别 npm 概念 npm&#xff08;Node Package Manager&#xff09;是一个用于管理 JavaScript 包…...

vue利用深拷贝解决修改不能取消的问题

vue利用深拷贝解决修改不能取消的问题 在对某数据进行修改时考虑还需要进行“确认”、“取消”操作&#xff0c;那么在取消时就需要返回保留的数据内容&#xff0c;那么如何将原有数据保留一份则是关键性问题。 显然修改值不能直接进行原值的赋值操作&#xff0c;因为这样无法取…...

MATLAB - 使用 YOLO 和基于 PCA 的目标检测,对 UR5e 的半结构化智能垃圾箱拣选进行 Gazebo 仿真

系列文章目录 前言 本示例展示了在 Gazebo 中使用 Universal Robots UR5e cobot 模拟智能垃圾桶拣选的详细工作流程。本示例提供的 MATLAB 项目包括初始化、数据生成、感知、运动规划和积分器模块&#xff08;项目文件夹&#xff09;&#xff0c;可创建完整的垃圾桶拣选工作流…...

个性化定制的知识付费小程序,为用户提供个性化的知识服务,知识付费saas租户平台

明理信息科技知识付费saas租户平台 在当今数字化时代&#xff0c;知识付费已经成为一种趋势&#xff0c;越来越多的人愿意为有价值的知识付费。然而&#xff0c;公共知识付费平台虽然内容丰富&#xff0c;但难以满足个人或企业个性化的需求和品牌打造。同时&#xff0c;开发和…...

基于flask和echarts的新冠疫情实时监控系统源码+数据库,后端基于python的flask框架,前端主要是echarts

介绍 基于flask和echarts的新冠疫情实时监控系统 软件架构 后端基于python的flask框架&#xff0c;前端主要是echarts 安装教程 下载到本地&#xff0c;在python相应环境下运行app.py,flask项目部署请自行完成 使用说明 flaskProject文件夹中 app.py是flask项目主运行文…...

总结js中遍历对象属性的方法

方法介绍 1、 forin循环&#xff1a;遍历对象自身的和原型链上的可枚举属性。 2、Object.getOwnPropertySymbols()方法&#xff1a;返回一个数组&#xff0c;包含对象自身的所有Symbol类型的属性。 3、 Object.getOwnPropertyNames()方法&#xff1a;返回一个数组&#xff0…...

编写fastapi接口服务

FastAPI是一个基于 Python 的后端框架&#xff0c;该框架鼓励使用 Pydantic 和 OpenAPI (以前称为 Swagger) 进行文档编制&#xff0c;使用 Docker 进行快速开发和部署以及基于 Starlette 框架进行的简单测试。 step1&#xff1a;安装必要库 pip install fastapi uvicorn st…...

RasaGPT对话系统的工作原理

RasaGPT 结合了 Rasa 和 Langchain 这 2 个开源项目&#xff0c;当超出 Rasa 现有意图(out_of_scope)的时候&#xff0c;就会执行 ActionGPTFallback&#xff0c;本质上就是利用 Langchain 做了一个 RAG&#xff0c;调用 LLM API。RasaGPT 涉及的技术栈比较多而复杂&#xff0c…...

C++设计模式 #7 工厂方法(Factory Method)

“对象创建”模式 通过“对象创建”模式绕开new&#xff0c;来避免对象创建&#xff08;new&#xff09;过程中所导致的紧耦合&#xff08;依赖具体类&#xff09;&#xff0c;从而支持创建的稳定。它是接口抽象之后的第一步工作。 动机 在软件系统中&#xff0c;经常面临着创…...

信息网络协议基础-接入网技术

文章目录 概述***基于ATM架构虚电路PVC和SVC信元格式为什么信元格式由AAL决定?网络架构传统电信网络:点对点链路PPP协议协议内容消息过程多协议封装功能电话网接入Internet(DSL 数字用户线路)主要接入技术ADSL关键技术DMTDSLAM体系结构PPPOE帧格式过程特点局域网定义参考模型L…...

springboot 自动装配原理

在理解springboot自动装配之前需要了解spring的Configuration原理和Conditional两个注解原理。可以看下以前写的这两篇文章 spring 源码阅读之Configuration解析 spring使用Conditional进行条件装配 spring的SPI机制之使用SpringFactoriesLoader加载服务实现 SpringBootApp…...

前端---表格标签

1. 表格的结构 表格是由行和列组成&#xff0c;好比一个excel文件 2. 表格标签 <table>标签&#xff1a;表示一个表格 <tr>标签&#xff1a;表示表格中的一行 <td>标签&#xff1a;表示表格中的列<th>标签&#xff1a;表示表格中的表头 示例代码: &l…...

【软件工程】可执行文件和数据分离

一、概述 可执行文件和数据分离是一种软件设计策略&#xff0c;旨在将程序代码和程序使用的数据分离存储。这种方法通常用于提高软件的模块化程度和灵活性&#xff0c;以及方便软件的管理和维护。 在可执行文件和数据分离中&#xff0c;程序代码通常以可执行文件的形式存储&a…...

Linux dirs命令

Linux dirs命令用于显示目录记录。 显示目录堆叠中的记录。 语法 dirs [/-n -l]参数&#xff1a; n 显示从左边算起第n笔的目录。-n 显示从右边算起第n笔的目录。-l 显示目录完整的记录。 实例 列出"/home/cc/Ruijie"里所有内容的详细信息。可用如下命令。 dir…...

有什么好用的C/C++源代码混淆工具?

​ 有什么好用的C/C源代码混淆工具&#xff1f; 开始使用ipaguard 前言 iOS加固保护是直接针对ios ipa二进制文件的保护技术&#xff0c;可以对iOS APP中的可执行文件进行深度混淆、加密。使用任何工具都无法逆向、破解还原源文件。对APP进行完整性保护&#xff0c;防止应用…...

iOS设备信息详解

文章目录 ID 体系iOS设备信息详解IDFA介绍特点IDFA新政前世今生获取方式 IDFV介绍获取方式 UUID介绍特点获取方式 UDID介绍获取方式 OpenUDID介绍 Bundle ID介绍分类其他 IP地址介绍获取方式 MAC地址介绍获取方式正常获取MAC地址获取对应Wi-Fi的MAC地址 系统版本获取方式 设备型…...

如何使用支付宝沙箱环境支付并公网调用sdk创建支付单服务

文章目录 1.测试环境2.本地配置2. 内网穿透2.1 下载安装cpolar内网穿透2.2 创建隧道3. 测试公网访问4. 配置固定二级子域名4.1 保留一个二级子域名4.2 配置二级子域名5. 使用固定二级子域名进行访问 1.测试环境 MavenSpring bootJdk 1.8 2.本地配置 获取支付宝支付Java SDK,…...

[EFI]Dell Latitude-7400电脑 Hackintosh 黑苹果efi引导文件

硬件型号驱动情况主板 Dell Latitude-7400 处理器Intel Core i7-8665U已驱动内存16GB DDR4 RAM已驱动硬盘Toshiba KIOXIA 512GB SSD已驱动显卡Intel UHD 620 Graphics已驱动声卡Realtek ALC256已驱动有线网卡 无 无无线网卡蓝牙Intel Wireless-AC 9560已驱动 支持系统版本 maco…...

用芯片SIC8833可开发电子秤方案

SIC8833作为一款高性能的电子秤方案芯片&#xff0c;这款芯片是一个带24bitADC的8位RISC MCU&#xff0c;内置8k16位OTP程序存储器。具体24位双向I/O口的特性&#xff0c;广泛应用于电子衡器和精密测量及控制系统&#xff0c;能满足用户的不同需求和应用场景。 以下是电子秤方案…...

【Qt-QFile-QDir】

Qt编程指南 ■ Stream■ QTextStream■ QDataStream ■ QDial■ QDir■ QFile■■ ■ Stream ■ QTextStream /* 获取文件的路径 */ QString fileName QFileDialog::getOpenFileName(this);/* 指向文件 */ file.setFileName(fileName);/* 判断文件是否存在 */ if (!file.exi…...

设计模式之-单列设计模式,5种单例设计模式使用场景以及它们的优缺点

系列文章目录 设计模式之-6大设计原则简单易懂的理解以及它们的适用场景和代码示列 设计模式之-单列设计模式&#xff0c;5种单例设计模式使用场景以及它们的优缺点 设计模式之-3种常见的工厂模式简单工厂模式、工厂方法模式和抽象工厂模式&#xff0c;每一种模式的概念、使用…...

Android 13 - Media框架(25)- OMXNodeInstance(二)

上一节我们了解了 OMXNodeInstance 的创建过程&#xff0c;以及 IOmx 服务和 OMXNodeInstance、OMX组件之间的联系。接下来我们将一起了解 ACodec 是如何通过 OMXNodeInstance 这个中间层进行端口定义设置&#xff0c;以及端口Buffer分配的。 OMXNodeInstance 的代码还是比较长…...

生物系统学中的进化树构建和分析R工具包V.PhyloMaker2的介绍和详细使用

V.PhyloMaker2是一个R语言的工具包&#xff0c;专门用于构建和分析生物系统学中的进化树&#xff08;也称为系统发育树或phylogenetic tree&#xff09;。以下是对V.PhyloMaker2的一些基本介绍和使用说明&#xff1a; 论文介绍&#xff1a;V.PhyloMaker2: An updated and enla…...

XStream 反序列化漏洞 CVE-2021-39144 已亲自复现

XStream 反序列化漏洞 CVE-2021-39144 已亲自复现 漏洞名称漏洞描述影响版本 漏洞复现环境搭建 修复建议总结 漏洞名称 漏洞描述 在Unmarshalling Time处包含用于重新创建前一对象的类型信息。XStream基于这些类型的信息创建新实例。攻击者可以控制输入流并替换或注入对象&am…...

深入剖析LinkedList:揭秘底层原理

文章目录 一、 概述LinkedList1.1 LinkedList简介1.2 LinkedList的优点和缺点 二、 LinkedList数据结构分析2.1 Node节点结构体解析2.2 LinkedList实现了双向链表的原因2.3 LinkedList如何实现了链表的基本操作&#xff08;增删改查&#xff09;2.4 LinkedList的遍历方式 三、 …...

计算机网络复习-OSI TCP/IP 物理层

我膨胀了&#xff0c;挂我啊~ 作者简介&#xff1a; 每年都吐槽吉师网安奇怪的课程安排、全校正经学网络安全不超20人情景以及割韭菜企业合作的FW&#xff0c;今年是第一年。。 TCP/IP模型 先做两道题&#xff1a; TCP/IP协议模型由高层到低层分为哪几层&#xff1a; 这题…...

虚拟机服务器中了lockbit2.0/3.0勒索病毒怎么处理,数据恢复应对步骤

网络技术的不断发展也为网络威胁带来了安全隐患&#xff0c;近期&#xff0c;对于许多大型企业来说&#xff0c;许多企业的虚拟机服务器系统遭到了lockbit2.0/3.0勒索病毒攻击&#xff0c;导致企业所有计算机系统瘫痪&#xff0c;无法正常工作&#xff0c;严重影响了企业的正常…...

【MATLAB】 RGB和YCbCr互转

前言 在视频、图像处理领域经常会遇到不同色域图像的转换&#xff0c;比如RGB、YUV、YCbCr色域间的转换&#xff0c;这里提供一组转换公式&#xff0c;供大家参考。 色彩模型 RGB RGB色彩模型是一种用于表示数字图像的颜色空间&#xff0c;其中"RGB"代表红色&…...

【线性代数】决定张成空间的最少向量线性无关吗?

答1&#xff1a; 是的&#xff0c;张成空间的最少向量是线性无关的。 在数学中&#xff0c;张成空间&#xff08;span space&#xff09;是一个向量空间&#xff0c;它由一组向量通过线性组合&#xff08;即每个向量乘以一个标量&#xff09;生成。如果这组向量是线性无关的&…...

暴力破解(Pikachu)

基于表单的暴力破解 先随便输入一下&#xff0c;然后抓包&#xff0c;进行字典爆破 验证码绕过(on server) server服务端要输入正确的验证码后进行爆破 之后的操作没什么不一样 验证码绕过(on client) 这个也需要输入验证码&#xff0c;但是后面进行字典爆破的时候&#xf…...

如何使用CMake查看opencv封装好的函数

当我们有时想查看opencv自带的函数的源代码&#xff0c;比如函数cvCreateImage, 此时我们选中cvCreateImage, 点击鼠标右键->转到定义&#xff0c;我们会很惊讶的发现为什么只看到了cvCreateImage的一个简单声明&#xff0c;而没有源代码呢&#xff1f;这是因为openCV将很多…...

微盛·企微管家:用户运营API集成,电商无代码解决方案

连接电商平台的新纪元&#xff1a;微盛企微管家 随着电子商务的蓬勃发展&#xff0c;电商平台的高效运营已经成为企业成功的关键。在这个新纪元里&#xff0c;微盛企微管家以其创新的无代码开发连接方案&#xff0c;成为企业之间连接电商平台的强大工具。它允许企业轻松集成电…...

Hive 部署

一、介绍 Apache Hive是一个分布式、容错的数据仓库系统&#xff0c;支持大规模的分析。Hive Metastore&#xff08;HMS&#xff09;提供了一个中央元数据存储库&#xff0c;可以轻松地进行分析&#xff0c;以做出明智的数据驱动决策&#xff0c;因此它是许多数据湖架构的关键组…...