Linux中的线程

news2025/3/1 20:48:52

目录

线程的概念

进程与线程的关系

线程创建

线程终止

线程等待

线程分离

原生线程库

线程局部存储

自己实现线程封装

线程的优缺点

多线程共享与独占资源

线程互斥

互斥锁

自己实现锁的封装

加锁实现互斥的原理

死锁

线程同步


线程的概念

回顾进程相关概念

● 进程 = 内核数据结构(pcb等) + 内存中的代码/数据

● 进程创建成本较高,需要创建pcb, 进程地址空间,页表,构建页表映射关系,将磁盘的代码和数据加载到内存中等一系列工作

● 一个进程访问的大部分资源都在物理内存中,需要通过进程地址空间+页表获取到,因此可以认为进程地址空间是进程的"资源窗口",因此进程是承担系统资源分配的基本实体

● 创建进程目的是为了让cpu去调度执行进程中的代码,访问相应的数据,完成任务,因此,之前的认知是: 一个进程本质就是一个执行流

线程的概念

● 在一个进程内只创建若干pcb,这些pcb指向同一个进程地址空间,通过同一个页表,映射到同一个内存,看到的是同一份资源

● 目前一个进程内有多个pcb了,本质就是有多个执行流了,多个执行流的地位是对等的,cpu调度时选择任意一个执行流调度即可,每个执行流本质就是一个线程,所以cpu调度的基本单位是线程

● 每个线程都有自己的pcb, 不同的pcb中保存同一个虚拟地址空间的不同起始地址,进而通过页表映射到不同的物理内存区域,相当于多线程瓜分了进程地址空间,从而并发执行同一个进程内的不同代码,共同完成一项任务

● 多个线程由于共用同一个进程地址空间,通过同一个页表映射,看到的是同一份资源,所以资源共享在线程之间显得非常容易,比如全局变量、环境变量、命令行参数等

● 线程是在进程内部执行的一种执行流

线程是更加轻量级的进程/线程是比进程更加轻量化的执行流

a.创建线程更加简单了,因为创建进程时该进程用到的资源都申请好了,一系列工作都已经做好了,创建线程只是在分配资源!!! 

b.创建线程更加简单意味着释放线程也更加容易了!

c.线程调度也更加简单

c.1 因为不同的线程看到的是同一个地址空间,访问的是同一个资源,因此线程间切换时只需要把一个pcb切换成另一个pcb,  把保存线程临时数据的少量寄存器切换,而页表和地址空间不用切换!

c.2 cpu内部集成了高速缓存cache,线程间切换不需要切换cache, 因为cache中保存的是整个进程中高频访问的数据,但是进程间切换需要切换cache, 因为cache中的大部分数据都失效了!!! 这是线程创建更加简单的最主要的原因

● 创建线程时,线程会瓜分进程总体的时间片,因为时间片也是资源!

● 站在cpu角度,cpu不需要区分调度的是线程还是进程,只需要找到pcb,找到进程地址空间,通过页表映射执行代码即可

Linux中并不存在真正的线程,只有"轻量级进程"的概念

一个进程内可能存在多个线程,要不要把所有的线程管理起来呢?? 要管理! 如何管理? 先描述,再组织!  --- 描述结构体叫做 tcb, 而线程也要有自己的各种队列,线程id, 状态,调度算法等,这都是 tcb中的属性字段,最后把所有tcb用链表链接起来!!!

事实上,windows就是这样实现的,而Linux系统中,并没有单独为线程设计tcb,因为线程的大部分属性特征进程也是有的,线程和进程都是执行流,  不必为线程单独设计,反倒会增加程序员的负担,因此Linux中用pcb可以充当tcb, 所有代码在线程级别上复用即可, 一整套调度算法也可以直接复用!

进程与线程的关系

线程创建

● pthread_create 接口

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

功能: 创建一个新线程

参数:

thread: 输出型参数,获取新线程id

attr: 创建线程时设置的线程属性,直接设为nullptr即可

start_routine:新线程执行的函数

arg: 新线程执行函数的参数

返回值: 创建成功,返回0,创建失败,返回错误码

● 创建线程代码示例

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

