Linux学习记录——이십오 多线程(2)

news2024/10/3 8:17:18

文章目录

  • 1、理解原生线程库
    • 线程局部存储
  • 2、互斥
    • 1、并发代码(抢票)
    • 2、锁
    • 3、互斥锁的实现原理
  • 3、线程封装
    • 1、线程本体
    • 2、封装锁
  • 4、线程安全
  • 5、死锁
  • 6、线程同步
    • 1、条件变量
      • 1、接口
      • 2、demo代码


1、理解原生线程库

线程库在物理内存中存在,也映射到了地址空间的共享区,那么每个线程就可以很方便地去实现自己的代码,库里也包括了线程切换,管理等代码。库对于线程的管理也是先描述再组织,它会创建类似管理进程的TCB,TCB管理LWP,也就是轻量级进程,用户把代码给TCB,LWP也会提供一些可用的东西给到TCB。

线程库中有动态库的一部分,以及各个线程的TCB,TCB里有线程局部存储,线程栈以及其他属性。要想找到一个TCB,只需要找到它的起始地址即可,而这个地址就是线程ID,是一个地址数据,所以这个数据就比较大。

这个线程ID是一个用户级线程ID,是一个虚拟地址。

每个线程都要有自己独立的栈结构,线程栈在TCB里。主线程用的是进程系统栈,新线程用的是库中提供的栈。

当用户用pthread_create创建进程时,会在库中创建描述线程的相关结构体,创建轻量级进程,创建TCB,里面包含线程在用户空间定义的各种东西,轻量级进程、地址以及用户让轻量级进程执行的方法传给系统,系统就会调度这个轻量级进程,然后运行。

C++也有多线程,C++里的线程实现接口也封装了系统的线程库,所以执行C++文件的makefile需要这样写

在这里插入图片描述

#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

void run1()
{
    while(true)
    {
        cout << "thread 1" << endl;
        sleep(1);
    }
}

void run2()
{
    while(true)
    {
        cout << "thread 2" << endl;
        sleep(1);
    }
}

int main()
{
    thread th1(run1);
    thread th2(run2);
    th1.join();
    th2.join();
    return 0;
}

线程局部存储

几个线程执行同一个方法,那么里面的局部变量是只有一个,几个线程轮番对它进行操作,还是每次调用方法每次的变量都是独一份的?实际上是独立的,每个线程都有一份局部变量,说明这些局部变量在线程栈上,这些变量的地址也都不一样。那么变量名和地址应当怎样看待?

函数在调用之前会被编译,声明变量的代码会被转换为汇编代码,在计算机内部,对于一个进程的栈会存储它的栈顶(esp)和栈底(ebp),开辟空间的时候,会让ebp减去一个变量类型占用的字节,减去之后所在的那个地址就是这个变量的地址。这也就是运行时开辟空间,换成特定的代码去创建变量。ebp和esp是CPU内部的寄存器,只要更改ebp和esp就会切换线程的栈。所以不同线程的开辟的变量的地址也就不同,ebp和esp一换就到了另一个栈,然后再开辟空间。创建变量也可以看作通过偏移量来确定怎么创建。

取地址的时候取的是最低的地址,因为ebp是减一个数字来开辟空间,所以低地址加上偏移量就是栈底地址。

那么全局变量呢?

全局变量多个线程共享,它开辟在地址空间的已初始化数据段。如果不想被所有线程共享,那就在前面加上__thread,这时候变量就存在线程局部存储,从已初始化数据段拷贝到局部存储。打印出来的时候会发现地址变得更大了,是因为线程的库在堆栈之间,已初始化数据段的地址要比堆的地址低。

2、互斥

多线程中大部分资源会被共享,所以一定存在并发访问的情况,也就是多个线程访问同一个资源。那么为了解决这个问题,一个线程访问一个资源,其他线程就不允许再访问,这也就是串行式访问,也就是互斥,这些资源就是临界性资源,访问临界资源的代码就是临界区。

