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

Linux系统编程 --- 多线程

 线程:是进程内的一个执行分支,线程的执行粒度,要比进程要细。

一、线程的概念

1、Linux中线程该如何理解

地址空间就是进程的资源窗口。

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

Linux实现方案:

        在Linux中,线程在进程内部执行,线程在进程的地址空间内运行。任何执行流要执行,都要有资源,地址空间是进程的资源窗口。

        在Linux中,线程的执行粒度要比进程要更细,线程执行进程代码的一部分。

2、重新定义线程和进程

什么叫做线程呢?线程是操作系统调度的基本单位。

什么叫做进程呢?内核观点,进程是承担分配系统资源的基本实体。

执行流也是资源。

如何理解我们以前的进程呢?

操作系统以进程为单位,分配资源,我们当前的进程内部,只有一个执行流。常规情况就是,一个进程里面有多个执行流。

Linux 设计者复用进程数据结构和管理算法。struct task_struct --- 模拟线程。Linux没有真正意义的线程,他没有对应的 TCB 结构,而是使用进程的内核和数据结构模拟线程。

3、重谈地址空间

虚拟地址是如何转换到物理地址的?

虚拟地址是32位的, 并不是一个整体,把他分成了10 + 10 + 12前十位找到二级页表,中间十位找到页框,后面12位找到页框里的地址。

二级页表大部分情况都是不全的 ,线程目前分配资源,本质就是分配地址空间的范围。

4、Linux线程周边的概念

线程 vs 进程 切换问题

线程比进程更轻量化,为什么?

a.创建和释放轻量化。

b.切换更加轻量化(运行) 整个生命周期。

线程内的切换,不需要重新切换cache中的数据。、

共享:文件描述符表,私有:栈和上下文

样例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>void* threadRun(void* thread)
{while(true){std::cout << "new thread tid: " << getpid() << std::endl;sleep(1);}return nullptr;
}
int main()
{//创建一个线程pthread_t tid;pthread_create(&tid,nullptr,threadRun,nullptr);while(true){std::cout << "main thread tid: " << getpid() << std::endl;sleep(1);}
}

内核中没有线程的概念,操作系统不会提供线程的系统调用,只会给我们提供轻量级进程的系统调用!我们用户需要线程的接口,所以就有了应用层pthread线程库,轻量级进程接口进行封装,为用户提供直接线程的接口。几乎所有的LInux平台,都是默认自带这个库的!Linux中编写多线程代码,需要使用第三方pthread库!

快速使用线程接口:

创建线程:

thread 线程id,attr 线程的属性设置成nullptr,start_routine 函数指针

void* 返回值,返回任意的指针类型,8个字节。void大小为1个字节

arg 创建线程成功,新县城回调线程函数的时候,需要参数,这个参数就是给线程函数传递的。

成功0被返回,errno不会被设置。!0表示错误码。

查看轻量级进程。

轻量级进程的lwp,light weight process

 

任何一个线程被杀掉了,进程也会退出, kill -9 发给了进程。全区变量是线程间共享的。 

线程id是什么东西呢?乍一看他像地址,实质上他是什么呢?

拿到自己的线程id

clone函数被我们的原生线程库封装。

线程库调用系统调用,参数为回调函数,独立栈。所以线程的概念是库给我们维护的。书写的原生线程库,需要加载到内存中,所以我们的讨论都在内存中。线程库需要维护线程的概念不用维护线程的执行流。所以线程库里面都要维护多个线程属性集合,线程库需要管理线程,先描述再组织。我们的线程称为用户级线程。

tid是每一个线程的库级别的tcb的起始地址。是共享区中的一个地址。除了主线程,所有其他线程的独立栈,都是在共享区,具体来说是在pthread库中,tid指向的用户tcb中!

每一个线程在运行的时候都有独自的栈结构,都有自己的调用链。

给新线程传递参数。

二、线程的控制

线程的等待:

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>
//实现一个累加器,让新线程计算,返回给主线程
//设置请求任务
class request
{
public:request(int start,int end,std::string threadname):_start(start),_end(end),_threadname(threadname){}
public:int _start;int _end;std::string _threadname;
};//任务的回复
class response
{
public:response(int result,int exitcode):_result(result),_exitcode(exitcode){}
public:int _result;int _exitcode;
};void* sumCount(void* args)
{//强转为request指针request* rq = static_cast<request*>(args);//计算求和response* res = new response(0,0);for(int i = rq->_start;i <= rq->_end; i++){   std::cout << rq->_threadname << " is runing, caling..., " << i << std::endl;res->_result += i;usleep(1000);}delete rq;return res;
}
int main()
{//创建新线程pthread_t tid;request* rq = new request(1,100,"[new thread]");pthread_create(&tid,nullptr,sumCount,rq);//进行等待//8个字节void* res;pthread_join(tid,&res);//强转response* rsp = static_cast<response*>(res);std::cout << "rsp->result: " << rsp->_result << ", exitcode: " << rsp->_exitcode << std::endl;delete rsp;return 0;
}

主线程最后退出。

thread:等待线程的id,

value_ptr: 二级指针传参,解引用访问到void*  x   函数内部 *retval = z 就把z传递到了x中。就拿到了新线程的退出状态。

线程函数执行完后,线程就退出了。主线程等待的时候,默认是阻塞等待的! 