//新线程
void* ThreadRoutine(void* arg)
{
    const char* threadName = (const char*)arg;
    while(true)
    {
        cout << "I am a new thread "<< threadName << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    //主线程
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread 1");
    while(true)
    {
        cout << "I am main thread" << endl;
        sleep(1);
    }
    return 0;
}

● 尽管有主线程和新线程两个线程,但始终只有1个进程,因此打印出的进程pid是一样的

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

void* ThreadRoutine(void* arg)
{
    const char* threadName = (const char*)arg;
    while(true)
    {
        cout << "I am a new thread" << ", pid: " << getpid() << ", " << threadName << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread 1");
    while(true)
    {
        cout << "I am main thread" << ", pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

●  ps -aL 指令查看系统内的线程,cpu调度线程依据的是LWP(light weight process), PID和LWP一样,就是主线程,否则是新线程

● 线程之间看到同一份资源是非常容易的,比如定义一个全局变量,线程就都能看到了!

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;

int gcnt = 100;

void *ThreadRoutine(void *arg)
{
    while (true)
    {
        cout << "I am a new thread, gcnt: "  << gcnt << ", &gcnt : " << &gcnt << endl;
        gcnt--;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRoutine, nullptr);
    while (true)
    {
        cout << "I am main thread, gcnt: "  << gcnt << ", &gcnt : " << &gcnt << endl;
        sleep(1);
    }
    return 0;
}

● 创建多线程

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <functional>
#include <vector>
using namespace std;

using func_t = function<void()>;

class ThreadData
{
public:
    ThreadData(const string &name, const uint64_t &ctime, func_t f)
        : threadname(name), createtime(ctime), func(f)
    {}

public:
    string threadname;
    uint64_t createtime;
    func_t func;
};

void Print()
{
    cout << "我是线程执行的大任务的一部分" << endl;
}

// 新线程
void *ThreadRoutine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args); // 安全强转
    while (true)
    {
        cout << "new thread" << ", thread name :" << td->threadname
             << ", create time : " << td->createtime << endl;
        td->func();
        sleep(1);
    }
}

int main()
{
    for (int i = 0; i < 3; i++) 
    {
        char threadname[64];    
        snprintf(threadname, sizeof(threadname), "%s-%d", "thread", i);
        ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);
        pthread_t tid;
        pthread_create(&tid, nullptr, ThreadRoutine, td);
        sleep(1);
    }

    while (true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }
    return 0;
}

● pthread_create 接口的最后一个参数类型是void*,可以接收任意数据类型的地址,因此除了给线程执行方法传递常规的数据类型,还可以传递我们自己封装的类对象

● 类对象中可以封装自定义函数,传递给线程执行方法,在线程内部进行回调

线程终止

● pthread_self()接口可以获取调用该接口的线程id,本质是一个地址

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

//十进制数转十六进制数
string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

void* threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);
    usleep(1000);
    while(true)
    {
        cout << "new thread is running, thread name: " << name << ", my thread id: " 
        << ToHex(pthread_self()) << endl;
        sleep(1);
    } 
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");
    while(true)
    {
        cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;
        sleep(1);
    }
    return 0;
}

● 线程终止有很多方法,比如在线程中直接return(终止线程)/exit(本质是终止整个进程),也可以调用pthread_exit()接口终止线程

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
using namespace std;

string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

void* threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);
    usleep(1000);
    int cnt = 5;
    while(cnt--)
    {
        cout << "new thread is running, thread name: " << name << ", my thread id: " 
        << ToHex(pthread_self()) << endl;
        sleep(1);
    }
    pthread_exit(nullptr); //终止调用该接口的线程
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");
    while(true)
    {
        cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;
        sleep(1);
    }
    return 0;
}

● 主线程中调用pthread_cancle() 可以取消指定的线程

int pthread_cancel(pthread_t thread);

参数: 要取消的线程id

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

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

void* threadRoutine(void* args)
{
    int cnt = 10;
    while(cnt--)
    {
        cout << "thread is running..." << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);
    sleep(3);

    pthread_cancel(tid); //取消tid线程
    cout << "我是主线程,取消了新线程" << endl;
    return 0;
}

● 一个线程异常,整个进程都会终止

#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <functional>
#include <vector>
using namespace std;

using func_t = function<void()>;

class ThreadData
{
public:
    ThreadData(const string &name, const uint64_t &ctime, func_t f)
        : threadname(name), createtime(ctime), func(f)
    {
    }

public:
    string threadname;
    uint64_t createtime;
    func_t func;
};

void Print()
{
    cout << "我是线程执行的大任务的一部分" << endl;
}

// 新线程
void *ThreadRoutine(void *args)
{
    int a = 10;
    ThreadData *td = static_cast<ThreadData *>(args); // 安全强转
    while (true)
    {
        cout << "new thread" << ", thread name :" << td->threadname
             << ", create time : " << td->createtime << endl;
        td->func();

        //异常终止
        if(td->threadname == "thread-2")
        {
            cout << td->threadname << "触发了异常" << endl;
            a /= 0;
        }
        sleep(1);
    }
}

int main()
{
    for (int i = 0; i < 3; i++)
    {
        char threadname[64];
        snprintf(threadname, sizeof(threadname), "%s-%d", "thread", i);
        ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);
        pthread_t tid;
        pthread_create(&tid, nullptr, ThreadRoutine, td);
        sleep(1);
    }

    while (true)
    {
        cout << "main thread" << endl;
        sleep(1);
    }
    return 0;
}

线程等待

● 线程退出但没有被等待,也会出现和进程类似的僵尸问题

● 新线程退出时需要让主线程等待,从而获取新线程的退出信息

● 当一个新线程出异常了,其他线程也会受到影响,整个进程都终止了,主线程再等待新线程也就没有了意义

● pthread_join 线程等待代码演示

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

参数:

thread: 被等待的线程id

retval: 输出型参数,根据threadRoutine的返回值可以获取子进程的退出信息,如果不关心新线程的退出信息,该参数直接设置为nullptr即可

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

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

void* threadRoutine(void* args)
{
    int cnt = 5;
    while(cnt--)
    {
        cout << "thread is running..." << endl;
        sleep(1);
    }
    char* ret = "新线程正常退出啦!!!";
    return ret;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);
    sleep(3);

    void* ret = nullptr;
    pthread_join(tid, &ret); 
    cout << "main thread join done, thread return: "<< (char*)ret << endl;
    return 0;
}

