【Linux】线程控制分析:如何获取线程ID?线程如何自动回收?

news2025/1/19 2:39:07

Linux系统中, 线程是轻量级的进程. 我们已经介绍过了线程的相关概念, 见过了线程再Linux操作系统中的存在形式.

我们知道, 进程有自己相关控制接口, 等待、创建等

而线程作为轻量级的进程, 其实也是有控制接口的.


文章目录

  • 线程控制
    • 线程的创建与回收演示
    • 获取线程id
      • `pthread_self()` 获取线程id
    • 线程的状态
      • 线程与进程共享信号处理方法
        • 线程异常
    • 线程退出
      • `pthread_exit()`
      • `pthread_cancel()`
      • 被取消的线程的退信息
    • 线程分离
      • `pthread_detach()`

线程控制

在介绍线程的相关概念的时候, 我们简单的演示了一下, 线程的创建和回收. 以及使用ps命令 展示了操作系统中正在运行的线程.

线程的创建与回收演示

使用 pthread_create()pthread_join() 两个接口来创建和回收线程已经演示过了:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void* callBack1(void* args) {
    string str = (char*)args;
    while (true) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
    }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while (true) {
        cout << " 主线程运行: " << getpid() << " " << endl;
        sleep(1);
    }

    pthread_join(tid1, nullptr);

    return 0;
}

 |wide

这段代码的执行结果, 是两个线程同时运行.

pthread_create() 的使用并不复杂, 只需要接收线程id, 并指定线程需要执行的回调函数 和 参数就可以了.

pthread_join() 这个回收线程的函数也不复杂. 只不过 此函数的第二个参数是一个 二级指针.

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

void** retval 参数是一个输出型参数, 是用来接收数据的.

不过为什么, retval是一个二级指针的类型呢?

其实原因很简单, 因为我们在使用 pthread_create() 接口创建线程的时候, 给线程指定的回调函数的返回值是 void* 类型的.

pthread_join() 的作用是回收线程, 既然要回收线程那么就一定要接收到线程运行的结果, 即 需要接收 线程执行的回调函数的返回值.

函数的参数要接收一个指针类型的内容, 就是要用二级指针来接收.

即, pthread_join() 接口的第二个参数, 实际上可以接收线程执行的回调函数的返回值.

我们可以使用下面这段代码测试一下:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void* callBack1(void* args) {
    string str = (char*)args;
    int cnt = 5;
    while (cnt) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
        cnt--;
    }

    return (void*)"thread_1 is over";
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    void* ret = nullptr;
    pthread_join(tid1, &ret);

    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl; 

    sleep(2);
    cout << (char*)ret << endl;
    
    return 0;
}

这段代码的执行结果是:

thread_join_retval

可以看到, 我们输出传入 join接口的参数, 得到的结果是 thread_1 执行的回调函数的返回值.


pthread_create()pthread_join() 这两个接口的使用都不算困难.

其实这两个接口都不是系统调用, 而是第三方库 pthread 中的接口. 不过虽然他们不是系统调用, 却胜似系统调用. 因为 Linux中其实是必须装载这个库的.

这两个不是系统调用, Linux操作系统提供的真正的系统调用是什么呢?

Linux操作系统的线程是轻量级进程, 也就是说 Linux其实并没有提供创建线程的系统调用, 因为根本就没有独立的线程这个概念.

所以, Linux操作系统给我们提供的创建线程的系统调用接口:

vfork():

这个系统调用接口是用来创建与父进程共享进程地址空间的子进程的, 其实就是一个线程.

clone():

还有clone() 系统调用. 这个接口作用是创建一个子进程来模拟线程.

不过看参数就可以看出来这个函数, 太麻烦了!

还需要自己定义方法, 自己定义一个子进程的栈(线程栈)等内容.

这个接口是为了更加细粒度的定制创建一个线程. 就是太麻烦了. 非常的麻烦. 看看就好.

我们更常用的还是 pthread库中的接口, pthread_create

获取线程id

pthread_create() 接口的第一个参数是一个输出型参数, 其实就是为了接收线程id的.

也就是说, 创建完线程之后 其实就已经得到了创建的线程的id:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void* callBack1(void* args) {
    string str = (char*)args;
    int cnt = 5;
    while (cnt) {
        cout << str << ": " << getpid() << " " << endl;
        sleep(1);
        cnt--;
    }

    return (void*)"thread_1 is over";
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    cout << "thread_1 id: " << tid1 << endl;

    while (true) {
        cout << " 主线程运行: " << getpid() << " " << endl;
        sleep(1);
    }

    return 0;
}

