利用线程池多线程并发实现TCP两端通信交互,并将服务端设为守护进程

news2024/9/21 14:29:56

文章目录

  • 实现目标
  • 实现步骤
  • 封装日志类
  • 封装线程池
    • 封装线程
    • 封装锁
    • 封装线程池
  • TCP通信的接口和注意事项
    • accept
  • TCP
    • 封装任务
    • 客户端
      • Client.hpp
      • Client.cc
    • 服务端
      • Server.hpp
    • Server.cc
    • 实现效果
  • 守护进程
    • 服务端守护进程化

实现目标

利用线程池多线程并发实现基于TCP通信的多个客户端与服务端之间的交互,客户端发送数据,服务端接收后处理数据并返回。服务端为守护进程

实现步骤

  1. 封装一个记录日志的类,将程序运行的信息保存到文件
  2. 封装线程类、服务端处理任务类以及将锁进行封装,为方便实现线程池
  3. 实现服务端,使服务端能接收客户端所发来的数据,处理数据后返回。服务端采用多线程并发处理
  4. 封装守护进程方法,使服务端为守护进程
  5. 实现客户端,可以向服务端发送数据,并接收到服务端发送回来的数据

封装日志类

将程序运行的信息保存到指定文件,例如创建套接字成功或者失败等信息。以【状态】【时间】【信息】的格式保存。

状态可分为五种:“DEBUG”,“NORMAL”,“WARNING”,“ERROR”,“FATAL”

日志类保存的信息需带有可变参数

#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

using namespace std;

#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *to_levelstr(int level)
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case NORMAL:
        return "NORMAL";
    case WARNING:
        return "WARNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return nullptr;
    }
}

void LogMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logpre[NUM];
    snprintf(logpre, sizeof(logpre), "[%s][%ld][%d]", to_levelstr(level), (long int)time(nullptr), getpid());

    char line[NUM];
    // 可变参数
    va_list arg;
    va_start(arg, format);

    vsnprintf(line, sizeof(line), format, arg);

    // 保存至文件
    FILE* log = fopen("log.txt", "a");
    FILE* err = fopen("log.error", "a");

    if(log && err)
    {
        FILE *curr = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING) 
            curr = log;
        if(level == ERROR || level == FATAL) 
            curr = err;
        if(curr) fprintf(curr, "%s%s\n", logpre, line);

        fclose(log);
        fclose(err);
    }
}

封装线程池

封装线程

将线程的创建,等待封装成类的成员函数。不再需要单个的条用线程库接口,以对象的方式创建。

需要注意:在类里面的线程回调方法必须设为static类型,而静态的方法是不能访问类内成员的,因此传给回调函数的参数需要将整个对象传过去,通过对象来获取类内成员

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>

typedef std::function<void *(void *)> func_t;

class Thread
{
private:
    // 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
    static void *start_routine(void *args) // 类内成员,有缺省参数!
    {
        Thread *_this = static_cast<Thread *>(args);
        return _this->callback();
    }

public:
    // 构造函数里直接生成线程名,利用静态变量从1开始
    Thread()
    {
        char namebuffer[1024];
        snprintf(namebuffer, sizeof namebuffer, "thread-NO.%d", threadnum++);
        _name = namebuffer;
    }

    // 线程启动
    void start(func_t func, void *args = nullptr)
    {
        _func = func;
        _args = args;
        // 由于静态的方法是不能访问类内成员的,
        // 因此传给回调函数的参数需要将整个对象传过去,通过对象来获取类内成员
        // 也就是this指针
        int n = pthread_create(&_tid, nullptr, start_routine, this);
        assert(n == 0);
        (void)n;
    }

    // 线程等待
    void join()
    {
        int n = pthread_join(_tid, nullptr);
        assert(n == 0);
        (void)n;
    }

    ~Thread()
    {
    }

    void *callback()
    {
        return _func(_args);
    }

private:
    std::string _name; // 类名
    func_t _func;      // 线程回调函数
    void *_args;       // 线程回调函数的参数
    pthread_t _tid;    // 线程id

    static int threadnum; // 线程的编号,为生成线程名
};
// static的成员需在类外初始化
int Thread::threadnum = 1;

封装锁

同样的为了不再需要一直调用系统接口,可以将整个方法封装成类,通过类的对象实现加锁过程

#pragma once

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