● 线程执行方法的返回值是void*,可以返回任意类型的数据,自定义类对象也是可以的!

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
using namespace std;

string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", tid);
    return id;
}

class ThreadReturn
{
public:
    ThreadReturn(pthread_t id, const string& info, int code)
    :id_(id)
    ,info_(info)
    ,code_(code)
    {}

public:
    pthread_t id_;
    string info_;
    int code_;
};

void *threadRoutine(void *args)
{
    string name = static_cast<const char *>(args);
    usleep(1000);
    int cnt = 5;
    while (cnt--)
    {
        cout << "我是新线程, 正在运行噢, 我的线程id是: " << ToHex(pthread_self()) << endl;
        sleep(1);
    }
    ThreadReturn* ret = new ThreadReturn(pthread_self(),"thread quit normal", 10);
    return ret;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"thread-1");
    cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;

    void *ret = nullptr;
    pthread_join(tid, &ret);
    ThreadReturn* r = static_cast<ThreadReturn*>(ret);    
    cout << "main thread get new thread info : " << r->code_  << ", " << ToHex(r->id_)<< ", " << r->info_<< endl;
    return 0;
}

线程分离

大部分软件,跑起来之后都是死循环,比如用户打开qq, 打开网易云音乐等等,打开后不会自动退出的,直到用户手动关掉。也就是说,新线程大多数是不需要被等待的,主线程创建出新线程之后就让新线程去跑了,主线程就不管了

线程默认是joinable状态的,但如果主线程就是不想等待新线程,不关心新线程的退出状态,  主线程自己直接做其他事情,那么就可以将新线程设置为分离状态

可以在主线程中将新线程设置为分离状态,新线程也可以让自己设置成分离状态

线程分离代码演示

int pthread_detach(pthread_t thread);

参数:分离的线程id

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

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

void* threadRoutine(void* args)
{
    pthread_detach(pthread_self()); //新线程中将自己分离
    int cnt = 5;
    while(cnt--)
    {
        cout << "thread is running..." << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);
    //pthread_detach(tid); //主线程中将tid线程分离 
    int n = pthread_join(tid, nullptr);
    cout << n << endl;
    return 0;
}

● 线程被分离后,可以被取消,但不能被join,取消线程后线程返回值是PTHREAD_CANCELED

#define PTHREAD_CANCELED ((void *) -1)
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
using namespace std;

void* threadRoutine(void* args)
{
    int cnt = 5;
    while(cnt--)
    {
        cout << "thread is running..." << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);
    sleep(1);

    pthread_detach(tid); 

    int n = pthread_cancel(tid); //取消tid线程
    cout << "main thread cancel done, " << " n: " << n << endl;

    void* ret = nullptr;
    n = pthread_join(tid, &ret); 
    cout << "main thread join done, " << " n: " << n  << ", thread return:  "<< (int64_t)ret << endl;
    return 0;
}

原生线程库

Linux下没有真线程,只有轻量级进程,所以OS只会提供轻量级进程创建的系统调用,不会提供线程创建的系统调用

但用户只认线程,而且windows下是有真线程的,因此Linux在内核和用户层之间加了一层软件层,也就是pthread原生线程库,对内核的轻量级进程(LWP)接口进行封装,向上提供线程的一系列接口,同时管理多个线程,先描述,再组织,因此pthread库中是包含了一堆描述线程属性的结构体

原生线程库意思是任何一款操作系统都要默认有的,不属于C/C++语言本身, 因此编译时要带-l

作为用户,如果想知道一共创建了几个线程,每个线程的状态,当前有几个线程,一个线程退出了,退出结果是多少等信息,就直接去pthread库中获取即可

线程要有自己的一些独立属性:

1.上下文数据(被OS以轻量级进程形式维护在tcb中)

2.栈结构(栈大小,栈在哪里等信息,都必须在线程库中维护)

但是线程有多个,而地址空间中栈只有1个,如何分配??

clone接口 --- 创建轻量级进程, pthread_create的底层和fork的底层都是clone

第一个参数是线程执行的函数

第二个参数是线程库在堆区new的一段空间的起始地址,作为栈起始地址

第三个参数flags表示是创建轻量级进程还是创建一个真正的子进程

● 进程地址空间中的栈默认由主线程使用

线程库是共享的, 所以线程内部要管理整个系统中, 多个用户启动的多个线程!

而库要管理线程,就要在库中存在管理线程的结构体 --- struct pthread

线性局部存储是存放一些只能被线程自己看见的数据

线程栈就是保存了堆区new出来的一块空间的起始地址

每个线程在库中都是这三部分,可以把多个这部分看成一个数组,因此对线程的管理就转化成了对数组的增删查改

●  当线程退出时,退出结果会保存到库中的struct pthread中,因此主线程只需要去库中的struct pthread拷贝数据,拿到结果即可!

● 结论: pthread_t tid 表示的是线程属性集合在库中的地址!!! LWP是内核的概念!

C++的线程库本质是对pthread的封装, 因为去掉-lpthread选项之后报链接错误

线程局部存储

全局变量本身就是被线程共享的,而如果定义全局变量时带上__thread,会发现全局变量不是只有1份了,而是每个线程都有一份!