看一下并发访问的场景。AB两个线程访问同一个变量gval,也就是共享资源。数据在内存,但是计算在CPU,所以执行流要把数据加载到寄存器中,然后进行计算操作,再把修改后的数据写会到内存中,也就是3步。假设gval == 100,线程A把变量放到寄存器,–,gval变成99后,这时候A的时间片到了,那么系统就会把它停止,A就会带着自己的上下文以及运算好的变量挂起了。

接着B再运行,B也做一样的动作,但是B认为gval还是100,因为A的返回动作并没有做,然后B计算好,再写回到内存中,假设AB都是while循环里去–,那么B回去后再次重复刚才的动作,把值为99的gval放到寄存器,再去计算;一直循环,假设gval被减到了10,然后又减到了9,返回之前像A一样被还没写回去就被迫停止。

B结束后又轮到了A,A会继续进行写回去的动作,所以会把原本为10的数据又改成了99。对全局变量做访问,没有保护的话,会存在并发访问的问题,进而导致数据不一致问题。

共享资源如果被保护起来,就叫临界资源。任何一个线程都有代码访问临界资源,这部分代码就叫做临界区,当然线程也有不访问临界资源的,这些部分就是非临界区。想让多个线程安全地访问临界资源,就有互斥访问,加锁等方式。

1、并发代码(抢票)

int tickets = 10000;

void* threadRoutine(void* name)
{
    string tname = static_cast<const char*>(name);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(2000);//模拟抢票花费的时间, usleep的参数是微秒级别
            cout << tname << "get a ticket: " << tickets-- << endl;
        }
        else break;
    }
    return nullptr;
}

int main()
{
    pthread_t t[4];
    int n = sizeof(t) / sizeof(t[0]);
    for(int i = 0; i < n; ++i)
    {
        char* data = new char[64];
        snprintf(data, 64, "thread-%d", i + 1);
        pthread_create(t + i, nullptr, threadRoutine, data);
    }
    for(int i = 0; i < n; ++i)
    {
        pthread_join(t[i], nullptr);
    }
    return 0;
}

这样会最终变成负数。当票数还有1个时,有可能几个线程都可以进去,如果进去的线程还没开始操作那就会持续进入线程,进入的线程也有可能时间片到了,在tickets–这里,存在多个线程的话,那么最终就会出现负数,所以tickets > 0,和tickets–两个地方都是临界区。

2、锁

为了加锁,需要引入pthread.h这个头文件

在这里插入图片描述

初始化有两种方式,只要锁是全局的或者静态的,那么就可以按照最后一行那样,pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER,之后也不需要销毁;另一种就是局部锁,这就必须初始化+销毁。pthread_mutex_t就是互斥锁类型,先初始化锁,才能让锁可用,最后用完要销毁。

在这里插入图片描述

加解锁接口。如果没加上,那就阻塞等待,直到加上。

int tickets = 10000;
pthread_mutex_t mutex;//必须先申请一把锁<F7>