// 加锁 解锁
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p = nullptr) : _lock_p(lock_p)
    {
    }
    // 加锁
    void lock()
    {
        if (_lock_p)
            pthread_mutex_lock(_lock_p);
    }
    // 解锁
    void unlock()
    {
        if (_lock_p)
            pthread_mutex_unlock(_lock_p);
    }
    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_lock_p;
};

// 锁的类
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex) : _mutex(mutex)
    {
        _mutex.lock(); // 在构造函数中进行加锁
    }
    ~LockGuard()
    {
        _mutex.unlock(); // 在析构函数中进行解锁
    }

private:
    Mutex _mutex;
};

封装线程池

在类里面的线程回调方法必须设为static类型,而静态的方法是不能访问类内成员的,因此传给回调函数的参数需要将整个对象传过去,通过对象来获取类内成员。

线程池需要实现为单例模式:

  1. 第一步就是把构造函数私有,再把拷贝构造和赋值运算符重载delete
  2. 在设置获取单例对象的函数的时候,注意要设置成静态成员函数,因为在获取对象前根本没有对象,无法调用非静态成员函数
  3. 可能会出现多个线程同时申请资源的场景,所以还需要一把锁来保护这块资源,而这把锁也得设置成静态,因为单例模式的函数是静态的
#pragma once

#include "Thread.hpp"
#include "log.hpp"
#include "Lock.hpp"
#include <vector>
#include <queue>
#include <mutex>
#include <pthread.h>
#include <unistd.h>

using namespace std;

// 线程池类定义位于下面,因此属性类想要获取到
// 就必须在前面声明
template <class T>
class ThreadPool;

template <class T>
class ThreadData
{
public:
    ThreadPool<T> *threadpool; // 线程所在的线程池,获取到线程的this指针
    std::string _name;         // 线程的名字

public:
    ThreadData(ThreadPool<T> *tp, const std::string &name) : threadpool(tp), _name(name)
    {
    }
};

template <class T>
class ThreadPool
{
private:
    // 线程最终实现的方法
    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = (ThreadData<T> *)args;
        while (true)
        {
            T t;
            {
                LockGuard lockguard(td->threadpool->mutex());
                while (td->threadpool->isQueueEmpty())
                {
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop();
            }
            t();
        }
        delete td;
        return nullptr;
    }

    ThreadPool(const int &num = 10) : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }

    void operator=(const ThreadPool &) = delete;
    ThreadPool(const ThreadPool &) = delete;

public:
    // 将加锁 解锁 判断任务队列是否为空 和条件变量等待全部封装成类内方法
    // 方便在线程的回调方法中通过对象直接调用
    void lockQueue() { pthread_mutex_lock(&_mutex); }
    void unlockQueue() { pthread_mutex_unlock(&_mutex); }
    bool isQueueEmpty() { return _task_queue.empty(); }
    void threadWait() { pthread_cond_wait(&_cond, &_mutex); }

    // 任务队列删除队头,并返回队头的任务
    T pop()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }
    pthread_mutex_t *mutex()
    {
        return &_mutex;
    }

public:
    // 让每个线程对象调用其启动函数,并将线程辅助类和最终执行的任务方法传入函数中
    // 线程的辅助类对象里包含了线程当前线程池对象,也就是可以
    // 通过辅助类对象可以调用到线程池对象里的成员
    void run()
    {
        for (const auto &t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            // 创建成功后打印日志
            LogMessage(DEBUG, "%s start ...", t->threadname().c_str());
        }
    }

    // 往任务队列里插入一个任务
    void push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (const auto &t : _threads)
            delete t;
    }

    // 实现单例模式
    static ThreadPool<T> *getInstance()
    {
        if (nullptr == tp)
        {
            _singlock.lock();
            if (nullptr == tp)
            {
                tp = new ThreadPool<T>();
            }
            _singlock.unlock();
        }
        return tp;
    }

private:
    int _num;//线程的数量
    std::vector<Thread *> _threads;//线程组
    std::queue<T> _task_queue;//任务队列
    pthread_mutex_t _mutex;//锁
    pthread_cond_t _cond;//条件变量

    static ThreadPool<T> *tp;
    static std::mutex _singlock;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;

template <class T>
std::mutex ThreadPool<T>::_singlock;

TCP通信的接口和注意事项