__thread修饰全局变量,会把全局变量拷贝到每个线程内的线程局部存储空间中!

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

//__thread是一个编译选项, 编译的时候就会把线程控制块中的空间开辟出来 --- 拷贝到线程局部存储空间中!
__thread int g_val = 100; 

void* threadRoutine(void* args)
{
    string name = static_cast<const char* >(args);
    while(true)
    {
        cout << "I am new thread" << ", thread name: " << name << ", g_val:" << g_val 
            << ", &g_val: " << &g_val << endl << endl;
        g_val++;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");
    while(true)
    {
        cout << "I am main thread" <<  ", g_val:" << g_val 
            << ", &g_val: " << &g_val << endl << endl;
        sleep(1);
    }

    pthread_join(tid, nullptr); 
    return 0;
}

● 线程局部存储的用途: 定义一个全局变量,用__thread修饰,这样就可以在每个线程内部获取到线程的lwp

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
using namespace std;

__thread pid_t lwp; 

void* threadRoutine(void* args)
{
    string name = static_cast<const char* >(args);
    lwp =  syscall(SYS_gettid); //系统调用获取当前线程的lwp
    while(true)
    {
        cout << "I am new thread" << ", thread name: " << name << "new thread lwp: " << lwp << endl; 
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");
    lwp =  syscall(SYS_gettid); //系统调用获取当前线程的lwp
    while(true)
    {
        cout << "I am new thread" << "new thread lwp: " << lwp << endl; 
        sleep(1);
    }
    pthread_join(tid, nullptr); 
    return 0;
}

注意:

__thread string threadname;  //err, __thread只能存储内置类型,不能存储一些容器

线程中可以fork, 本质是在创建子进程, 也可以调用execl, 不过替换的是整个进程,会影响其他所有线程,因此不建议在线程中excel,如果要execl,建议先fork, 再execl

自己实现线程封装

Thread.hpp

#pragma once 

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

//设计方的视角

template<class T>
using func_t = function<void(T)>;  //返回值为void, 参数为T的类型

template<class T>
class Thread
{
public:
    Thread(const string& threadname, func_t<T> func, T data)
        :_tid(0)
        ,_threadname(threadname)
        ,_isrunning(false)
        ,_func(func)
        ,_data(data)
    {}

    //改为static, 参数就没有this指针了!
    static void* ThreadRoutine(void* args) //不加static, 类内方法, 默认携带this指针
    {
        Thread* ts = static_cast<Thread *>(args); 
        ts->_func(ts->_data);
        return nullptr;   
    }

    //启动线程(内部调用线程创建)
    bool start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
        if(n == 0) 
        {
            _isrunning  = true;
            return true;
        }
        else 
        {
            return false;
        }
    }

    //线程等待
    bool join()
    {
        if(!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        else
        {
            return false;
        }
    }

    string ThreadName()
    {
        return _threadname;
    }
    
    bool IsRunning()
    {
        return _isrunning;
    }

    ~Thread()
    {}

private:
    pthread_t _tid; //线程id
    string  _threadname; //线程名
    bool _isrunning; //线程是否在运行
    func_t<T> _func; //线程执行方法
    T _data;
};

main.cc

#include <iostream>
#include <unistd.h>
#include <vector>
#include "thread.hpp"

//应用方的视角
string GetThreadName()
{
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread-%d", number++);
    return name;
}

void Print(int num)
{
    while(num)
    {
        cout << "hello world: " << num-- << endl;
        sleep(1);
    }    
}

int main()
{
    Thread<int> t(GetThreadName(), Print, 10);
    t.start();
    t.join();
    return 0;
}

线程的优缺点

优点:

创建一个新线程的代价要比创建一个新进程小得多

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

线程占用的资源要比进程少很多

能充分利用多处理器的可并行数量

在等待慢速I/O操作结束的同时程序可执行其他的计算任务

计算密集型应用,为了能在多处理器系统上运行, 将计算分解到多个线程中实现

I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点:

缺乏访问控制

进程是访问控制的基本粒度, 由于大部分资源都是共享的,在一个线程中调用某些OS函数会对整个进程造成影响,而同步和互斥就是在解决这个问题

健壮性/鲁棒性降低

多线程中,一个线程崩溃,整个进程都崩溃,而多进程程序,一个进程崩溃,不影响其他进程,因为进程具有独立性

编程难度提高

编写与调试一个多线程程序比单线程程序困难得多

性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

多线程共享与独占资源

多线程之间共享的资源

1. 进程代码段

2. 进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)

3. 进程打开的文件描述符

4. 信号的处理器

5. 进程的当前目录

6. 进程用户ID与进程组ID。

多线程之间独立的资源

1.线程ID

2.寄存器组的值

3.线程的堆栈

4.错误返回码

5.线程的信号屏蔽码

6.线程的优先级

线程互斥

下面是一段模拟多线程抢票的代码

#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"

//构造线程名称
string GetThreadName()
{
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread-%d", number++);
    return name;
}

//抢票逻辑
int ticket = 10000; // 全局的共享资源
void GetTicket(string name)
{
    while (true)
    {
        if (ticket > 0)
        {
            // 充当抢票花费的时间
            usleep(1000);
            printf("%s get a ticket : %d\n", name.c_str(), ticket);
            ticket--; 
        }
        else
        {
            break;
        }
        // 实际情况, 还有后续的动作
    }
}

int main()
{
    string name1 = GetThreadName();
    Thread<string> t1(name1, GetTicket, name1);
    sleep(2);

    string name2 = GetThreadName();
    Thread<string> t2(name2, GetTicket, name2);
    sleep(2);

    string name3 = GetThreadName();
    Thread<string> t3(name3, GetTicket, name3);
    sleep(2);

    t1.start();
    sleep(2);
    t2.start();
    sleep(2);
    t3.start();
    sleep(2);

    t1.join();
    t2.join();
    t3.join();
    return 0;
}

运行代码发现最后出现了票数出现了负数,但是我们在if语句中判断票数>0了呀,为啥还会出现票数为负数呢???

显然票数是公共资源,可以被多个执行流同时访问,而多个执行流同时访问公共资源显然出现了问题,因此我们需要把公共资源保护起来,使得任何一个时刻,只允许一个线程正在访问公共资源,此时公共资源就叫做临界资源! 而我们的代码中只有一部分代码会去访问临界资源的,进程中访问临界资源的代码叫做临界区

任何时刻只允许一个执行流进入临界区,使得多个执行流只能串行访问临界资源,叫做互斥!

++/--本质是三条汇编语句, 每一条汇编语句都是原子性的, 而执行每一条汇编语句都有可能被中断, 三条汇编语句过程是 先把内存中的a拷贝到cpup寄存器中,然后在寄存器中对a++, 最后将寄存器的a拷贝会回内存空间中!

多线程同时访问公共资源有什么问题呢??? 举个例子!

比如有A线程和B线程, 公共资源是int a = 10, 两个线程都要进行a++操作, 目前的情况是A线程把汇编的第二步执行完毕,寄存器中a为11, 然后被切换走了,于是A线程的上下文数据中就保存了a为11, 此时线程B被cpu调度,一直将内存空间中的a++到了100,此时被切走了,线程A被调度,接着执行第三条汇编语句,将自己的上下文数据,a=11恢复到寄存器中,然后将寄存器内容写回内存,于是内存空间中的a改为了11,就出现了数据不一致的问题!!

而我们今天的抢票代码最后票出现了负数原因是:

当票数减为1时,多个线程进行了if条件判断,都是成立的,语句进入到了if循环内部,此时某个线程被调度,将内存中的tickets--到了0, 此时其他线程都执行过了if判断, 再对票数--, 就会将内存中的票数--到负数!

互斥锁

● 互斥锁的功能就是用来实现互斥,使得临界资源只能同时被一个执行流访问

● 尽量要给少的代码块加锁 (因为加锁之后,同一时间内只允许一个线程访问临界区资源,如果给大量代码加锁,多线程就没有意义了,效率可能大大降低)

● 一般都是给临界区加锁

● 使用锁的相关接口

定义全局互斥锁并初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

 解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

使用全局锁实现互斥访问临界资源

//定义全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//抢票逻辑
int ticket = 10000; // 全局的共享资源
void GetTicket(string name)
{
    while (true)
    {
        pthread_mutex_lock(&mutex); 
        if (ticket > 0)
        {
            // 充当抢票花费的时间
            usleep(1000);
            printf("%s get a ticket : %d\n", name.c_str(), ticket);
            ticket--; 
            pthread_mutex_unlock(&mutex); 
        }
        else
        {
            pthread_mutex_unlock(&mutex); 
            break;
        }
        // 实际情况, 还有后续的动作
    }
}

注意:

● 为了实现互斥访问临界资源,我们定义了一把全局锁,而全局锁也是全局变量,也是公共资源的,也得保证申请锁是安全的呀!!! 而申请锁本身是原子性操作,是安全的!

● 加锁是由程序员自己保证的,是一种规则,不遵守就是自己写的bug

● 根据互斥的定义, 任何时刻,只允许一个线程申请锁成功! 就注定了会有多个线程申请锁失败,失败的线程默认会在mutex锁上阻塞,阻塞的本质就是等待!

● 一个线程在临界区访问临界资源的时候, 是完全有可能发生线程切换的,但是切换走的线程依旧没有释放锁,可以理解成把锁带走了,其他线程依旧访问不了临界资源

● 加锁的情况下,if里面的代码块也表现出"原子性", 因为这段代码要么不执行,要么执行完,别的线程才能访问

定义局部互斥锁

初始化局部锁(第二个参数可以设置锁属性,传nullptr即可)

int pthread_mutex_init(pthread_mutex_t *restrict mp,
          const pthread_mutexattr_t *restrict mattr);

释放局部锁

int pthread_mutex_destroy(pthread_mutex_t *mutex)

加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

 使用局部锁实现互斥访问临界资源

#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"

string GetThreadName()
{
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread-%d", number++);
    return name;
}

int ticket = 10000; // 全局的共享资源

void GetTicket(pthread_mutex_t* mutex)
{
    while (true)
    {   
        pthread_mutex_lock(mutex); 
        if (ticket > 0)
        {
            // 充当抢票花费的时间
            usleep(1000);
            printf("get a ticket : %d\n", ticket);
            ticket--;
            pthread_mutex_unlock(mutex);
        }
        else
        {
            pthread_mutex_unlock(mutex);
            break;
        }
        // 实际情况, 还有后续的动作
    }
}

int main()
{
    pthread_mutex_t mutex; //定义局部锁
    pthread_mutex_init(&mutex, nullptr); //初始化局部锁

    string name1 = GetThreadName();
    Thread<pthread_mutex_t*> t1(name1, GetTicket, &mutex);

    string name2 = GetThreadName();
    Thread<pthread_mutex_t*> t2(name2, GetTicket, &mutex);

    string name3 = GetThreadName();
    Thread<pthread_mutex_t*> t3(name3, GetTicket, &mutex);

    t1.start();
    t2.start();
    t3.start();

    t1.join();
    t2.join();
    t3.join();

    pthread_mutex_destroy(&mutex); //释放局部锁
    return 0;
}

自己实现锁的封装

LockGuard.hpp

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

//不定义锁, 默认外部会传入锁对象
class Mutex
{
public:
    Mutex(pthread_mutex_t* lock):_lock(lock)
    {}

    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* lock):_mutex(lock)
    {
        _mutex.Lock();
    }

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

 main.cc

#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"
#include "LockGuard.hpp"

string GetThreadName()
{
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread-%d", number++);
    return name;
}

int ticket = 10000;                                // 全局的共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 全局锁对象

void GetTicket(string name)
{
    while (true)
    {
        LockGuard lockguard(&mutex);
        {
            if (ticket > 0)
            {
                // 充当抢票花费的时间
                usleep(1000);
                printf("get a ticket : %d\n", ticket);
                ticket--;
            }
            else
            {
                break;
            }
        }
        // 实际情况, 还有后续的动作
    }
}

int main()
{
    string name1 = GetThreadName();
    Thread<string> t1(name1, GetTicket, name1);
    string name2 = GetThreadName();
    Thread<string> t2(name2, GetTicket, name2);
    string name3 = GetThreadName();
    Thread<string> t3(name3, GetTicket, name3);

    t1.start();
    t2.start();
    t3.start();

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

加锁实现互斥的原理

● 大多数体系结构都提供了swap和exchange指令, 作用是把寄存器内容和内存单元的数据进行交换

● 而锁本质是一个结构体,可以简单认为内部有一个变量 int mutex = 1

● A线程加锁时,先将寄存器%al内容置成0,然后交换寄存器%al的内容和内存变量mutex内容,于是%al的内容变为了1, mutex变为了0

此时就算线程A被切走了,会把上下文数据包括%al的内容带走,线程B开始调度运行, 内存内容已经是0了,因此尽管线程B交换寄存器%al的值和内存中的mutex的值,交换完之后还是0,此时进入else分支,挂起等待。因此可以认为,线程切换时把锁带走了!

● 所以交换本质是将一个共享的mutex资源,交换到自己的上下文中,属于线程自己了!

● 解锁就是直接将mutex置为1,其他线程申请锁时就正常执行上述汇编语句即可!

● 加锁和解锁的一般规则: 谁加锁,谁解锁

死锁

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

死锁的四个必要条件

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

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

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

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

避免死锁:

1. 破坏死锁的四个必要条件

2. 加锁顺序一致

3. 避免锁未释放的场景

4. 资源一次性分配

避免死锁算法

1.死锁检测算法

2.银行家算法

线程同步

● 同步本质是让多执行流访问临界资源具有一定的顺序

多线程访问公共资源可能会出现问题,因此我们使用互斥锁保证任何一个时刻只有1个线程访问公共资源,但有些线程竞争锁的能力很强,每次竞争时都会拿到锁,导致其他线程长时间访问不了公共资源,也就是会导致线程饥饿问题

● 互斥能保证访问资源的安全性,但只有安全是不够的,同步能够较为充分高效的使用资源

● 条件变量本质就是实现线程同步的一种机制,是pthread库提供的一个线程向另一个线程通知信息的方式

● 举个例子理解条件变量

一张桌子,两个人,一个人放苹果,另一个蒙着眼睛的人拿苹果,放苹果的时候不能拿,拿的时候不能放,因此要加锁实现互斥!而拿苹果的人不知道放苹果的人什么时候放,因此拿苹果的人不断的申请锁,检测,释放锁,导致了放苹果人的饥饿问题!

于是放了一个铃铛,让放苹果的人放苹果之后,敲一下铃铛,此时拿苹果的人再去拿苹果!

上述的铃铛本质就是条件变量,可以理解成以下结构体:

struct cond
{
     int flag; //条件是否就绪
     tcb_queue; //条件不满足,就排队!
}

● 上述例子中只有1个拿苹果的人,实际可以有多个拿苹果的人,可以认为所有拿苹果的人都要排队,拿完苹果之后再去队尾重新排队,这就使得多执行流可以较为均衡地按照一定顺序访问资源

● 条件变量使用接口

在cond条件变量下进行等待

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

唤醒条件变量下等待的一个线程

int pthread_cond_signal(pthread_cond_t *cond);

唤醒条件变量下等待的所有线程

int pthread_cond_broadcast(pthread_cond_t *cond);

● 条件变量使用示例

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;    // 定义条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁

void *threadRoutine(void *args)
{
    const char* threadname = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex); //在指定的条件变量下等待
        cout << "I am a new thread: " << threadname << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, threadRoutine, (void*)"thread-1");
    pthread_create(&t2, nullptr, threadRoutine, (void*)"thread-2");
    pthread_create(&t3, nullptr, threadRoutine, (void*)"thread-3");
    sleep(5); // 5s之后唤醒线程
    
    while(true)
    {
        pthread_cond_signal(&cond); //每次唤醒一个线程
        sleep(1);
    }
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
}