void* threadRoutine(void* name)
{
    string tname = static_cast<const char*>(name);
    while(true)
    {
        pthread_mutex_lock(&mutex);//所有线程都得遵守这个规则
        if(tickets > 0)
        {
            usleep(2000);//模拟抢票花费的时间, usleep的参数是微秒级别
            cout << tname << "get a ticket: " << tickets-- << endl;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_mutex_init(&mutex, nullptr);
    pthread_t t[4];
    int n = sizeof(t) / sizeof(t[0]);
    for(int i = 0; i < n; ++i)
    {
        char* data = new char[64];
        snprintf(data, 64, "thread-%d", i + 1);
        pthread_create(t + i, nullptr, threadRoutine, data);
    }
    for(int i = 0; i < n; ++i)
    {
        pthread_join(t[i], nullptr);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

这样改后,速度会变慢,因为有加锁的过程。但会发现全部都是一个线程在抢票。因为一个线程在加锁,另外几个线程都在等待,抢锁的这个线程时间片没到,其他线程就一直等待,所以会出现只有一个线程在抢票。

在这里插入图片描述

改成带有类的代码

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <phread.h>
#include <thread>
#include <ctime>
using namespace std;

int tickets = 10000;
//pthread_mutex_t mutex;//必须先申请一把锁

class TData
{
public:
    TData(const string& name, pthread_mutex_t* mutex):_name(name), _pmutex(mutex)
    {}
    ~TData()
    {}
public:
    string _name;
    pthread_mutex_t* _pmutex;
}

void* threadRoutine(void* args)
{
    TData* td = static_cast<TData*>(args);
    //string tname = static_cast<const char*>(name);
    while(true)
    {
        pthread_mutex_lock(td->_pmutex);//所有线程都得遵守这个规则
        if(tickets > 0)
        {
            usleep(2000);//模拟抢票花费的时间, usleep的参数是微秒级别
            cout << td->_name << "get a ticket: " << tickets-- << endl;
            pthread_mutex_unlock(td->_pmutex);
        }
        else
        {
            pthread_mutex_unlock(td->_pmutex);
            break;
        }

    }
    return nullptr;
}

int main()
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);
    pthread_t tids[4];
    int n = sizeof(t) / sizeof(t[0]);
    for(int i = 0; i < n; ++i)
    {
        char name[64];
        snprintf(name, 64, "thread-%d", i + 1);
        TData* td = new TData(name, &mutex);
        pthread_create(tids + i, nullptr, threadRoutine, td);
    }
    for(int i = 0; i < n; ++i)
    {
        pthread_join(tids[i], nullptr);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

通过以上可以发现

凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这是一个不能违反的规则

每一个线程访问临界区之前,得加锁,加锁本质是给临界区加锁 ,加锁后代码进行串行访问,加锁粒度尽量要细一些,不要让太多代码一块加锁,所以临界区附近不要有多余的代码,要不就会执行时间更长

加锁前所有线程要先看到同一把锁,所以锁本身就是一个公共资源,加锁和解锁本身是原子的,所以锁才能保障自身安全

临界区可以是一行代码,也可以是一批代码,那么在解锁前线程大概率被切换出去,以及在加锁的时候也可能会被切出去。但是没有影响,如果在临界区内加锁后被切换出去,其他进程也不能再次申请锁,进入临界区,只能等这个线程执行完后再申请锁,这也就是互斥带来的串行化的表现,也是为什么加锁后会变慢。站在其它线程角度,对它们有意义的状态就是锁被申请,锁被释放,所以锁的原子性就体现在这里,解锁也是原子的。

Linux把这个锁叫做互斥锁或者互斥量。

3、互斥锁的实现原理

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

在这里插入图片描述

调用的线程会去执行lock和unlock。内存中给mutex分配空间,默认变量值是1。一个线程开始lock的时候,会先把0传给%al这个寄存器,其实就是初始化它为0。寄存器的硬件只有一套,但是寄存器内部的数据,也就是执行流的上下文是线程自己的。相当于私人物品放在公共地区。所以lock的第一句的意思就是调用线程,向自己的上下文写入0,之后切走时还要带走这个0。

第二句就是exchange指令,寄存器和锁的空间交换数据,本质就是将共享数据交换到自己的私有上下文当中,也就是1和寄存器中的0交换一下位置,这个操作就是加锁,这一条汇编语句对应着加锁的原子性。如果这时候还没进行下一步,线程被切走了,这时候这个线程就会把寄存器中的内容拿走,并且记录下这个线程执行到的代码,切回之后还会继续这行代码。但是这时候锁是0,下一个线程还是执行lock的代码,寄存器中初始化为0,然后和锁交换,进入判断,发现寄存器中为0,所以挂起等待,所以申请锁失败,这个线程就会带着上下文等去了,等到申请锁成功的线程回来,把1放进寄存器中,然后判断,return 0,加锁成功。所以1只会流转。

那么解锁也就能看出来了。

3、线程封装

1、线程本体

这里就直接传代码

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>
using namespace std;

typedef void (*func_t)();

class Thread
{
public:
    typdef enum
    {
        New = 0,
        RUNNING,
        EIXTED
    }ThreadStatus;
    typedef void (*func_t)(void*);
public:
    Thread(int num, func_t func, void* args):_tid(0), _status(NEW), _func(func), _args(args)//num是线程编号
    {
        char name[128];
        snprintf(name, sizeof(name), "thread-%d", num);
        _name = name;
    }

    int status() {return _status;}
    string threadname() {return _name;}
    pthread_t threadid()
    {
        if(_status == RUNNING) return _tid;
        else
        {
            return 0;
        }
    }

    //runHelper是成员函数,类的成员函数具有默认参数this,也就是括号有一个Thread* this,pthread_create的参数应当是void*类型,就不符合,所以加上static,放在静态区,就没有this,但是又有新问题了
    //static成员函数无法直接访问类内属性和其他成员函数,所以create那里要传this指针,this就是当前线程对象,传进来才能访问类内的东西
    static void* runHelper(void* args)//args就是执行方法的参数
    {
        Thread* ts = (Thread*)args;//就拿到了当前对象
        //函数里可以直接调用func这个传过来的方法,参数就是_args
        (*ts)();//调用了func函数
        return nullptr;
    }

    void operator()()//仿函数
    {
        if(_func != nullptr) _func(_args);
    }
    
    void run()//run函数这里传方法
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);//为什么传this?
        if(n != 0) exit(1);
        _status = RUNNING;
    }
 
    void join()
    {
        int n = pthread_join(_tid, nullptr);
        if(n != 0)
        {
            cerr << "main thread join thread" << _name << "error" << endl;
            return ;
        } 
        _status = EXITED;
    }
    
    ~Thread()
    {}
private:
    pthread_t _tid;
    string _name;
    func_t func;//线程未来要执行的函数方法
    void* _args;//也可以不写这个,那么typedef的函数指针就没有参数。当然,参数也可用模板来写
    ThreadStatus _status;
}

那么原来的使用线程代码就可以这样写了

void threadRun(void* args)
{
    string message = static_cast<const char*>(args);
    while(true)
    {
        cout << "我是一个线程, " << message << endl;
        sleep(1);
    }
}

int main()
{
    //弄两个线程
    Thread t1(1, threadRun, (void*)"hellobit1");
    Thread t2(2, threadRun, (void*)"hellobit2");
    cout << "thread name: " << t1.threadname() << "thread id: " << t1.threadid() << ",thread status: " <<t1.status() << endl;
    cout << "thread name: " << t1.threadname() << "thread id: " << t1.threadid() << ",thread status: " <<t1.status() << endl;
    t1.run();
    t2.run();
    cout << "thread name: " << t1.threadname() << "thread id: " << t1.threadid() << ",thread status: " <<t1.status() << endl;
    cout << "thread name: " << t1.threadname() << "thread id: " << t1.threadid() << ",thread status: " <<t1.status() << endl;
    t1.join();
    t2.join();
    cout << "thread name: " << t1.threadname() << "thread id: " << t1.threadid() << ",thread status: " <<t1.status() << endl;
    cout << "thread name: " << t1.threadname() << "thread id: " << t1.threadid() << ",thread status: " <<t1.status() << endl;

    return 0;
}

2、封装锁

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex//自己不维护锁,由外部传入
{
public:
    Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
    {}

    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }

    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }

    ~Mutex()
    {}
private:
    pthread_mutex_t *_pmutex;
}