thread_id

代码的执行结果显示, 线程的id是一个非常长的数值. 我们暂时不考虑其有什么含义.

上面这种方法是通过创建线程时接收到的id 来获取线程id.

除此之外, pthread 还提供了一个获取线程自己id的接口: pthread_self()

pthread_self() 获取线程id

此接口作用是:获取调用此接口的线程的ID, 并作为返回值返回.

那么, 我们就可以定义一个函数, 来输出哪个线程和此线程的ID.

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (cnt) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
    }

    return (void*)"thread_1 is over";
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while(true) {
        cout << "主线程运行: " << getpid() << endl;
        sleep(1);
    }

    pthread_join(tid1, nullptr);

    return 0;
}

代码的执行结果为:

pthread_self

线程成功获取了自己的ID

线程的状态

Linux中 不能使用ps像查看进程状态那样细致的查看线程的状态.

不过还是可以简单的判断一下的.

对于线程, 如果线程退出了但是没有回收, 线程会怎么样呢?

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (cnt) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
    }
    cout << "thread_1 is over" << endl;

    int* ret = new int(123);
    return (void*)ret;				// 返回一个堆区数据 123
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");
    
    sleep(15);  // 等15s,让thread_1运行完

    void* ret = nullptr;
    pthread_join(tid1, &ret);
    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl;
    
    sleep(2);

    cout << "print thread_1 ret: " << *((int*)ret) << endl;
    delete (int*)ret; 		// 释放堆区数据

    return 0;
}

我们可以使用命令行监控脚本, 然后在执行这段代码.

// 监控线程脚本
while :; do ps -aL |head -1 && ps -aL |grep myThread |grep -v grep; sleep 1; done;

线程退出之后, 但还未join

执行和监控结果是, 当线程退出但还未被回收时, 线程会立刻从ps打印的列表中消失.

这说明什么呢?能说明 线程会被操作系统自动回收, 我们不用join吗?

不能, 因为ps终究只是一个软件. 没有证据可以证明 线程退出之后会被立刻回收, 不用手动回收.

事实也的确如此, 线程退出之后, 并不是自动回收的, 如果不手动join, 就可能会造成类似进程不回收一般的内存泄漏问题


线程与进程共享信号处理方法

线程和进程是共享信号处理方法的. 这个概念在上一篇线程概念的文章中就已经提到过, 但是没有演示.

在本篇文章中, 我们可以演示一下这个特点.

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    while (true) {
        printTid(threadName, pthread_self());s
        sleep(1);
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while(true) {
        printTid("main thread", pthread_self());
        sleep(1);
    }
}

线程和进程接收信号相同状态

我们向进程发送19信号, 所有线程都会暂停运行.

我们向进程发送一个18信号, 所有线程又会恢复运行.

ps列表中, 表示进程状态的一栏 l 即表示 存在轻量化进程, 即多线程进程

+ 表示前台进程

线程异常

线程异常会影响整个进程, 原因是线程异常影响的是整个进程的代码和数据.

所以, 如果线程异常 则整个进程都会异常

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << ", pid: " << getpid() << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (true) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
        if(cnt == 0) {
            int i = 1;
            i /= 0;
            // 段错误
            //int* pi = nullptr;
            //*pi = 123;
        }
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");

    while(true) {
        printTid("main thread", pthread_self());
        sleep(1);
    }
    return 0;
}

线程浮点异常

线程发生段错误

当线程出现不同的异常, 会影响进程的终止.

这其实是线程的 健壮性较低

不过这其实还是因为出bug了.

线程退出

介绍进程时, 我们分析进程退出存在三种情况:

  1. 代码跑完, 结果正确, 正常退出
  2. 代码跑完, 结果不正确, 正常退出
  3. 代码没跑完, 进程异常退出

其实线程也是一样的, 线程退出也分为这三种.

其实, 这三种情况可以统称为 执行流的退出情况

在父子进程中, 子进程退出不论是正常退出还是异常退出, 都会向父进程发送退出信息.

而 线程中, 只有线程正常退出且回收时, 主线程可以接收到线程的退出信息.

那么, 为什么线程异常时 主线程不会接收到来自线程的退出信息?

答案其实很简单, 因为线程异常, 也就是进程异常, 进程也会随之退出. 接不接受线程的退出信息已经没有意义了