说明:

● cout是往显示器上打印,多个线程都执行cout语句,访问显示器资源,此时显示器资源也是公共资源,打印会出现混乱的情况,因此显示器资源也需要被保护起来,因此在cout语句前后加锁解锁

● 只加锁,发现线程1、2、3打印没有任何的顺序性,且一个线程一打印就是一批语句,这就是竞争锁的能力不同而导致的

● 为了让线程1、2、3打印具有一定的顺序性,我们引入了条件变量,在加锁和解锁之间使用pthread_cond_wait 接口让线程在条件变量下等待,在主线程中,每隔1s唤醒一个线程,从而使得打印结果具有明显的顺序性

● 如果在主线程中,每隔1s唤醒所有线程,那么所有线程都会去参与锁的竞争,因此最后打印的顺序依旧不确定

抢票代码加入线程同步:

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;    // 定义条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁

int tickets = 5000;

void *threadRoutine(void *args)
{
    string threadname = static_cast<const char *>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000); // 充当抢票花费的时间
            cout << threadname << ": get a ticket, ticket : " << tickets << endl;
            tickets--;
        }
        else
        {
            cout << threadname << ", 没有票了" << endl;
            pthread_cond_wait(&cond, &mutex); // 没有票了, 就去条件变量下等待
        }
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, threadRoutine, (void *)"thread-1");
    pthread_create(&t2, nullptr, threadRoutine, (void *)"thread-2");
    pthread_create(&t3, nullptr, threadRoutine, (void *)"thread-3");

    sleep(5); // 5s之后唤醒线程

    while (true)
    {
        sleep(6);
        pthread_mutex_lock(&mutex);
        tickets += 1000; // 每隔6s, 就再放1000张票
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond); // 唤醒一个线程
        // pthread_cond_broadcast(&cond); // 唤醒所有线程
    }

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    return 0;
}