class LockGuard//自己不维护锁,由外部传入
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();
    }

    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
     Mutex _mutex;
}

代码加锁

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//全局初始化
void threadRoutine(void* args)
{
    string *message = static_cast<const char*>(args);
    while(true)
    {
        LockGuard lockguard(&mutex);//交给类去自动创建,析构锁//RAII风格加锁
        if(tickets > 0)
        {
            usleep(2000);
            cout << message << "get a ticket: " << tickets-- << endl;
        }
        else
        {
            break;
        }
    }
}

int main()
{
   Thread t1(1, threadRoutine, (void*)"hellobit1");
   Thread t2(2, threadRoutine, (void*)"hellobit2");
   Thread t3(3, threadRoutine, (void*)"hellobit3");
   Thread t4(4, threadRoutine, (void*)"hellobit4");
   t1.run();
   t2.run();
   t3.run();
   t4.run();
   t1.join();
   t2.join();
   t3.join();
   t4.join();
   return 0;
}

4、线程安全

常见不安全情况:
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

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

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

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

可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题,可以通过加锁等方式来控制
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别:
可重入函数是描述函数状态的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的(仅仅是在调用过程中安全,如果在这之前之后调用全局变量等那就不一定了)
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生
死锁,因此是不可重入的。

5、死锁