以正常退出的情况来说, 除了回调函数内 return.

线程退出还可以使用接口退出.

pthread_exit()

pthread_exit() 接口也是 pthread 库提供的, 作用就是 以指定的退出信息使线程退出.

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << ", pid: " << getpid() << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (true) {
        printTid(threadName, pthread_self());
        sleep(1);
        cnt--;
        if(cnt == 0) {
            pthread_exit((void*)123);
        }
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");
    sleep(10);

    void* ret = nullptr;
    pthread_join(tid1, &ret);
    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl;
    sleep(2);
    cout << "print thread_1 ret: " << (long long)ret << endl;
    
    return 0;
}

线程退出

pthread_cancel()

pthread_cancel() 接口可以向指定的线程发送取消请求.

此接口可以**同一进程内, 任意线程调用并向任意线程发送**. 即, 可以在主线程中调用向新线程发送请求. 也可以自己想自己发送请求

并且, 如果线程是被取消的, 那么此线程的退出信息就是-1.

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t &tid) {
    cout << threadName << " is runing, " << "tid: " << tid << ", pid: " << getpid() << endl;
}

void* callBack1(void* args) {
    char* threadName = (char*)args;
    int cnt = 5;
    while (true) {
        printTid(threadName, pthread_self());

        sleep(1);
        cnt--;
        if(cnt == 0) {
            pthread_exit((void*)123);
        }
     }
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, callBack1, (void*)"thread_1");
    sleep(2);
	 pthread_cancel(tid1); 						// 主线程向新线程发送取消请求
   	cout << "main thread cancel thread_1" << endl;

    void* ret = nullptr;
    pthread_join(tid1, &ret);
    cout << "main thread join thread_1 , ready to print thread_1 ret" << endl;
    sleep(2);
    cout << "print thread_1 ret: " << (long long)ret << endl;

    return 0;
}

首先是, 主线程向新线程发送取消请求

主线程向新线程发送取消请求

新线程被取消 退出, 退出信息为-1. 即为 取消退出.

即 主线程是可以向新线程发送取消信号的. 但是 有可能发生错误.

我们上述代码中, 向新线程发送取消信号的动作 是在创建新线程2s之后执行的.

如果将那2s的暂停取消(主线程中, cancel动作前的 sleep(2)):

主线程向新线程发送取消请求, 出现错误

可以发现, 线程、进程都不正常的退出了.

同一进程内的所有线程都可以调用pthread_cancel()像任意线程发送取消信号.

甚至可以向主线程发送取消信号. 线程自己也可以向自己发送取消信号.

可以自己去测试一下.

被取消的线程的退信息

线程被成功的取消, 我们用 pthread_join() 接收线程的退出信息. 结果得到的退出信息是-1

那, 这个-1是从哪里来的呢?

我们知道, Linux中线程是由PCB模拟实现的, PCB中维护的都有自己执行流的退出信息.

我们return也好 或者 调用pthread_exit() 也好, 实际上都会修改PCB中维护的退出信息.

pthread_cancel() 也是如此, 如果取消线程成功了, 操作系统就会修改线程PCB中的退出信息.

将退出信息改为 PTHREAD_CANCELED . 这是一个 pthread 库提供的宏, 其实就是 ((void*)-1) 的宏定义:

|wide

线程分离

操作系统中的线程, 在默认情况下是 joinable 的.

即, 线程退出之后 是需要调用 pthread_join 进行接收线程信息和资源回收的, 否则可能会造成内存泄漏问题.

不过, 如果一个线程不需要关心返回值, 如果不是需要回收资源, 其实 join 的必要没有那么大.

那么, 对于不关心返回值的线程, 可否不用 join回收资源, 可否让线程自动回收资源呢

是可以的. 这样的操作叫 线程分离

pthread_detach()

此接口的作用, 其实可以理解为 将线程与主线程分离. 即 主线程不在管这个线程, 主线程也就不关心退出信息, 不关心资源回收.

这个接口一般线程自己调用或主线程调用.

不过, joinable 和 分离 是冲突的. 毕竟 joinable 表示线程需要调用join回收, 分离线程 则表示此线程是自动回收的. 很明显是两个冲突的状态

线程join成功

这是线程被正常join的现象.

如果我们设置分离:

#include <iostream>
#include <cstring>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t& tid) {
    printf("%s is runing, tid: %lu, pid: %d\n", threadName, tid, getpid());
}