说明:

● 加锁和解锁之间,往往要访问临界资源,但是临界资源不一定是满足条件的,所以我们要判断,条件不满足,就应该让线程去条件变量下等待

● 线程在条件变量下等待的时候, 会自动释放锁

● 线程被唤醒的时候,是在临界区内被唤醒的,当线程被唤醒,在wait函数返回时,要重新申请并持有锁,才能真正被唤醒,这也就是 pthread_cond_wait 的参数中同时有条件变量和锁的原因,而重新申请并持有锁也是要参与锁的竞争的!

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

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

相关文章

shell编程(完结)

shell编程&#xff08;完结&#xff09; 声明&#xff01; 学习视频来自B站up主 ​泷羽sec​​ 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章 笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其…

ctfshow-web 151-170-文件上传

151. 我们首先想到就是上传一句话木马。但是看源代码限制了png。 &#xff08;1&#xff09;改前端代码。 这里是前端限制了上传文件类型&#xff0c;那我们就改一下就好了嘛,改成php。 这里直接修改不行&#xff0c;给大家推荐一篇简短文章&#xff0c;大家就会了&#xff08…

Docker的初识

目录 1. 容器技术发展史1.1 Jail 时代1.2 云时代1.3 云原生时代1.3.1 Google & Docker 竞争1.3.2 k8s 成为云原生事实标准 2. 虚拟化和容器化的概念2.1 什么是虚拟化、容器化2.2 为什么要虚拟化、容器化&#xff1f;2.3 虚拟化实现方式2.3.1 应用程序执行环境分层2.3.2 虚拟…