死锁,一种临界资源,被访问需要同时拥有两把锁,两个线程各自持有一把锁,并且互相申请另一把锁,导致两个线程都被挂起,这也就造成了死锁。

死锁的四个必要条件:
1、互斥条件:一个资源每次只能被一个执行流使用
2、请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
3、不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
4、环路条件(循环与等待):若干执行流之间形成一种头尾相接的循环等待资源的关系

也就是只要产生死锁,这四个条件必然都有了。

避免死锁的方法就是破坏死锁,破坏这四个条件的任何一个。

不加锁
主动释放锁(trylock函数是非阻塞式申请锁,申请失败就不会加锁,一般用在第一把锁已经用lock接口申请了,第二把锁就trylock一下,不行的话那就再自己操作,比如释放第一把锁)
多个线程申请锁的顺序保持一致
A线程申请锁,B线程可以去释放A的锁吗?加解锁可以在不同线程。可以控制线程统一释放锁。这一点可以看一下上面的加锁过程的伪代码,最后是mov,而是exchange这样的代码,说明什么,线程不必归还锁,系统自然有办法回收这个锁。

6、线程同步

线程在临界区内是可以自由切换,系统保护它不受影响。一个线程出了临界区,释放锁后,其它线程就可以来申请锁,但是如果还没等其它线程来申请,刚离开临界区的这个线程又一次申请锁,又占据了临界区,其它线程有不能申请了,只能等待,如果循环往复,这个线程不断地申请,释放锁,那么这就是毫无效率的一个线程,为了解决这个问题,系统就制定规则,刚释放锁的线程不能立刻再次申请锁,等待的线程要按照顺序排列好,出来的这个线程要排到队列尾部。像这样,在安全的规则下,多线程访问资源具有一定的顺序性,这就是线程同步。这是多线程协同工作的一种方式。

1、条件变量

用来实现多线程同步,一个线程可以通过条件变量来通知另一个线程要做的操作。在之前的抢票中,先加锁,然后去判断票数是否小于0,这个判断是临界资源,所以是在临界区内判断的,如果票数小于0,就会break退出,如果改一下,变成票数小于0就释放锁,然后再重复刚才的动作,申请锁,判断,同时系统会自动放票,所以需要这个程序不停地判断,这样就造成了低效程序,在这个线程不停地做这些动作时,其他线程无法执行其他操作。所以就需要用到条件变量来控制加锁解锁,访问临界资源的整个顺序性的执行

1、接口

在这里插入图片描述

条件变量的类型其实就是一个结构体。wait那个函数,mutex就是互斥锁,条件判断一定是在加锁之后的,如果不满足条件就调用这个接口,它会释放线程拥有的锁,然后让这个线程去等待,这也就是为什么wait接口有锁这个参数。signal可以唤醒一个等待的线程,broadcast可以唤醒所有等待的线程,broadcast也就是广播,这个在Python中比较熟悉。

2、demo代码

条件变量和锁的创建等都相似

在这里插入图片描述

const int num = 5;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_COND_INITIALIZER;

void* active(coid* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);//先默认一进入循环就已经是不符合判断了
        cout << name << "活动" << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t tids[num];
    for(int i = 0; i < num; i++)
    {
        char* name = new char[32];
        snprintf(name, 32, "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, active, name);
    }
    sleep(3);//经过上面的代码,所有线程都处于等待状态了,接下来全部唤醒
    while(true)
    {
        //唤醒会从队列的第一个线程开始,唤醒后这个线程就会从被wait处开始,继续执行之后的代码,所以会打印活动
        cout << "main thread wake up..." << endl;
        pthread_cond_signal(&cond);
        //pthread_cond_broadcast(&cond);会一次性打印5个线程,打印的顺序就是队列中的顺序,并且之后都会以这个顺序打印
        sleep(1);//每隔一秒唤醒一个
    }
    for(int i = 0; i < num ; i++)
    {
        pthread_join(tids[i], nullptr);
    }
}