为了实现TCP版的通信,首先来了解一下相关接口和注意事项

  1. TCP需要在通信前先创建链接,因此在TCP没有链接之前其创建的套接字并不是用来通信的,而是用来监听的。一旦创建链接成功后,才会返回一个用来通信的套接字
  2. TCP时面向字节流的,因此其通信就是往文件上IO,因此不用指定的调用某接口去完成,直接用文件接口读写就可以完成

accept

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

这就是用来创建链接的接口

参数一为负责监听的套接字

参数二就是socket的结构体

参数三为结构体的大小

返回值,成功创建链接之后会返回一个值,这个值就是负责通信的套接字,也就是后面利用文件通信的文件描述符

TCP

封装任务

因为上述说到TCP是可以直接使用文件操作来完成通信的,那么也就是说其通信根本就用不到其他的成员了,只需要知道一个套接字即可。那么这个方法就可以不放在类内,因为这就是线程最后的执行目的,因此可以将这个任务单独放到一个头文件中。因为线程池是一个模板类,则可以封装一个任务类。

#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <functional>
#include "log.hpp"

// TCP的通信
// 线程的最终执行方法
void ServerIO(int sock)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            // read
            buffer[n] = 0;
            std::cout << "recv message: " << buffer << std::endl;

            // write
            std::string outbuffer = buffer;
            outbuffer += " server[echo]";

            write(sock, outbuffer.c_str(), outbuffer.size());
        }
        else if (n == 0)
        {
            // 代表client退出
            LogMessage(NORMAL, "client quit, me too!");
            break;
        }
    }
    close(sock);
}

// 任务类
// 为了最终执行的方法而服务
class Task
{
    using func_t = std::function<void(int)>;

public:
    Task()
    {
    }
    Task(int sock, func_t func)
        : _sock(sock), _callback(func)
    {
    }
    void operator()()
    {
        _callback(_sock);
    }

private:
    int _sock; // 通信套接字
    func_t _callback;
};

客户端

客户端不需要显示的绑定端口号,而是由操作系统随机去绑定。TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept。TCP的客户端只需要向服务端发起链接请求

Client.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp"

using namespace std;

class Client
{
public:
    Client(const string &serverip, const uint16_t &port)
        : _serverip(serverip), _port(port), _sock(-1)
    {
    }
    
    void Init()
    {
        // 创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            LogMessage(FATAL, "create socket error");
            exit(1);
        }

        // TCP的客户端也不需要显示绑定端口,让操作系统随机绑定
        // TCP的客户端也不需要监听,因为并没有去主动链接客户端,所以不需要accept
        // TCP的客户端只需要向服务端发起链接请求
    }

    void start()
    {
        // 向服务端发起链接请求
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_serverip.c_str());
        if (connect(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
            LogMessage(ERROR, "connect socket error");
        
        // 和服务端通信
        else
        {
            string line;
            while (1)
            {
                cout << "Please cin: " << endl;
                getline(cin, line);

                // 向服务端写
                write(_sock, line.c_str(), line.size());

                // 读服务端返回来的数据
                char buff[1024];
                int n = read(_sock, buff, sizeof(buff) - 1);
                if (n > 0)
                {
                    buff[n] = 0;
                    cout << "接收到的消息为:" << buff << endl;
                }
                else
                    break;
            }
        }
    }

    ~Client()
    {
        if(_sock >= 0)
            close(_sock);
    }

private:
    int _sock;
    string _serverip;
    uint16_t _port;
};

Client.cc

#include "Client.hpp"
#include <memory>

// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}

int main(int argc, char* argv[])
{
    // 再运行客户端时,输入的指令需要包括主机ip和端口号
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    string serverip = argv[1];
    uint16_t port = atoi(argv[2]);
    unique_ptr<Client> client(new Client(serverip, port));

    client->Init();
    client->start();

    return 0;
}

服务端

那么对于服务端而言,必须要显式的去绑定端口号。则创建的套接字并不是负责通信的。创建好套接字和绑定完网络信息后,需要设置创建的套接字为监听状态。和UDP一样,服务端是不能指定IP的.

还需要注意的是:因为封装的线程池是单例模式,所以不需要创建对象,直接调用静态对象去调用类方法即可

步骤可分为:

  1. 创建监听套接字
  2. 绑定网络信息
  3. 设置套接字为监听状态
  4. 获取链接,得到通信的套接字
  5. 通信
  6. 关闭不需要的套接字