Jenkins流水线初体验(六)

DevOps之安装和配置 Jenkins (一) DevOps 之 CI/CD入门操作 (二) Sonar Qube介绍和安装(三) Harbor镜像仓库介绍&安装 (四) Jenkins容器使用宿主机Docker(五) Jenkins流水线初体验(六) 一、Jenkins流水线任务介绍 之前采用Jenkins的自由风格构建的项目,每个步骤…

链式设计模式——装饰模式和职责链模式

一、装饰模式 1、概述 动态地给一个对象添加一些额外的职责&#xff0c;就增加功能来说&#xff0c;装饰模式比生成子类更为灵活。 ConcreteComponent &#xff1a;是定义了一个具体的对象&#xff0c;可以给这个对象添加一些职责&#xff1b;Decorator &#xff1a;装饰抽象…

JavaEE多线程案例之阻塞队列

上文我们了解了多线程案例中的单例模式&#xff0c;此文我们来探讨多线程案例之阻塞队列吧 1. 阻塞队列是什么&#xff1f; 阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则. 阻塞队列是⼀种线程安全的数据结构,并且具有以下特性: 当队列满的时候,继续⼊队列就会…

【Linux】VMware 安装 Ubuntu18.04.2

ISO镜像安装步骤 选择语言 English 选择键盘布局 English 选择系统 Ubuntu 虚拟机网卡地址&#xff0c;默认即可 代理地址&#xff0c;默认空即可 镜像地址&#xff0c;修改成阿里云地址 选择第二项&#xff0c;LVM 磁盘扩容技术 第一块硬盘名sda&#xff0c;默认…