开另一个窗口,用while :; do ps -aL | head -1&&ps -aL | grep threadtest; sleep 1; done来查看情况。threadtest是程序员自己命名的可执行程序的名字。

条件变量允许多线程在cond中队列式等待。

本篇gitee

结束。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

常用性能测试工具及其功能

在软件开发周期的不同阶段&#xff0c;性能测试工具被广泛用于评估系统的性能和发现潜在的性能瓶颈。本文介绍了几种常用的性能测试工具&#xff0c;包括负载测试工具、压力测试工具和基准测试工具&#xff0c;并详细描述了它们的功能和用法。 性能测试在软件开发的各个阶段都至…

SpringBoot 调用外部接口

SpringBoot 调用外部接口 一、第一种方式(HttpClient等) 使用插件方式&#xff0c;比如自带的HttpClient&#xff0c;或者OkHttp&#xff0c;甚至是原生的HttpURLConnection 等等&#xff0c;这里以HttpClient为例。 1、封装工具类 简单封装的get请求 /*** 发送get请求:带请求…

数字时代的先驱者,「Adobe之父」离世,享年82岁!

原创 | 文 BFT机器人 John Warnock于当地时间8月19日辞世&#xff0c;享年82岁。 他作为图形和出版软件公司Adobe的共同创始人&#xff0c;被誉为“Adobe之父”&#xff0c;在计算机图形学和电子出版等领域都做出了重大的贡献&#xff0c;为后人留下一笔丰厚的“遗产”&#x…

校园二手物品交易平台/二手交易系统/基于java的校园跳蚤市场系统

​ 摘 要 本文论述了校园二手物品交易平台的设计和实现&#xff0c;该网站从实际运用的角度出发&#xff0c;运用了计算机网站设计、数据库等相关知识&#xff0c;网络和Mysql数据库设计来实现的&#xff0c;网站主要包括用户注册、用户登录、浏览商品、搜索商品、查看商品并进…

8.23 类 构造函数 析构函数 拷贝构造函数

#include <iostream>using namespace std;class Per{string name;int age;float *high;float *weight; public:Per(string name,int age,float high,float weight):name(name),age(age),high(new float(high)),weight(new float(weight)){cout << "Per的构造函…