Server.hpp

#pragma once

#include "Task.hpp"
#include "ThreadPool.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>

class Server
{
public:
    Server(const uint16_t &port = 8000)
        : _port(port)
    {
    }

    void Init()
    {
        // 创建负责监听的套接字 面向字节流
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0)
        {
            LogMessage(FATAL, "create socket error!");
            exit(1);
        }
        LogMessage(NORMAL, "create socket %d success!", _listenSock);

        // 绑定网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_listenSock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            LogMessage(FATAL, "bind socket error!");
            exit(3);
        }
        LogMessage(NORMAL, "bind socket success!");

        // 设置socket为监听状态
        if (listen(_listenSock, 5) < 0)
        {
            LogMessage(FATAL, "listen socket error!");
            exit(4);
        }
        LogMessage(NORMAL, "listen socket success!");
    }

    void start()
    {
        while (1)
        {
            // 因为线程池时单例模式,所以直接调用初始化
            ThreadPool<Task>::getInstance()->run();
            LogMessage(NORMAL, "Thread init success");

            // server获取建立新连接
            struct sockaddr_in peer;
            memset(&peer, 0, sizeof(peer));
            socklen_t len = sizeof(peer);
            // 创建通信的套接字
            // accept的返回值才是真正用于通信的套接字
            _sock = accept(_listenSock, (struct sockaddr *)&peer, &len);
            if (_sock < 0)
            {
                // 获取通信的套接字失败并不影响未来的操作,只是当前的链接失败而已
                LogMessage(ERROR, "accept socket error, next");
                continue;
            }
            LogMessage(NORMAL, "accept socket %d success", _sock);
            cout << "sock: " << _sock << endl;

            // 往线程池的任务队列里插入任务
            ThreadPool<Task>::getInstance()->push(Task(_sock, ServerIO));
        }
    }

private:
    int _listenSock; // 负责监听的套接字
    int _sock;       // 通信的套接字
    uint16_t _port;  // 端口号
};

Server.cc

#include "Server.hpp"
#include "daemon.hpp"
#include <memory>

// 输出命令错误函数
void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << " local_ip local_port\n\n";
}

int main(int argc, char* argv[])
{
    // 启动服务端不需要指定IP
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = atoi(argv[1]);

    unique_ptr<Server> server(new Server(port));

    server->Init();
    
    server->start();

    return 0;
}

实现效果

image-20230804003006475

可以看到多个客户端同时访问也没有问题,并且所对应的套接字也就是文件描述符也不一样。

守护进程

守护进程是一种特殊的孤儿进程,其运行于后台,生存期较长并且独立与终端周期性的执行任务或者等待处理任务

进程分为前台运行和后台运行,每一个进程都会属于一个会话组里。每一个会话组都有且只有能一个前台进程。像上述的服务端,当运行服务端时,操作系统会将其分到含有bash的会话组内,并且将服务端置为前台任务进程,因此服务端运行时bash把放置后台这也就是为什么用户不能再bash继续输入命令的原因。

每一个会话组都会有一个组长,一般而言在bash中输入命令执行的进程都会分到bash的会话组内,这个会话组的组长即为bash。可以通过查看进程的SID确认进程的会话组

image-20230805132436861

可以看到上述图片中运行了三个进程并置于后台,他们的SID也就是会话组都是一样的。那么如果将他们置于前台运行会发生什么呢

image-20230805133033613

可以看到,置于前台运行后,命令行输入什么都没有反应了。也就是说,此时的bash被自动的放到了后台运行,证实了一个会话组只能有一个前台进程

image-20230805133230899

输入ctr + Z 之后前台的进程就会把切回后台,但是切回后台后进程是阻塞状态的,因此输入bg + 作业号就可让进程启动。

服务端守护进程化

那么很显然,在业务逻辑上服务端肯定是需要守护进程化的。因为服务端没有特殊情况是不会关闭的,需要一直运行。如果服务端是前台进程的话,那服务端运行时bash都不能用了,显然不符合。

这里要介绍一个接口:

#include <unistd.h>
pid_t setsid(void);

这个接口的作用是使调用的进程独立成为一个会话组并且为该组的组长。但是调用这个接口是有前置条件的:调用这个接口的进程不能为某个会话组的组长