void* startRoutine(void* args) {
    pthread_detach(pthread_self());			// 线程分离
    string name = (char*)args;
    int cnt = 1;
    while (cnt--) {
        printTid(name.c_str(), pthread_self());
        sleep(1);
    }
    printf("%s is over\n", name.c_str());

    return nullptr;
}

int main() {
    pthread_t tid1, tid2, tid3, tid4;

    pthread_create(&tid1, nullptr, startRoutine, (void*)"thread_1");
    pthread_create(&tid2, nullptr, startRoutine, (void*)"thread_2");
    pthread_create(&tid3, nullptr, startRoutine, (void*)"thread_3");
    pthread_create(&tid4, nullptr, startRoutine, (void*)"thread_4");

    sleep(2);

    int joinRet = pthread_join(tid1, nullptr);
    cout << strerror(joinRet) << endl;
    joinRet = pthread_join(tid2, nullptr);
    cout << strerror(joinRet) << endl;
    joinRet = pthread_join(tid3, nullptr);
    cout << strerror(joinRet) << endl;
    joinRet = pthread_join(tid4, nullptr);
    cout << strerror(joinRet) << endl;

    return 0;
}

上面的这段代码, 在线程需要执行的回调函数内 调用pthread_detach(pthread_self());将线程自己分离, 然后主线程内依旧使用 pthread_join() 回收. 不过接收返回值, 判断join执行的结果:

分离和joinable状态不可共存

可以看到, 主线程中的 pthread_join() 并没有成功的将4个线程回收掉. 而是报出了 Invalid argument 无效参数 的错误.

这其实就意味着, 分离过的线程 在运行结束之后就自动被回收了, 无法再用 pthread_join() 回收.


上面举得例子是正确使用的情况.

如果, 我们将主线程中的 sleep(2); 语句删除了, 并且我们只创建、回收一个线程:

#include <iostream>
#include <cstring>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t& tid) {
    printf("%s is runing, tid: %lu, pid: %d\n", threadName, tid, getpid());
}

void* startRoutine(void* args) {
    pthread_detach(pthread_self());
    string name = (char*)args;
    int cnt = 1;
    while (cnt--) {
        printTid(name.c_str(), pthread_self());
        sleep(1);
    }
    printf("%s is over\n", name.c_str());

    return nullptr;
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, startRoutine, (void*)"thread_1");

    int joinRet = pthread_join(tid1, nullptr);
    cout << strerror(joinRet) << endl;

    return 0;
}

我们依旧在 回调函数内将线程分离, 那么这段代码的执行结果是:

分离了, 但是join执行成功

惊奇的发现, 线程退出之后成功得被 join 回收了.

这是什么原因?我们不是在线程内分离线程了吗?

其实是因为, 在主线程内 我们创建了线程之后 没有使主线程暂停一会, 直接就继续执行了 pthread_join(). 线程还没来得及 将自己分离.

所以线程退出时, 又会被主线程中的 pthread_join() 回收成功

要避免这种情况, 可以在创建线程之后将主线程等一会, 让新线程执行完分离再让主线程继续执行.

或者, 可以直接在主线程内将线程分离.

#include <iostream>
#include <cstring>
#include <string>
#include <pthread.h>
#include <unistd.h>
using std::cout;
using std::endl;
using std::string;

void printTid(const char* threadName, const pthread_t& tid) {
    printf("%s is runing, tid: %lu, pid: %d\n", threadName, tid, getpid());
}

void* startRoutine(void* args) {
    string name = (char*)args;
    int cnt = 1;
    while (cnt--) {
        printTid(name.c_str(), pthread_self());
        sleep(1);
    }
    printf("%s is over\n", name.c_str());

    return nullptr;
}

int main() {
    pthread_t tid1;

    pthread_create(&tid1, nullptr, startRoutine, (void*)"thread_1");
    pthread_detach(tid1);
    cout << "main thread detach thread_1" << endl;

    int joinRet = pthread_join(tid1, nullptr);
    cout << strerror(joinRet) << endl;
	
    sleep(5); 		// 防止主线程先退出
    
    return 0;
}

在主线程中分离线程

这样就可以解决 join 时, 新线程还未分离的问题.


线程分离之后, 主线程就不再管分离的线程了. 即使主线程先退出了, 也不会管分离的线程. 即主线程先退出, 被分离的线程可能还会运行.

所以, 存在线程被分离时, 我们的一般会将主线程不退出, 常驻内存

线程分离, 就像是给线程设置了一下让线程如何退出. 等线程执行完毕之后, 自动退出回收.