docker: /lib64/libc.so.6: version `GLIBC_2.32‘ not found (required by docker)

Linux环境 Ubuntu 22.04 docker 最新版 jenkins docker 版本(以下版本都会报错 jenkins/jenkins:centos7 jenkins/jenkins:lts-centos7 jenkins/jenkins:ltsdocker-compose.yml配置 version: 3.6 services:gitlab:image: twang2218/gitlab-ce-zhrestart: alwayscontainer_nam…

数据通信——传输层(UDP)

引言 我们上网观看比赛的时候&#xff0c;一旦网络信号出现问题&#xff0c;那可就太难受了&#xff0c;这意味着卡顿的时间内&#xff0c;你会错过这段时间内的内容。这种特性要归功于UDP&#xff08;User Datagram Protocol&#xff09;用户数据报协议。 无连接性 一般的&am…

全网最全ArrayList底层原理实现

1. ArrayList集合底层数据结构 1. ArrayList集合介绍 ArrayList是实现了List接口的动态数组&#xff0c;所谓动态数组就是他的大小是可变的。实现了所有可选列表操作&#xff0c;并允许包括Null在内的所有元素。除了实现 List 接口外&#xff0c;此类还提供一些方法来操作内部…

【Adobe After Effects】关于ae点击空格不会播放反而回退一帧的解决方案

最近玩ae的时候遇见了一个小问题&#xff0c;就是有时候敲空格&#xff0c;视频没办法播放&#xff0c;反而会回退一帧&#xff0c;经过摸索发现了一个解决办法&#xff1a; 点击编辑---首选项 然后选择“音频硬件” 然后选择正确的默认输出&#xff0c;点击确定即可

小心悄悄被成为公司“法人”!曝多个APP存在重大安全漏洞

目录 多个政务App存在安全漏洞 人脸识别风险的分析 保障人脸识别应用的安全 张女士从未到过湖南株洲&#xff0c;却发现自己名下有一家个体工商户&#xff0c;且该公司位于千里之外。她报警和反馈后得知&#xff0c;该个体户是通过网上办理并进行了实名验证&#xff0c;合法…

Day3: 前端路由(基础篇)

❝ 「目标」: 持续输出&#xff01;每日分享关于web前端常见知识、面试题、性能优化、新技术等方面的内容。 ❞ ❝ 「主要面向群体&#xff1a;」前端开发工程师&#xff08;初、中、高级&#xff09;、应届、转行、培训等同学 ❞ Day3-今日话题 想必大家经常会在面试中或者工作…

麒麟系统在FT2000+下预留连续物理内存空间

1、背景介绍 项目需要在系统下预留一段连续物理地址空间供FPGA启动DMA直接写入&#xff0c;这样提高读写带宽。目前有两种方式可以实现该需求。 注意&#xff1a;前提是操作系统将内存空间访问权限全部放开&#xff0c;否则无法预留空间。 2、实现方法 方式一&#xff1a; …

vue中form和table标签过长

form标签过长 效果&#xff1a; 代码&#xff1a; <el-form-item v-for"(item,index) in ticketEditTable1" :label"item.fieldNameCn" :propitem.fieldName :key"item.fieldNameCn" overflow"":rules"form[item.fieldName…

测试先行:探索测试驱动开发的深层价值

引言 在软件开发的世界中,如何确保代码的质量和可维护性始终是一个核心议题。测试驱动开发(TDD)为此提供了一个答案。与传统的开发方法相比,TDD鼓励开发者从用户的角度出发,先定义期望的结果,再进行实际的开发。这种方法不仅可以确保代码满足预期的需求,还可以在整个开…

数组和指针练习(1)

题目&#xff1a; int main() { int a[5] { 1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5}; int * ptr (int * )(&a 1); printf("%d&#xff0c;%d"&#xff0c;*(a 1)&#xff0c;*(ptr - 1)); return 0; } 思路分析&#xff1a;…

BFT最前线|字节跳动AI对话产品“豆包”上线!联想集团推出AI大模型训练服务器!雷尼绍推出工业自动化产品系列

原创 | 文 BFT机器人 AI视界 TECHNOLOGY NEWS 看点1 天才少年稚晖君首秀&#xff0c;官宣智元人形机器人&#xff01; 2023年8月18日上午&#xff0c;从华为离职的“天才少年”彭志辉&#xff0c;也是B站硬核科技UP主稚晖君&#xff0c;公布了他所在的智元团队创业半年的成果…

【严重】Smartbi windowUnloading 限制绕过导致远程代码执行 (MPS-e2z8-wdi6)

zhi.oscs1024.com​​​​​ 漏洞类型授权机制不恰当发现时间2023-08-22漏洞等级严重MPS编号MPS-e2z8-wdi6CVE编号-漏洞影响广度广 漏洞危害 OSCS 描述 Smartbi 是思迈特软件旗下的一款商业智能应用&#xff0c;提供了数据集成、分析、可视化等功能&#xff0c;帮助用户理解和…

湘潭大学 湘大 XTU OJ 1116 水仙花数 题解(非常详细)

链接 1116 题面 Description 如果一个n位数的每个数位的n次方和就是本身&#xff0c;那么我们称这种数为“水仙花数”。比如371,337313273431 371。现给你一个数&#xff0c;请求这个数是否是水仙花数。 输入 有多组样例。每个样例占一行&#xff0c;为一个整数a&#xff0…

72 # http 缓存策略

前面实现了一个 http-server&#xff0c;并且实现了 gzip 的压缩&#xff0c;下面通过前面几节学习的缓存知识来添加一下缓存。 大致就是先强制缓存 10s&#xff0c;然后采用协商&#xff08;对比&#xff09;缓存&#xff0c;大致图如下 在之前的 http-server 的代码基础上添…

2023年8月22日OpenAI推出了革命性更新:ChatGPT-3.5 Turbo微调和API更新,为您的业务量身打造AI模型

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…