Unity游戏实战

很小的时候在键盘机上玩过一个游戏叫寻秦&#xff0c;最近看有大佬把他的安卓版做出来了&#xff0c;打开封面就是Unity&#xff0c;想自己也尝试一下。

R语言的数据结构-向量

【图书推荐】《R语言医学数据分析实践》-CSDN博客 《R语言医学数据分析实践 李丹 宋立桓 蔡伟祺 清华大学出版社9787302673484》【摘要 书评 试读】- 京东图书 (jd.com) R语言编程_夏天又到了的博客-CSDN博客 在R语言中&#xff0c;数据结构是非常关键的部分&#xff0c;它提…

CTF misc 流量分析特训

以下题目来源于西电的靶场&#xff0c;从NewStar CTF开始 wireshark_checkin 进来看一下http流&#xff0c;结果真的找到flag了&#xff08;感觉有点狗运&#xff09;&#xff0c;第一道流量分析题就这么奇奇妙妙的解出来了 wireshark_secret 根据提示猜测flag可能在图片里&…

React v19稳定版发布12.5

&#x1f916; 作者简介&#xff1a;水煮白菜王 &#xff08;juejin/csdn同名&#xff09;&#xff0c;一位资深前端劝退师 &#x1f47b; &#x1f440; 文章专栏&#xff1a; 前端专栏 &#xff0c;记录一下平时在博客写作中&#xff0c;总结出的一些开发技巧✍。 感谢支持&a…

【JVM】JVM基础教程(三)

上一章&#xff1a;【JVM】JVM基础教程&#xff08;二&#xff09;-CSDN博客 目录 运行时数据区 应用场景 程序计数器 程序计数器在运行时会出现内存溢出吗&#xff1f; 栈 IDEA的debug工具查看栈帧的内容 栈帧的组成 局部变量表 关于 this 的内存存储 操作数栈 帧…

Postman Sandbox 项目教程

Postman Sandbox 项目教程 postman-sandbox Sandbox for Postman Scripts to run in Node.js or browser 项目地址: https://gitcode.com/gh_mirrors/po/postman-sandbox 1. 项目介绍 Postman Sandbox 是一个用于在 Node.js 或浏览器中执行 Postman 脚本的沙盒环境。它…

Maven、mybatis框架

一、Maven介绍 1.概念&#xff1a; Maven项目对象模型(POM)&#xff0c;可以通过一小段描述信息来管理项目的构建&#xff0c;报告和文档的项目管理工具软件。 2.为啥使用maven: 之前项目中需要引入大量的jar包。这些jar从网上下载&#xff0c;可能下载地址不同意。这些jar之间…

Python连接和操作Elasticsearch详细指南

Python连接和操作Elasticsearch详细指南 一、服务器端配置1. 修改 Elasticsearch 配置文件2. 开放防火墙端口 二、本地 Python 连接 Elasticsearch1. 连接 Elasticsearch2. 索引操作3. 文档操作4. 搜索内容5. 聚合查询6. 批量操作 三、注意事项四、故障排除结论 Elasticsearch …

获得日志记录之外的新视角:应用程序性能监控简介(APM)

作者&#xff1a;来自 Elastic David Hope 日志记录领域即将发生改变。在这篇文章中&#xff0c;我们将概述从单纯的日志记录到包含日志、跟踪和 APM 的完全集成解决方案的推荐流程。 通过 APM 和跟踪优先考虑客户体验 企业软件开发和运营已成为一个有趣的领域。我们拥有一些非…

Python - 面向对象;类和对象;方法属性;init,self;魔法方法;析构方法;函数方法区别(六)

一、面向对象编程&#xff08;OOP&#xff09; 定义 面向过程(Procedure Oriented Programming, POP)是一种程序设计范式&#xff0c;主要关注的是实现功能的步骤&#xff0c;设计时模块化和流程化。面向过程编程是一种以过程为中心的编程方式&#xff0c;它将问题分解成一系…

源码编译安装MySQL

MySQL相应版本的tar包下载 在5.7的版本的MySQL编译安装的时候&#xff0c;需要依赖C语言的库文件【boost】&#xff0c; 如上图所示&#xff0c;如果你使用第一个MySQL的tar包&#xff0c;还需要去网上去下载boost即C语言的库文件&#xff0c;但是第二个tar包就既包含MySQL的源…

关于Kubernetes(K8S)认证含金量?

Kubernetes越来越流行&#xff0c;目前它是市场上最佳的容器编排工具之一&#xff0c;也是运维工程师必备的技能之一。 大厂都在用K8S&#xff08;就业行情&#xff09; 虽说今年的大环境不是很好&#xff0c;但是从招聘数据来看&#xff0c;K8S岗位薪资不降反而上涨不…

Linux / Windows | ping IP + Port 测试

注&#xff1a;本文为 “Linux / Windows | ping IP Port 测试端口通畅” 相关文章合辑。 未整理去重。 windows 如何确认服务器上程序端口是否正常&#xff08;ping、tcping&#xff09; 三希已于 2023-05-22 18:08:06 修改 方式 1&#xff1a;ping 命令 ping 命令说明 p…