所以, 其实 线程分离也可以看作是线程的第四种退出方式, 延迟退出

第一种是, 回调函数返回

第二种是, pthread_exit()

第三种是, pthread_cancel()


到这里, 线程的概念和控制基本上介绍完了.

但是还有一个问题, 就是 如何理解线程id. 不过会是下一篇文章的内容.

感谢阅读~

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

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

相关文章

用户管理系统-自动化测试

文章目录1. 思维导图编写 Web 自动化测试用例2. 创建测试项目3. 根据思维导图设计用户管理系统自动化测试用例3.1 准备工具类3.2 测试登录页面3.3 测试用户列表页3.4 测试添加用户页3.5 测试修改用户页3.6 未登录状态4. 自动化测试项目总结4.1 自动化测试项目实现步骤4.2 当前项…

图数据库驱动的基础设施运维实操

本文系图技术在大型、复杂基础设施之中 SRE/DevOps 的实践参考&#xff0c;并以 OpenStack 系统之上的图数据库增强的运维案例为例&#xff0c;揭示图数据库、图算法在智能运维上的应用。本文所有示例代码开源。 最近&#xff0c;有些尚未使用过图技术、DevOps/Infra 领域的工程…

除了Java,还可以培训学习哪些IT技术?

除了Java&#xff0c;还可以培训学习哪些IT技术&#xff1f;转行IT学Java似乎已经成为很多人的首选&#xff0c;原因无非是开发技术含量高、开发有前景、开发是一个互联网企业的核心岗位&#xff0c;最重要的是开发薪资待遇高。但其实只单纯因为薪资选择Java的话&#xff0c;小…

Flask数据迁移详细步骤

数据迁移详细步骤&#xff1a; 1. 安装好数据迁移的包 flask-sqlalchemy和flask-migrate Flask模型相关包安装 2. 在exts.py中初始化Migrate和SQLAlchemy 3. 在models中定义好模型 4. 在views.py中一定要导入models模块 from .models import * 5. 配置好数据库&#xff08;sql…

MYSQL笔记01 数据库概述,SELECT语句,运算符,排序与分页,多表查询

数据库概述 为什么要使用数据库 持久化&#xff1a;把数据保存在可掉电式存储设备中以供之后使用。大多数情况下&#xff0c;特别是企业级应用&#xff0c;数据持久化意味着将内存中的数据保存到硬盘上加以"固化"&#xff0c;而持久化的实现过程大多通过各种关系数据…

详解 23 种设计模式(多图 + 代码)

创建型模式 创建型模式的作用就是创建对象&#xff0c;说到创建一个对象&#xff0c;最熟悉的就是 new 一个对象&#xff0c;然后 set 相关属性。但是&#xff0c;在很多场景下&#xff0c;我们需要给客户端提供更加友好的创建对象的方式&#xff0c;尤其是那种我们定义了类&a…

每68个孩子里,就有一个自闭症,“来自星星的孩子”,离我们很近

今年4月2日是世界上第14个“世界自闭症日”。自闭症儿童&#xff0c;又称“星星儿童”&#xff0c;形容他们像遥远的星星一样独自在夜空中闪耀。自闭症并不少见根据世卫组织的调查&#xff0c;世界上每160名儿童中就有一人患有自闭症。根据《中国自闭症&#xff08;自闭症&…

【从零开始学Skynet】基础篇(四):网络模块常用API

游戏服务端要处理客户端请求&#xff0c;作为服务端引擎&#xff0c;网络编程也是Skynet的核心功能。1、学习网络模块 skynet.socket模块提供了网络编程的API&#xff0c;常用的API如下表所示&#xff1a;Lua API说明socket.listen(address ,port)监听一个端口&#xff0c;返回…

你需要知道的企业网页制作流程

企业网页制作是企业建立线上形象和宣传的重要手段之一&#xff0c;它不仅可以提高企业的品牌知名度&#xff0c;还可以扩大企业的影响力和拓展客户群。下面&#xff0c;我们将介绍一些企业网页制作的基本流程和技巧&#xff0c;并结合一个案例来详细解析。 企业网页制作的基本…

【DT】蒸脱机的结构和工作原理

DT蒸脱机的结构和工作原理什么是DTDT结构图工作过程什么是DT DT 蒸脱机&#xff08;DesolventazationerToaster&#xff09;&#xff0c;根据英文名可以看出来&#xff0c;他的作用是脱溶、烘烤。用于蒸脱湿豆粕中的溶剂。 大豆油生产工艺有2种&#xff1a;压榨油的加工工艺是…