守护进程化的步骤:

  1. 让调用进程忽略掉异常信号,因为其不受终端控制的
  2. 让调用进程不为组长
  3. 关闭或者重定向之前默认打开的文件,如0 1 2文件描述符
#pragma once

#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define DEV "/dev/null"

void daemonSelf(const char *currPath = nullptr)
{
    // 1. 让调用进程忽略掉异常的信号
    signal(SIGPIPE, SIG_IGN);

    // 2. 让自己不是组长,setsid
    if (fork() > 0)
        exit(0);
    // 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!
    pid_t n = setsid();
    assert(n != -1);

    // 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
    int fd = open(DEV, O_RDWR);
    if(fd >= 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);

        close(fd);
    }
    else
    {
        close(0);
        close(1);
        close(2);
    }
}

接着只需要服务端在初始化完成后调用这个函数,将自己设为守护进程化即可

image-20230805134141141

一起来看看效果:

image-20230805134304036

可以看到服务端启动后并不会影响bash,仍然可以在bash上输入指令去执行。客户端也能够很好的接收到数据,这就符合现实中服务端的逻辑。

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

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

相关文章

《向量数据库指南》——腾讯云向量数据库Tencent Cloud VectorDB应用场景

目录 大模型知识库 推荐系统 问答系统 文本/图像检索 大模型知识库 腾讯云向量数据库可以和大语言模型 LLM 配合使用。企业的私域数据在经过文本分割、向量化后,可以存储在腾讯云向量数据库中,构建起企业专属的外部知识库,从而在后续的检索任务中,为大模型提供提示信息…

C语言每日一题:14《数据结构》复制带随机指针的链表

题目一&#xff1a; 题目链接&#xff1a; 思路一&#xff1a; 找相对位置暴力求解的方法&#xff1a; 1.复制一个新的链表出来遍历老的节点给新的节点赋值&#xff0c;random这个时候不去值。 2.两个链表同时遍历&#xff0c;遍历老链表的时候去寻找相对位置&#xff0c;在遍…

「Qt」常用事件介绍

&#x1f514; 在开始本文的学习之前&#xff0c;笔者希望读者已经阅读过《「Qt」事件概念》这篇文章了。本文会在上篇文章的基础上&#xff0c;进一步介绍 Qt 中一些比较常用的事件。 0、引言 当我们想要让控件收到某个事件时做一些操作&#xff0c;通常都需要重写相应的事件处…

Mysql如何实现XA规范

文章目录 一、什么是XA二、XA规范涉及到的角色&#xff0c;以及相关概念1. XA规范涉及到的角色包括&#xff1a;1.1 事务管理器&#xff08;Transaction Manager&#xff09;&#xff1a;1.2 资源管理器&#xff08;Resource Manager&#xff09;&#xff1a; 2. 相关概念包括&…

复原 IP 地址——力扣93