#include <iostream>
#include <unistd.h>
#include <pthread.h>
int g_val = 0;
void* threadRoutine(void* args)
{const char* name = static_cast<const char*>(args);int cnt = 5;while (true){//printf("%s,  pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val);std::cout << "thread name:" << name << std::endl;sleep(1);if(cnt == 0){break;}cnt--;}//新线程发生异常时,主线程会自动退出,新线程发送 值为-1 的宏return nullptr;
}
int main()
{//创建线程pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void*)"new thread");//pthread_joinvoid* retval;pthread_join(tid,&retval);std::cout << "main thread quit ..., ret: " << (long long int)retval << std::endl;return 0;
}
#include <iostream>
#include <unistd.h>
#include <pthread.h>
int g_val = 0;
void* threadRoutine(void* args)
{const char* name = static_cast<const char*>(args);int cnt = 5;while (true){//printf("%s,  pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val);std::cout << "thread name:" << name << std::endl;sleep(1);if(cnt == 0){break;}//发生浮点数错误int x = 10;int z = x/0;cnt--;}//新线程发生异常时,主线程会自动退出return nullptr;
}
int main()
{//创建线程pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void*)"new thread");//pthread_joinvoid* retval;pthread_join(tid,&retval);std::cout << "main thread quit ..., ret: " << (long long int)retval << std::endl;return 0;
}

上述代码会产生浮点数错误。

exit 是用来终止进程的!不能直接用来终止线程。

        

终止线程的函数。也可以使用返回值的方式。

void* threadRoutine(void* args)
{const char* name = static_cast<const char*>(args);int cnt = 5;while (true){//printf("%s,  pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val);std::cout << "thread name:" << name << std::endl;sleep(1);//exit(22);if(cnt == 0){break;}//发生浮点数错误// int x = 10;// int z = x/0;//cnt--;pthread_exit((void*)100);}//新线程发生异常时,主线程会自动退出return nullptr;
}

线程取消,不常见

int main()
{//创建线程pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,(void*)"new thread");//join的时候会得到一个宏,PTHREAD_CANCEL 值为-1。pthread_cancel(tid);//pthread_joinvoid* retval;pthread_join(tid,&retval);std::cout << "main thread quit ..., ret: " << (long long int)retval << std::endl;return 0;
}

join的时候会得到一个宏,PTHREAD_CANCEL 值为-1。

所有语言的多线程都会封装Linux的原生线程库。

用户级线程 + 内核的轻量级进程 = Linux线程。Linux线程就是用户级线程。

都有独立的栈结构,其实线程和线程之间没有秘密,线程的栈上的数据,也是可以被其他线程看到并访问的。

全局变量是被所有的线程同时看到并访问的!就变成了临界资源。如果线程想要一个私有的全局变量呢?定义全局变量时在前面加上__thread,它只能定义内置类型,自定义变量不可以被它修饰,称为线程的局部存储。它是线程级别的全局变量。在线程里面获取pid和tid时我们不需要传参,直接使用局部存储的变量就可以了。

代码:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
int g_val = 100;
void *threadRoutine(void *args)
{const char *name = static_cast<const char *>(args);int cnt = 5;while (true){// printf("%s,  pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val);printf("%s,  pid: %d, g_val: %d, &g_val: 0x%p\n", name,getpid(), g_val, &g_val);//std::cout << "thread name:" << name << std::endl;sleep(1);// exit(22);if (cnt == 0){break;}// 发生浮点数错误//  int x = 10;//  int z = x/0;// cnt--;// pthread_exit((void*)100);cnt--;}// 新线程发生异常时,主线程会自动退出return nullptr;
}
int main()
{// 创建线程pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"new thread");// join的时候会得到一个宏,PTHREAD_CANCEL 值为-1。// pthread_cancel(tid);while (true){printf("main thread pid: %d, g_val: %d, &g_val: 0x%p, create new thread tid: %p\n", getpid(), g_val, &g_val, tid);std::cout << "main thread, pid: " << getpid() << ", g_val: " << g_val << ", &g_val:" << &g_val << std::endl;sleep(1);g_val++;}// pthread_joinvoid *retval;pthread_join(tid, &retval);std::cout << "main thread quit ..., ret: " << (long long int)retval << std::endl;return 0;
}

 

线程分离:

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

代码:

int main()
{// 创建线程pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"new thread");// join的时候会得到一个宏,PTHREAD_CANCEL 值为-1。// pthread_cancel(tid);// while (true)// {//     printf("main thread pid: %d, g_val: %d, &g_val: 0x%p, create new thread tid: %p\n", getpid(), g_val, &g_val, tid);//     std::cout << "main thread, pid: " << getpid() << ", g_val: " << g_val << ", &g_val:" << &g_val << std::endl;//     sleep(1);//     g_val++;// }pthread_detach(tid);// pthread_join//void *retval;//pthread_join(tid, &retval);//std::cout << "main thread quit ..., ret: " << (long long int)retval << std::endl;return 0;
}

三、线程的互斥

共享数据,数据的不一致问题,肯定是和多线程并发访问是有关系的。对一个全局变量进行多线程并发操作不是安全的。

代码:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <vector>
#define NUM 10
int tickets = 1000;
class threaddata
{
public:threaddata(int name){threadname = "thread-" + std::to_string(name);}
public:std::string threadname;
};
void* getTicks(void* args)
{//强转threaddata* td = static_cast<threaddata*>(args);const char* name = td->threadname.c_str();while (true){if(tickets > 0){usleep(1000);printf("who=%s,get a ticket:%d\n",name,tickets);tickets--;}else{break;}}printf("%s ... quit\n", name);return nullptr;
}
int main()
{std::vector<threaddata*> dates;std::vector<pthread_t> tids;for(int i = 1; i <= NUM; i++){threaddata* date = new threaddata(i);dates.push_back(date);pthread_t tid;//创建子线程pthread_create(&tid,nullptr,getTicks,dates[i - 1]);tids.push_back(tid);}//等待for(auto thread:tids){pthread_join(thread,nullptr);}//释放资源for(auto td: dates){delete td;}return 0;
}

ticket-- 

1、先将tickets读入到CPU寄存器中;2、CPU内部进行运算操作。3、将计算结果写回内存。每一步都会对应一条汇编操作。

--操作并不是原子性操作,会导致数据不一致。
这给问题怎么解决呢?对共享数据的任何访问,保证任何时候只有一个执行流访问! ------ 互斥!引入锁的概念。

生成锁:

使用锁:

加锁的本质是使用时间换安全。加锁的表现是线程对于临界区代码串行执行。加锁的原则是尽量保证临界区代码越少越好!

代码:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <vector>
#define NUM 10
int tickets = 1000;
class threaddata
{
public:threaddata(int name,pthread_mutex_t* lock){threadname = "thread-" + std::to_string(name);_mutex = lock;}
public:std::string threadname;pthread_mutex_t* _mutex;
};
void* getTicks(void* args)
{//强转threaddata* td = static_cast<threaddata*>(args);const char* name = td->threadname.c_str();while (true){//使用锁pthread_mutex_lock(td->_mutex);if(tickets > 0){usleep(1000);printf("who=%s,get a ticket:%d\n",name,tickets);tickets--;pthread_mutex_unlock(td->_mutex);}else{pthread_mutex_unlock(td->_mutex);break;}//不加这个会产生其他线程的饥饿问题,通过同步机制解决这个问题。usleep(13);}printf("%s ... quit\n", name);return nullptr;
}
int main()
{//生成一个锁pthread_mutex_t lock;//初始化一个锁pthread_mutex_init(&lock,nullptr);std::vector<threaddata*> dates;std::vector<pthread_t> tids;for(int i = 1; i <= NUM; i++){threaddata* date = new threaddata(i,&lock);dates.push_back(date);pthread_t tid;//创建子线程pthread_create(&tid,nullptr,getTicks,dates[i - 1]);tids.push_back(tid);}//等待for(auto thread:tids){pthread_join(thread,nullptr);}//释放资源for(auto td: dates){delete td;}//释放一个锁pthread_mutex_destroy(&lock);return 0;
}

申请锁成功才能往后走。不成功,阻塞等待。

细节问题:

没有sleep一直再被一个线程抢是正常的,线程对于锁的竞争能力会不同。

我们抢到了票,我们不会立马抢下一张。其实多线程还要执行得到票之后的后续动作。usleep模拟。

纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿问题。不是说只要有互斥,必有饥饿。适合纯互斥的场景,就用互斥。为了解决这个问题,设置了两个规则:线程必须排队,出来的人,不能立马重新申请锁,必须排到队列的尾部。让所有的线程获取锁,按照一定的顺序。按照一定的顺序性获取资源叫做同步问题!

锁本身就是共享资源!那么谁来保护锁的安全呢?申请锁和释放锁本身就被设计称为了原子性操作。那么这是怎么做到的呢?锁的原理。
原子:一条汇编语句就是原子的!

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

return 0 申请锁成功。 交换的本质就是把内存中的数据,交换到CPU的寄存器中。把数据交换到线程的硬件上下文中。把一个共享的锁,让一个线程一条交换汇编的方式,交换到自己的上下文中。当前线程持有锁。

在临界区中,线程可以被切换,在线程被切换出去的时候,是持有锁被切换出去的。所以我不在期间,照样没有人能进入临界区访问临界资源!对于其他线程来讲,一个线程要么持有锁,要么释放锁。当前线程访问临界区的过程,对于其他线程是原子的!

锁的应用:

做一个锁的封装。

代码:

#pragma once
#include <pthread.h>class mutex
{
public:mutex( pthread_mutex_t * mutex):_lock(mutex){}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 *mutex):_mutex(mutex){_mutex.Lock();}~lockGuard(){_mutex.UnLock();}
private:mutex _mutex;
};
#include <cstdio>
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <vector>
#include "lockGuard.hpp"
#define NUM 10
int tickets = 1000;
// 全局锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
class threaddata
{
public:threaddata(int name /*pthread_mutex_t* lock*/){threadname = "thread-" + std::to_string(name);//_mutex = lock;}public:std::string threadname;// pthread_mutex_t* _mutex;
};
// void* getTicks(void* args)
// {
//     //强转
//     threaddata* td = static_cast<threaddata*>(args);
//     const char* name = td->threadname.c_str();
//     while (true)
//     {
//         //使用锁
//         pthread_mutex_lock(td->_mutex);
//         if(tickets > 0)
//         {
//             usleep(1000);
//             printf("who=%s,get a ticket:%d\n",name,tickets);
//             tickets--;
//             pthread_mutex_unlock(td->_mutex);
//         }
//         else
//         {
//             pthread_mutex_unlock(td->_mutex);
//             break;
//         }
//         //不加这个会产生其他线程的饥饿问题,通过同步机制解决这个问题。
//         usleep(13);
//     }
//     printf("%s ... quit\n", name);
//     return nullptr;
// }void *getTicks(void *args)
{// 强转threaddata *td = static_cast<threaddata *>(args);const char *name = td->threadname.c_str();while (true){// 使用锁{lockGuard _lockGuard(&lock);  //C++ RAIIif (tickets > 0){usleep(1000);printf("who=%s,get a ticket:%d\n", name, tickets);tickets--;}else{break;}}// 不加这个会产生其他线程的饥饿问题,通过同步机制解决这个问题。usleep(13);}printf("%s ... quit\n", name);return nullptr;
}
int main()
{// 生成一个锁// pthread_mutex_t lock;// 初始化一个锁// pthread_mutex_init(&lock,nullptr);std::vector<threaddata *> dates;std::vector<pthread_t> tids;for (int i = 1; i <= NUM; i++){threaddata *date = new threaddata(i /*&lock*/);dates.push_back(date);pthread_t tid;// 创建子线程pthread_create(&tid, nullptr, getTicks, dates[i - 1]);tids.push_back(tid);}// 等待for (auto thread : tids){pthread_join(thread, nullptr);}// 释放资源for (auto td : dates){delete td;}// 释放一个锁// pthread_mutex_destroy(&lock);return 0;
}

线程安全和重入的概念:

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

四、死锁问题

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

死锁的四个必要条件:

1、互斥条件:一个资源每次只能被一个执行流使用。

2、请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不妨。

3、不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺。

4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

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

 五、线程同步

同步问题是保证数据安全的情况下,  让我们的线程访问资源具有一定的顺序性。

1、提出解决方案

Linux中的条件变量,条件变量必须依赖于锁的使用。因为申请锁资源的时候申请不上,所以我们需要让线程到条件变量上按一定顺序等待。

代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
//创建的线程数
#define NUM 5
int cnt = 0;
//创建锁和条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* Count(void* argc)
{//分离线程pthread_detach(pthread_self());uint64_t num = (uint64_t)argc;//访问临界资源while (true){//加锁以后其他线程访问不到pthread_mutex_lock(&lock);//线程访问到以后,查看资源是否具备,不具备进行等待,等待的过程中释放锁pthread_cond_wait(&cond,&lock);std::cout << "thread:" << num << ", cnt:" << cnt++ << std::endl;pthread_mutex_unlock(&lock);}
}
int main() 
{//创建线程for(uint64_t i = 0;i < NUM; i++){pthread_t tid;//创建线程pthread_create(&tid,nullptr,Count,(void*)i);usleep(1000);}sleep(3);std::cout << "main thread ctrl begin: " << std::endl;//唤醒线程while(true){sleep(1);pthread_cond_signal(&cond);std::cout << "signal a thread ... " << std::endl;}return 0;
}

2、CP --- 问题

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而 通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者 要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队 列就是用来给生产者和消费者解耦的。

生产者和消费者的行为,进行一定程度的解耦

生产者和消费者都是由线程承担。

执行流在做通信,如何高效安全的通信。看到同一份资源,共享资源,所以会有并发问题。

生产者 vs 生产者:互斥关系

消费者 vs 消费者:互斥关系 

生产者 vs 消费者:互斥,同步关系。  统一为三种关系。 

二种角色:生产和消费

一个交易场所:特定结构的内存空间。 统称为321原则。

优点:1、支持忙闲不均。 

           2、生产和消费进行解耦。

3、快速实现CP问题 

实现一个基于阻塞队列的生产消费者模型。

在多线程编程中阻塞队列 (Blocking Queue) 是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别 在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元 素的操作也会被阻塞,直到有元素被从队列中取出( 以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程 操作时会被阻塞)

代码:

#pragma once
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <queue>
//类模板
template <class T>
class BlockQueue
{static const int defaultmax = 5;
public:BlockQueue(int maxcap = defaultmax):maxcap_(maxcap){pthread_mutex_init(&mutex_,nullptr);pthread_cond_init(&_c_cond_,nullptr);pthread_cond_init(&_p_cond_,nullptr);lowwater = maxcap_/3;highwater = (maxcap_ * 2)/3;}T pop(){   pthread_mutex_lock(&mutex_);while(queue_.size() == 0)  //while循环是为了防止伪唤醒{pthread_cond_wait(&_c_cond_,&mutex_);}T out = queue_.front();queue_.pop();if(queue_.size() < lowwater) pthread_cond_signal(&_p_cond_);pthread_mutex_unlock(&mutex_);return out;}void push(T data){//互斥pthread_mutex_lock(&mutex_);//判断临界资源状态,没有就绪阻塞。while(queue_.size() == maxcap_) //while循环是为了防止伪唤醒{pthread_cond_wait(&_p_cond_,&mutex_);}queue_.push(data);if(queue_.size() > highwater) pthread_cond_signal(&_c_cond_);pthread_mutex_unlock(&mutex_);}~BlockQueue(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&_c_cond_);pthread_cond_destroy(&_p_cond_);}
private:std::queue<T> queue_;int maxcap_;    //极大值pthread_mutex_t mutex_; //互斥锁pthread_cond_t _c_cond_;  //消费条件变量pthread_cond_t _p_cond_;  //生产条件变量变量int lowwater;             int highwater;
};

#include "blockqueue.hpp"void* Consumer(void* argc)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(argc);while (true){sleep(1);int num = bq->pop();std::cout << "消费了一个任务:" << num << std::endl;}return nullptr;
}void* Productor(void* argc)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(argc);int data = 0;while(true){bq->push(data++);std::cout << "生产了一个任务!" << std::endl;}return nullptr;
}int main()
{pthread_t c,p;BlockQueue<int>* bq = new BlockQueue<int>();//消费者线程pthread_create(&c,nullptr,Consumer,bq);//生产者线程pthread_create(&p,nullptr,Productor,bq);//等待线程pthread_join(c,nullptr);pthread_join(p,nullptr);
}

生产者的数据可以从用户或者网络中产生,生产者生产数据也是要花时间获取的。消费者需要做数据的加工处理,也要花时间。两者在执行临界区外的代码是并发访问的。这就提高了效率。

判断资源状态为什么要在临界区内?因为判断临界资源调试是否满足,也是在访问临界资源!

pthread_cond_wait接口会自动释放锁。

如果线程wait时,被误唤醒了呢?多个线程被唤醒,而正好阻塞队列满了,但是没有被消费,还是生产者进程拿到了互斥锁还会往阻塞队列里面写数据。由于阻塞队列是满的,所以会发生写入数据错误。这就是伪唤醒。通过while循环防止伪唤醒。

 改成多线程模式

代码:

#pragma once
#include <iostream>
#include <string>std::string opers="+-*/%";enum{DivZero=1,ModZero,Unknown
};class Task
{
public:Task(int x, int y, char op) : data1_(x), data2_(y), oper_(op), result_(0), exitcode_(0){}void run(){switch (oper_){case '+':result_ = data1_ + data2_;break;case '-':result_ = data1_ - data2_;break;case '*':result_ = data1_ * data2_;break;case '/':{if(data2_ == 0) exitcode_ = DivZero;else result_ = data1_ / data2_;}break;case '%':{if(data2_ == 0) exitcode_ = ModZero;else result_ = data1_ % data2_;}            break;default:exitcode_ = Unknown;break;}}void operator ()(){run();}std::string GetResult(){std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=";r += std::to_string(result_);r += "[code: ";r += std::to_string(exitcode_);r += "]";return r;}std::string GetTask(){std::string r = std::to_string(data1_);r += oper_;r += std::to_string(data2_);r += "=?";return r;}~Task(){}private:int data1_;int data2_;char oper_;int result_;int exitcode_;
};
#include "blockqueue.hpp"
#include "Task.hpp"
#include <ctime>void *Consumer(void *argc)
{BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(argc);sleep(1);while (true){Task task = bq->pop();// 处理数据task.run();std::cout << "得到一个运算结果:" << task.GetResult() << std::endl;}return nullptr;
}void *Productor(void *argc)
{int len = opers.size();BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(argc);// int data = 0;// 产生数据while (true){int x = rand() % 10 + 1; //[1,10]int y = rand() % 10 + 1; //[1,10]char op = opers[rand() % len];Task task(x, y, op);bq->push(task);std::cout << "生产了一个任务!" << task.GetTask() << std::endl;sleep(1);}return nullptr;
}
//BlockQueue 和上面的BlockQueue相同int main()
{srand(time(NULL));pthread_t c[5], p[5];BlockQueue<Task> *bq = new BlockQueue<Task>();// 消费者线程for (int i = 0; i < 5; i++){pthread_create(c + i, nullptr, Consumer, bq);}// 生产者线程for (int i = 0; i < 5; i++){pthread_create(p + i, nullptr, Productor, bq);}// 等待线程for (int i = 0; i < 5; i++){pthread_join(c[i], nullptr);}for (int i = 0; i < 5; i++){pthread_join(p[i], nullptr);}
}

六、POSIX信号量

上述的阻塞队列,queue被当作整体使用,queue只有一份,加锁,但是共享资源也可以被看作多份。

信号量的本质是一把计数器,那么这把计数器的本质是用来描述资源数目的,把资源是否就绪放在了临界区之外,申请信号量时,其实就间接的已经在做判断了。

基于环形队列的生产消费模型。满了的时候和空的时候head和tail指向的是同一个位置。空和满的时候,tail和head指向的是同一个位置,无法判断是空还是满。

解决方案:添加计数器。空一个位置。

1、指向同一个位置的时候,不能同时访问。

空:生产者。

满:消费者。

2、消费者不能超过生产者。 

空或者满的时候会指向同一个位置,不空和不满的时候指向不同的位置,我们可以同时访问!

3、生产者不能套消费者一个圈。正常使用必须满足这三个条件。

Productor 关注环形队列还有多少剩余空间。SpaceSem空余信号量

Consumer关注环形队列还有多少剩余数据。DataSem 数据信号量

代码:

#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
const static int defaultcap = 30;
template <class T>
class RingQueue
{
private://封装PV操作void p(sem_t& sem){sem_wait(&sem);}void v(sem_t& sem){sem_post(&sem);}void lock(pthread_mutex_t& lock){pthread_mutex_lock(&lock);}void unlock(pthread_mutex_t& lock){pthread_mutex_unlock(&lock);}
public://给构造函数传入我们需要多大的环形队列,这里使用缺省参数给定RingQueue(int num = defaultcap):cap_(num),_ringQ(num),c_step_(0),p_step_(0){//初始的资源信号量有0个sem_init(&_cdata_sem,0,0);//初始的空信号量有cap_个sem_init(&_pspace_sem,0,cap_);pthread_mutex_init(&_c_lock,nullptr);pthread_mutex_init(&_p_lock,nullptr);}void push(const T& in){//要访问临界资源首先申请信号量p(_pspace_sem); //先申请信号量再加锁的原因是: 1、pv操作是原子的 2、提高并发度lock(_p_lock);_ringQ[p_step_] = in;//位置后移,维护环形队列p_step_++;p_step_%=cap_;unlock(_p_lock);v(_cdata_sem);}void pop(T* out){p(_cdata_sem);lock(_c_lock);*out = _ringQ[c_step_];c_step_++;c_step_%= cap_;unlock(_c_lock);v(_pspace_sem);}~RingQueue(){sem_destroy(&_cdata_sem);sem_destroy(&_pspace_sem);pthread_mutex_destroy(&_c_lock);pthread_mutex_destroy(&_p_lock);}
private://这里我们需要实现一个基于环形队列的生产消费模型。//使用STL容器queuestd::vector<T> _ringQ;int cap_;  //定义环形队列的最大值//还需要定义两个指针,指向当前消费者和生产者资源的位置。int c_step_;int p_step_;//考虑到需要资源数量,这里使用信号量作为线程同步互斥的实现。///由于生产者和消费者关心的资源不同我们需要两个信号量表示生产消费者所需要的模型//定义两个信号量,一个表示空位置有几个,另一个表示有多少个资源数量sem_t _cdata_sem;sem_t _pspace_sem;//为了实现多线程的互斥我们需要定义两把锁pthread_mutex_t _c_lock;pthread_mutex_t _p_lock;
};
#include "RingQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <unistd.h>
#define PNUM 3
#define CNUM 2// 线程执行的函数
void *Productor(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){// 生成数据int data1 = rand() % 10 + 1;usleep(3);int data2 = rand() % 10;char op = opers[rand() % opers.size()];Task t(data1, data2, op);// 生成任务rq->push(t);std::cout << "Productor task done, task is : " << t.GetTask() << std::endl;sleep(1);}return nullptr;
}
void *Consumer(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);// 拿到数据while (true){Task t;rq->pop(&t);// 处理任务t();std::cout << "Consumer get task, task is : " << t.GetTask() << " result: " << t.GetResult() << std::endl;}// sleep(1);return nullptr;
}int main()
{srand(time(nullptr) ^ getpid());// 实例化RingQueue类的对象,需要放入任务。所以类模板实例化为Task类RingQueue<Task> *rq = new RingQueue<Task>();// 生成生产消费者模型的线程// 是共享区中的线程结构体的地址。pthread_t c[CNUM], p[PNUM];for (int i = 0; i < PNUM; i++){pthread_create(p + i, nullptr, Productor, rq);}for (int i = 0; i < CNUM; i++){pthread_create(c + i, nullptr, Consumer, rq);}for (int i = 0; i < PNUM; i++){pthread_join(p[i], nullptr);}for (int i = 0; i < CNUM; i++){pthread_join(c[i], nullptr);}return 0;
}

七、线程池

C++类内创建线程,使用原生线程。

代码:

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include "Task.hpp"struct ThreadInfo
{pthread_t tid;std::string name;
};
// 默认最大可以创建多少个线程
const static int defaultnum = 5;
template <class T>
class ThreadPool
{
private:void lock(){pthread_mutex_lock(&lock_);}void unlock(){pthread_mutex_unlock(&lock_);}void wakeup(){pthread_cond_signal(&cond_);}void threadsleep(){pthread_cond_wait(&cond_, &lock_);}bool GetEmpty(){return tasks_.empty();}std::string GetThreadTid(pthread_t tid){for (auto &t : threads_){if (t.tid == tid){return t.name;}}return "None";}public:static void *threadHandler(void *argc){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(argc);std::string name = tp->GetThreadTid(pthread_self());while (true){// 拿任务处理任务tp->lock();while (tp->GetEmpty()){tp->threadsleep();}T t = tp->pop();tp->unlock();t();std::cout << name << " run, "<< "result: " << t.GetResult() << std::endl;}}void Start(){// 创建线程int num = threads_.size();for (int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid), nullptr, threadHandler, this);}}void push(const T &in){// 向队列里push任务lock();tasks_.push(in);// 唤醒进程。wakeup();unlock();}T pop(){T t = tasks_.front();tasks_.pop();return t;}static ThreadPool<T> *GetInstance_(){// 防止多次争夺锁if (nullptr == tp_){pthread_mutex_lock(&mutex_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&mutex_);}return tp_;}private:ThreadPool(int num = defaultnum) : threads_(num){// 在这里初始化互斥锁和条件变量pthread_mutex_init(&lock_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&lock_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;private:// 我们需要一个vector来管理线程的信息std::vector<ThreadInfo> threads_;// 需要一个队列存放任务std::queue<Task> tasks_;// 需要一个锁pthread_mutex_t lock_;// 需要一个条件变量实现同步和互斥pthread_cond_t cond_;// 修改成单例模式static ThreadPool<T> *tp_;static pthread_mutex_t mutex_;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::mutex_ = PTHREAD_MUTEX_INITIALIZER;#include "ThreadPool.hpp"
#include <ctime>
#include <unistd.h>
int main()
{//ThreadPool<Task>* tp = new ThreadPool<Task>(5);srand(time(nullptr)^getpid());  //懒汉单例模式ThreadPool<Task>::GetInstance_()->Start();  while (true){//生成任务int x = rand()%10 + 1;usleep(3);int y = rand()%10;char op = opers[rand()%opers.size()];Task t(x,y,op);//交给线程池处理ThreadPool<Task>::GetInstance_()->push(t);std::cout << "main thread make task: " << t.GetTask() << std::endl;}return 0;
}

八、线程安全的单例模式

某些类 , 只应该具有一个对象 ( 实例 ), 就称之为单例 .
例如一个男人只能有一个媳妇。
在很多服务器开发场景中 , 经常需要让服务器加载很多的数据 ( 上百 G) 到内存中 . 此时往往要用一个单例的类来管理这些数据。

懒汉模式:延迟加载,能够优化服务器的启动速度。线程池已经改成了懒汉模式的单例模式

饿汉模式:直接加载 

全局变量在程序启动的时候就会创建。 

九、STL,智能指针和线程安全

STL容器不是线程安全的。

智能指针:

对于 unique_ptr, 由于只是在当前代码块范围内生效 , 因此不涉及线程安全问题 .
对于 shared_ptr, 多个对象需要共用一个引用计数变量 , 所以会存在线程安全问题 . 但是标准库实现的时候考虑到了这 个问题, 基于原子操作 (CAS) 的方式保证 shared_ptr 能够高效 , 原子的操作引用计数

十、其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行 锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不 等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁

 自旋锁:取决于其他线程执行临界区的时长,之前学到的锁都是挂起等待锁。会有资源上的浪费。我们可以使用自旋锁循环访问临界资源。可以使用这个接口实现一个自旋锁。需要外套一个while循环。

pthread库里面有自旋锁的调用接口。

访问临界区的时间长短,决定了是否需要自旋锁。

十一、读者写者问题

遵守321原则

三种关系:写 vs 写 互斥。

                  写 vs 读 互斥,同步。

                  读 vs 读 共享关系。

为什么读者vs读者是共享关系,消费者之间却是互斥?

数据的存留问题,因为读者不会把数据拿走,而消费者会把数据拿走。

两种角色:读者R,写者W,线程承担。

一个交易场所:数据交换的地点。

相关接口:

读写的理解 :读多和写少的情况。读写之间的同步问题。读者优先,写者的饥饿问题。写者优先,读者饥饿问题。

伪代码:

系统部分完结。 

相关文章:

Linux系统编程 --- 多线程

线程&#xff1a;是进程内的一个执行分支&#xff0c;线程的执行粒度&#xff0c;要比进程要细。 一、线程的概念 1、Linux中线程该如何理解 地址空间就是进程的资源窗口。 在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。更准确的定义是&#xff1…...

Grafana中的rate与irate以及histogram

用法 rate rate函数用于计算一个时间序列在给定时间范围内的平均速率。它对每个数据点进行线性插值来计算速率&#xff0c;因此对于平滑和稳定的数据来说&#xff0c;rate是一个不错的选择。语法如下&#xff1a; rate(metric_name[time_range])metric_name: 指标名称。time…...

什么是网络安全态势感知

态势感知是一种基于环境的、动态、整体地洞悉安全风险的能力&#xff0c;是以安全大数据为基础&#xff0c;从全局视角提升对安全威胁的发现识别、理解分析、响应处置能力的一种方式、最终是为了决策与行动&#xff0c;是安全能力的落地 态势感知的重要性 随着网络与信息技术的…...

php 在app中唤起微信app进行支付,并处理回调通知

<?phpnamespace app\api\controller;use think\facade\Db; use think\facade\Log;class Wxzf {...

高效同步与处理:ADTF流服务在自动驾驶数采中的应用

目录 一、ADTF 流服务 1、流服务源&#xff08;Streaming Source&#xff09; 2、流服务汇&#xff08;Streaming Sink&#xff09; 二、数据链路 1、数据管道&#xff08;Data Pipe&#xff09; 2、子流&#xff08;Substreams&#xff09; 3、触发管道&#xff08;Tri…...

【Arduino】ATmega328PB 连接 LSM6DS3 姿态传感器,并读数据(不确定 ESP 系列是否可行,但大概率是可行的)

总览 1.初始化 ATmega328PB&#xff0c;默认大家已经完成了 328 的配置准备工作&#xff0c;已经直接能够向里面写入程序 2.接线&#xff0c;然后验证 mega328 的 I2C 设备接口能否扫描到 LSM6DS3 3.编写代码&#xff0c;上传&#xff0c;查看串口数据。完成。 一、初始化 AT…...

live2d + edge-tts 优雅的实现数字人讲话 ~

震惊&#xff01;live2d数字人竟开口说话 ~ 之前有想做数字人相关项目&#xff0c;查了一些方案。看了一些三方大厂的商用方案&#xff0c;口型有点尴尬&#xff0c;而且很多是采用视频流的方案&#xff0c;对流量的消耗很大。后来了解了live2d 技术&#xff0c;常在博客网页上…...

二进制安装php

下载php二进制包&#xff1a; 官网地址&#xff1a;https://www.php.net/releases/ PHP: Releaseshttps://www.php.net/releases/在里边可以选择自己要下载的包进行下载&#xff1b; 下载完成后进行解压&#xff1a; tar xvzf php-7.3.12.tar.gz 解压后 进入目录进行预编…...

旧版Pycharm支持的python版本记录

版权声明&#xff1a;本文为博主原创文章&#xff0c;如需转载请贴上原博文链接&#xff1a;旧版Pycharm支持的python版本记录-CSDN博客 前言&#xff1a;近期由于打算研究GitHub上一个开源量化交易平台开发框架&#xff0c;但是该框架是基于python3.10的版本开发&#xff0c;所…...

java实现七牛云内容审核功能,文本、图片和视频的内容审核(鉴黄、鉴暴恐、敏感人物)

目录 1、七牛云内容审核介绍 2、查看内容审核官方文档 2.1、文本内容审核 2.1.1、文本内容审核的请求示例 2.1.2、文本内容审核的返回示例 2.2、图片内容审核 2.2.1、请求参数 2.2.2、返回参数 2.3、视频内容审核 3、代码实现 3.1、前期代码准备 3.2、文本内容审核…...

C++面试基础系列-struct

系列文章目录 文章目录 系列文章目录C面试基础系列-struct1.C中struct2.C中struct2.1.同名函数2.2.typedef定义结构体别名2.3.继承 3.总结3.1.C和C中的Struct区别 4.struct字节对齐5.struct与const 关于作者 C面试基础系列-struct 1.C中struct struct里面只能放数据类型&#…...

代码随想录算法训练营 | 动态规划 part05

完全背包 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品都有无限个&#xff08;也就是可以放入背包多次&#xff09;&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 例子&#xff1a; 背包可容纳重…...

英特尔XPU大模型应用创新

...

仿Muduo库实现高并发服务器——socket网络通信模块

本项目就是基于TCP网络通信搭建的。 TCP: 客户端&#xff1a;socket(),connect(). 服务端&#xff1a;socket(),bind(),listen(),accept(). 下面代码就是对原生API网络套接字的封装。需要熟悉原生API网络套接字接口。 下面这段代码&#xff0c;没什么好讲的&#xff0c;就不…...

模型 神经网络(通俗解读)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。仿脑智能&#xff0c;深度学习&#xff0c;精准识别。 1 神经网络的应用 1.1 鸢尾花分类经典问题 神经网络的一个经典且详细的经典应用是鸢尾花分类问题 。主要是通过构建一个神经网络模型来自动区分…...

事务的使用

1.如何使用事务: 1.1.事务的完成过程&#xff1a; 1.步骤1&#xff1a;开启事务2.步骤2&#xff1a;一系列的DML操作3.步骤3&#xff1a;事务结束状态&#xff1a;提交事务(COMMIT)&#xff0c;中止事务&#xff08;事务回滚ROLLBACK&#xff09; 1.2.事务分类&#xff1a; …...

【免费】企业级大模型应用推荐:星环科技无涯·问知

无涯问知是星环科技发布的大模型应用系统&#xff0c;那么我们先简单了解下星环科技吧&#xff01; 星环科技&#xff08;股票代码&#xff1a;688031&#xff09;致力于打造企业级大数据和人工智能基础软件&#xff0c;围绕数据的集成、存储、治理、建模、分析、挖掘和流通等数…...

从〇 搭建PO模式的Web UI自动化测试框架

Page Object模式简介 核心思想 将页面元素和操作行为封装在独立的类中&#xff0c;形成页面对象&#xff08;Page Object&#xff09;。每个页面对象代表应用程序中的一个特定页面或组件。 优点&#xff1a; 代码复用性高 页面对象可以在多个测试用例中复用。 易于维护 …...

在Ubuntu中重装Vscode(没有Edit Configurations(JSON)以及有错误但不标红波浪线怎么办?)

在学习时需要将vscode删除重装&#xff0c;市面上很多方法都不能删干净&#xff0c;删除之后拓展都还在。因此下面的方法可以彻底删除。注意&#xff0c;我安装时使用的是snap方法。 如果你的VScode没有Edit Configurations(JSON)&#xff0c;以及有错误但不标红波浪线的话&…...

Oracle 用户-表空间-表之间关系常用SQL

问题&#xff1a; 当某一个表数据量特别大&#xff0c;突然插入数据一直失败&#xff0c;可能是表空间不足&#xff0c;需要查看表的使用率 用户-表空间-表之间关系&#xff1a;用户可以有多个表空间&#xff0c;表空间可以有多个表&#xff0c;表只能拥有一个表空间和用户 1.…...

家政服务管理系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;管理阿姨管理&#xff0c;家政公司管理&#xff0c;服务项目管理&#xff0c;家政预约管理&#xff0c;评价管理&#xff0c;留言板管理&#xff0c;系统管理 微信端账号功能包括…...

【算法】并查集的介绍与使用

1.并查集的概论 定义&#xff1a; 并查集是一种树型的数据结构&#xff0c;用于处理一些不相交集合的合并及查询问题&#xff08;即所谓的并、查&#xff09;。比如说&#xff0c;我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。 主要构成&#xff1a; …...

Shell——运算符

在 Shell 编程中&#xff0c;运算符用于执行各种类型的操作&#xff0c;如算术运算、字符串比较、文件测试等。以下是 Shell 中常用的运算符分类和示例&#xff1a; 1. 算术运算符 Shell 中使用 expr 或 $(( ... )) 来进行算术运算。 : 加法-: 减法*: 乘法/: 除法%: 取余**:…...

SweetAlert2

1. SweetAlert2 SweetAlert2是一个基于JavaScript的库, 用于在网页上替换标准的警告框(alert), 确认框(confirm)和提示框(prompt), 并提供更加美观和用户友好的界面.需要在项目中引入SweetAlert2, 可以通过CDN链接或者将库文件下载到你的项目中来实现这一点. 通过CDN引入:<…...

c语言中比较特殊的输入函数

目录 一.getchar()函数 1.基本功能 2.使用方法 (1).读取单个字符 (2).读取多个字符&#xff08;直到遇到换行符&#xff09; (3).处理输入中的空白字符 3.返回值 4.应用场景 5.注意事项 二.fgets()函数 1.函数原型 2.工作原理 3.使用示例 (1).从标准输入读取一行…...

Java版自动化测试之Selenium

1. 准备 编程语言&#xff1a;Java JDK版本&#xff1a;17 Maven版本&#xff1a;3.6.1 2. 开始 声明&#xff1a;本次只测试Java的Selenium自动化功能 本次示例过程&#xff1a;打开谷歌游览器&#xff0c;进入目标网址&#xff0c;找到网页的输入框元素&#xff0c;输入指…...

【计算机网络】——计算机网络的性能指标

速率&#xff08;speed&#xff09; 连接在计算机网络上的主机在数字信道上传送数据的速率。 影响条件&#xff1a; 带宽&#xff08;band width&#xff09; 指在固定的时间可传输的资料数量 单位&#xff1a;bps或HZ 吞吐量&#xff08;throughtput&#xff09; 指对网络、…...

MongoDB数据类型介绍

MongoDB作为一种高性能、开源、无模式的文档型数据库&#xff0c;支持丰富的数据类型&#xff0c;以满足各种复杂的数据存储需求。本文将详细介绍MongoDB支持的主要数据类型&#xff0c;包括数值类型、字符串类型、日期和时间类型、布尔类型、二进制类型、数组、对象以及其他扩…...

【SpringBoot】SpringBoot 中 Bean 管理和拦截器的使用

目录 1.Bean管理 1.1 自定义Bean对象 1.2 Bean的作用域和生命周期 2.拦截器的使用 1.Bean管理 默认情况下&#xff0c;Spring项目启动时&#xff0c;会把我们常用的Bean都创建好放在IOC容器中&#xff0c;但是有时候我们自定义的类需要手动配置bean&#xff0c;这里主要介绍…...

Spring IoCDI(中)--IoC的进步

通过上文的讲解和学习, 我们已经知道了Spring IoC 和DI的基本操作, 接下来我们来系统的学习Spring IoC和DI 的操作. 前⾯我们提到IoC控制反转&#xff0c;就是将对象的控制权交给Spring的IOC容器&#xff0c;由IOC容器创建及管理对 象&#xff0c;也就是bean的存储。 1. Bean的…...

读软件开发安全之道:概念、设计与实施02经典原则

1. CIA原则 1.1. 软件安全都构建在信息安全的三大基本原则之上&#xff0c;即机密性(confidentiality)、完整性(integrity)和可用性(availability) 1.2. 双方交换的数据 1.2.1. 从技术上看&#xff0c;端点之间的数据交换本身就会削弱交互的机密性 1.2.2. 隐藏通信数据量的一…...

MySQL中处理JSON数据:大数据分析的新方向,详解与示例

文章目录 1. MySQL中的JSON数据类型2. JSON函数和运算符3. 创建JSON列的表4. 插入JSON数据5. 查询JSON数据6. 复杂查询和聚合7. JSON 数据的索引8. 总结 在当今的大数据时代&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;作为一种轻量级的数据交换格式&a…...

【图形学】TA之路-矩阵

在 Unity 中&#xff0c;矩阵广泛用于处理各种图形变换&#xff0c;例如平移、旋转、缩放等。矩阵的使用不仅限于三维空间&#xff0c;还可以应用于二维空间的操作。了解矩阵及其运算对于游戏开发和计算机图形学非常重要。Unity 中使用的是行向量不是列向量&#xff0c;这个要注…...

LAMM: Label Alignment for Multi-Modal Prompt Learning

系列论文研读目录 文章目录 系列论文研读目录文章题目含义AbstractIntroductionRelated WorkVision Language ModelsPrompt Learning MethodologyPreliminaries of CLIPLabel AlignmentHierarchical Loss 分层损失Parameter Space 参数空间Feature Space 特征空间Logits Space …...

mac编译opencv 通用架构库的记录

1,通用架构 (x86_64;arm64&#xff09;要设置的配置项&#xff1a; CPU_BASELINE CPU_DISPATCH 上面这两个我设置成SSE_3&#xff0c;其他选项未尝试&#xff0c;比如不设置。 CMAKE_OSX_ARCHITECTURES:x86_64;arm64 WITH_IPP:不勾选 2,contrib库的添加&#xff1a; 第一次…...

Python 向IP地址发送字符串

Python 向IP地址发送字符串 在网络编程中&#xff0c;使得不同设备间能够进行数据传输是一项基本任务。Python提供了强大的库&#xff0c;帮助开发者轻松地实现这种通信。本文将介绍如何使用Python通过UDP协议向特定的IP地址发送字符串信息。 UDP协议简介 UDP&#xff08;用…...

上升响应式Web设计:纯HTML和CSS的实现技巧-1

响应式Web设计&#xff08;Responsive Web Design, RWD&#xff09;是一种旨在确保网站在不同设备和屏幕尺寸下都能良好运行的网页设计策略。通过纯HTML和CSS实现响应式设计&#xff0c;主要依赖于媒体查询&#xff08;Media Queries&#xff09;、灵活的布局、可伸缩的图片和字…...

利用java结合python实现gis在线绘图,主要技术java+python+matlab+idw+Kriging

主要技术javapythonmatlabidwKriging** GIS中的等值面和等高线绘图主要用于表达连续空间数据的分布情况&#xff0c;特别适用于需要展示三维空间中某个变量随位置变化的应用场景。 具体来说&#xff0c;以下是一些适合使用GIS等值面和等高线绘图的场景&#xff1a; 地形与地貌…...

Android全面解析之context机制(三): 从源码角度分析context创建流程(下)

前言 前面已经讲了什么是context以及从源码角度分析context创建流程&#xff08;上&#xff09;。限于篇幅把四大组件中的广播和内容提供器的context获取流程放在了这篇文章。广播和内容提供器并不是context家族里的一员&#xff0c;所以他们本身并不是context&#xff0c;因而…...

执行docker compose命令出现 Additional property include is not allowed

问题背景 在由docker-compose.yml的文件目录下执行命令 docker compose up -d 出现错误 Additional ininoperty include is not allowed 原因 我的docker-compose.yml 文件中出现了include标签旧版本的docker-compose 不支持此标签 解决办法 下载支持的docker-compose 解决…...

STM32通过I2C硬件读写MPU6050

目录 STM32通过I2C硬件读写MPU6050 1. STM32的I2C外设简介 2. STM32的I2C基本框图 3. STIM32硬件I2C主机发送流程 10位地址与7位地址的区别 7位主机发送的时序流程 7位主机接收的时序流程 4. STM32硬件与软件的波形对比 5. STM32配置硬件I2C外设流程 6. STM32的I2C.h…...

ubuntu2204-中文输入法-pycharm-python-django开发环境搭建

文章目录 1.系统常用设置1.1.安装中文输入法1.2.配置输入法1.3.卸载输入法1.4.配置镜像源2.java安装3.pycharm安装与启动4.卸载ubuntu2204默认版本5.安装Anaconda5.1.安装软件依赖包5.2.安装命令5.3.激活安装5.4.常用命令5.5.修改默认启动源6.安装mysql6.1.离线安装mysql6.2.在…...

【学习笔记】Matlab和python双语言的学习(一元线性回归)

文章目录 前言一、一元线性回归回归分析的一般步骤一元线性回归的基本形式回归方程参数的最小二乘法估计对回归方程的各种检验估计标准误差的计算回归直线的拟合优度判定系数显著性检验 二、示例三、代码实现----Matlab四、代码实现----python回归系数的置信区间公式残差的置信…...

LeetCode //C - 316. Remove Duplicate Letters

316. Remove Duplicate Letters Given a string s, remove duplicate letters so that every letter appears once and only once. You must make sure your result is the smallest in lexicographical order among all possible results. Example 1: Input: s “bcabc”…...

【ARM+Codesys 客户案例 】RK3568/A40i/STM32+CODESYS在工厂自动化中的应用:PCB板焊接机

现代化生产中&#xff0c;电子元件通常会使用自动化设备来进行生产&#xff0c;例如像PCB&#xff08;印刷电路板&#xff09;的组装。但是生产过程中也会面临一些问题&#xff0c;类似于如何解决在PCB板上牢固、精准地安装各种组件呢&#xff1f;IBL Lttechnik GmbH公司的CM80…...

【二分查找】--- 初阶题目赏析

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 算法Joureny 上篇我们讲解了关于二分的朴素模板和边界模板&#xff0c;本篇博客我们试着运用这些模板。 &#x1f3e0; 搜索插入位置 &#x1f4cc; 题目…...

【PostgreSQL003】PostgreSQL数据表空间膨胀,磁盘爆满,应用宕机(经验总结,已更新)

1.一直以来想写下基于PostgreSQL的系列文章&#xff0c;作为较火的数据ETL工具&#xff0c;也是日常项目开发中常用的一款工具&#xff0c;最近刚好挤时间梳理、总结下这块儿的知识体系。 2.熟悉、梳理、总结下PostgreSQL数据库相关知识体系。空间膨胀&#xff08;主键、外键、…...

C语言第20天笔记

文件操作 概述 什么是 文件 文件时保存在外存储器上&#xff08;一般代指磁盘&#xff0c;也可以是U盘、移动硬盘等&#xff09;的数据的集合。 文件操作体现在哪几个方面 1. 文件内容的读取 2. 文件内容的写入 数据的读取和写入可被视为针对文件进行输入和输出的操作&a…...

为什么穷大方

为什么有些人明明很穷&#xff0c;却非常的大方呢&#xff1f; 因为他们认知太低&#xff0c;根本不懂钱的重要性&#xff0c;总是想着及时享乐&#xff0c;所以一年到头也存不了什么钱。等到家人孩子需要用钱的时候&#xff0c;什么也拿不出来&#xff0c;还到处去求人。 而真…...

HiveSQL实战——大数据开发面试高频SQL题

查询每个区域的男女用户数 0 问题描述 每个区域内男生、女生分别有多少个 1 数据准备 use wxthive; create table t1_stu_table (id int,name string,class string,sex string ); insert overwrite table t1_stu_table values(4,张文华,二区,男),(3,李思雨,一区,女),(1…...