C++标准库--IO库(Primer C++ 第五版 · 阅读笔记)

C标准库--IO库(Primer C 第五版 阅读笔记&#xff09;第8章 IO库8.1、IO类8.2、文件输入输出8.3、string流总结&#xff1a;第8章 IO库 8.1、IO类 为了支持这些不同种类的IO处理操作&#xff0c;在istream和ostream之外&#xff0c;标准库还定义了其他一些IO类型。 如下图分…

Java中的注解,自定义注解

文章目录1. 注解概述2. 注解与注释3. 注解的重要性4. 常见的Annotation作用4.1 生成文档相关的注解4.2 在编译时进行格式检查(JDK内置的三个基本注解)5. 元注解6. 自定义注解6.1 定义自定义注解6.2 使用自定义注解6.3 读取和处理自定义注解框架 注解 反射 设计模式 1. 注解概…

PC安装虚拟化平台趟坑记录

合肥先进光源永磁多极铁电机控制系统的规划 Zstack EPICS Archiver在小课题组的使用经验 神仙同学的永磁四极铁样铁已经开始加工了&#xff0c;过一个月左右就要回来了&#xff0c;电机控制部分交给留国做&#xff0c;调试的也差不多了。项目买过一台工控机&#xff0c;到时候…

STM32F407ZGT6实现OLED显示屏

1、调试工具 2、OLED简介 3、硬件电路&#xff08;接线&#xff09; 本文采用7脚,倘若采用4脚&#xff0c;资料代码啥的可以在江科大B站视频下载&#xff1a; 资料下载&#xff1a;https://pan.baidu.com/s/1SqKyKr5Fsl_9gBJi8aVxTw&#xff0c; 提取码&#xff1a;8kzh&#x…

日本首相会见奥特曼,考虑引入 ChatGPT 技术

文&#xff5c;小戏卖萌屋日本4月12日电&#xff0c;日本国第101任首相&#xff0c;日本自民党总裁岸田文雄4月10日于东京会见了奥特曼先生&#xff0c;二人就 ChatGPT 引入日本的可能性问题交换了意见并进行了深入的讨论。奥特曼先生表示&#xff0c;希望为日本人创造伟大的东…

NumPy 秘籍中文第二版:三、掌握常用函数

原文&#xff1a;NumPy Cookbook - Second Edition 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 在本章中&#xff0c;我们将介绍许多常用函数&#xff1a; sqrt()&#xff0c;log()&#xff0c;arange()&#xff0c;astype()和sum()ceil()&#xff0c;modf()&…

如何突破LinkedIn领英限制,导出非好友邮箱等社交方式

相信做外贸的朋友都有使用过Linkedin&#xff0c;如果还没有使用过的话&#xff0c;我只能说您错过一个很好的平台。只要是厉害的外贸人都特别擅长用Linkedin找客户。 那为什么说Linkedin是外贸业务员开发客户最有效的途径呢&#xff1f;主要基于以下几点&#xff1a; 第一&a…

常见的相似度计算方式

1.欧氏距离(Euclidean Distance) 欧氏距离(也称欧几里得度量)指在mmm维空间中两个点之间的真实距离&#xff0c;或者向量的自然长度(即该点到原点的距离)。 在二维和三维空间中的欧氏距离就是两点之间的实际距离。 计算公式&#xff1a; dist(A,B)∑i1n(Ai−Bi)2dist(A,B)\sqr…

DIN35电压电流转频率单位脉冲输出信号变换器集电极开路隔离变送器

主要特性 将直流电压或电流信号转换成单位脉冲信号。 精度等级&#xff1a;0.1 级、0.2 级。产品出厂前已检验校正&#xff0c;用户可以直接使用。 国际标准信号输入:0-5V/0-10V/1-5V 等电压信号,0-10mA/0-20mA/4-20mA 等电流信号。 输出标准信号&#xff1a;0-5KHz/0-…

MySQL 索引常见问题汇总,一次性梳理

hello&#xff0c;大家好&#xff0c;我是张张&#xff0c;「架构精进之路」公号作者。 提到MySQL查询分析&#xff0c;就会涉及到索引相关知识&#xff0c;要想学好MySQL&#xff0c;索引是重要且不得不啃下的一环&#xff0c;今天就把MySQL索引常见问题进行汇总&#xff0c;一…