文章目录 题目描述回溯题目描述 回溯 class Solution{public:static constexpr int seg_count=4<

解决CF窗口黑边办法

1、桌面鼠标右键点击【英伟达控制面板】 2、点击 【调整桌面尺寸和位置】 3、缩放模式改为 【全屏】即可。 最终效果&#xff1a;

redis的配置和使用、redis的数据结构以及缓存遇见的常见问题

目录 1.缓存 2.redis不仅仅可以做缓存&#xff0c;只不过说他的大部分场景&#xff0c;是做缓存。本地缓存重启后缓存里的东西就没有了&#xff0c;但是redis有。 3.redis有几个特性:查询快&#xff0c;但是是放到内存里的〈断电或者重启&#xff0c;数据就丢了)&#xff0c…

Golang之路---04 并发编程——WaitGroup

WaitGroup 为了保证 main goroutine 在所有的 goroutine 都执行完毕后再退出&#xff0c;前面使用了 time.Sleep 这种简单的方式。 由于写的 demo 都是比较简单的&#xff0c; sleep 个 1 秒&#xff0c;我们主观上认为是够用的。 但在实际开发中&#xff0c;开发人员是无法…

神码ai伪原创工具【php源码】

大家好&#xff0c;小编为大家解答python炫酷烟花表白源代码的问题。很多人还不知道html代码烟花特效python&#xff0c;现在让我们一起来看看吧&#xff01; 火车头采集ai伪原创插件截图&#xff1a; 目录 前言 环境准备 代码编写 效果展示 前言 Python实现浪漫的烟花特效 现在…

Java并发系列之六:CountDownLatch

CountDownLatch作为开发中最常用的组件&#xff0c;今天我们来聊聊它的作用以及内部构造。 首先尝试用一句话对CountDownLatch进行概括: CountDownLatch基于AQS&#xff0c;它实现了闩锁&#xff0c;在开发中可以将其用作任务计数器。 若想要较为系统地去理解这些特性&#xff…

(十三)大数据实战——hadoop集群之YARN高可用实现自动故障转移

前言 本节内容是关于hadoop集群下yarn服务的高可用搭建&#xff0c;以及其发生故障转移的处理&#xff0c;同样需要依赖zookeeper集群的实现&#xff0c;实现该集群搭建时&#xff0c;我们要预先保证zookeeper集群是启动状态。yarn的高可用同样依赖zookeeper的临时节点及监控&…

scanf函数读取数据 清空缓冲区

scanf函数读取数据&清空缓冲区 scanf 从输入缓冲区读取数据数据的接收数据存入缓冲区scanf 中%d读取数据scanf中%c读取数据 清空输入缓冲区例子用getchar()吸收回车练习 scanf 从输入缓冲区读取数据 首先&#xff0c;要清楚的是&#xff0c;scanf在读取数据的时候&#xff…

HttpServletRequest和HttpServletResponse的获取与使用

相关笔记&#xff1a;【JavaWeb之Servlet】 文章目录 1、Servlet复习2、HttpServletRequest的使用3、HttpServletResponse的使用4、获取HttpServletRequest和HttpServletResponse 1、Servlet复习 Servlet是JavaWeb的三大组件之一&#xff1a; ServletFilter 过滤器Listener 监…

分享一个页面

先看效果&#xff1a; 看下代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>顶部或底部特效</title><style>import url(//fonts.googleapis.com/css?familyLato:300:4…

(亲测解决)PyCharm 从目录下导包提示 unresolved reference(完整图解)

最近在进行一个Flask项目的过程中遇到了unresolved reference 包名的问题&#xff0c;在网上找了好久解决方案&#xff0c;并没有一个能让我一步到位解决问题的。 后来&#xff0c;我对该问题和网上的解决方案进行了分析&#xff0c;发现网上大多数都是针对项目同一目录下的py…

servlet生命周期和初始化参数传递

servlet生命周期和初始化参数传递 1、servlet生命周期 只有第一次访问才会初始化&#xff0c;之后访问都只执行service中的。 除非tomcat关闭重新启动&#xff1a; 2、初始化参数传递

在eclipse里进行Junit单元测试并生成测试报告

在eclipse里进行Junit单元测试&#xff0c;并生成测试报告 准备工作单元测试步骤1.引入Junit2.生成测试类3.生成测试报告 准备工作 eclipse里自带Junit&#xff0c;不需要下载相应jar包&#xff0c;所以你只需要新建一个Java Project&#xff0c;在里面写你想要测试的java类文…

【CodeWhisperer】亚马逊版代码生成工具

大家好&#xff0c;我是荷逸&#xff0c;今天给大家带来的是代码生成工具【CodeWhisperer】 CodeWhisperer简介 CodeWhisperer是亚⻢逊出品的一款基于机器学习的通用代码生成器&#xff0c;可实时提供代码建议。 在编写代码时&#xff0c;它会自动根据我们现有的代码和注释生…

数据特征选择 | Matlab实现具有深度度量学习的时频特征嵌入

文章目录 效果一览文章概述源码设计参考资料效果一览 文章概述 数据特征选择 | Matlab实现具有深度度量学习的时频特征嵌入。 深度度量学习尝试学习非线性特征嵌入或编码器,它可以减少来自同一类的示例之间的距离(度量)并增加来自不同类的示例之间的距离。 以这种方式工作的…

Portraiture 4.0.3 for windows/Mac简体中文版(ps人像磨皮滤镜插件)

Imagenomic Portraiture系列插件作为PS磨皮美白必备插件&#xff0c;可以说是最强&#xff0c;今天它更新到了4.0.3版本。但是全网都没有汉化包&#xff0c;经过几个日夜汉化&#xff0c;终于汉化完成可能是全网首个Portraiture 4的汉化包&#xff0c;请大家体验&#xff0c;有…