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

【Linux】线程ID、线程管理、与线程互斥

📚 博主的专栏

🐧 Linux   |   🖥️ C++   |   📊 数据结构  💡C++ 算法 | 🌐 C 语言

上篇文章: 【Linux】线程:从原理到实战,全面掌握多线程编程!-CSDN博客

下篇文章:   线程同步、条件变量、生产者消费者模型

目录

线程ID

线程ID是是虚拟地址

库内部承担对线程的管理

库如何管理线程的:先描述再组织。

Linux线程 = pthread库中的线程的属性集 +内核LWP(比率1:1)

clone创建的执行流默认是和主进程共享地址空间。

__thread 让每个线程各自私一份的同一个名称的变量

简单封装线程

准备三个文件:

实现线程控制的几个方法

Start()

Stop()与Join()

简单的线程封装成品:

创建一批线程:对一批线程进行管理

线程互斥

抢票程序:

可重入VS线程安全

常见锁概念

死锁

死锁四个必要条件

1. 什么是互斥锁?

2. 互斥锁的核心接口(C语言,pthread库)

1. 初始化互斥锁

(1) 静态初始化

(2) 动态初始化

2. 加锁与解锁

(1) 阻塞加锁

(2) 非阻塞加锁

(3) 解锁

3. 销毁互斥锁

3.所谓的对临界资源进行保护,本质是对临界区代码进行保护

注意:

修改线程封装:

从原理角度理解这个锁:

从实现角度理解锁:

加锁(lock)逻辑

解锁(unlock)逻辑


线程ID

线程ID是是虚拟地址

示例代码:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>void *threadrun(void *args)
{std::string name = static_cast<const char *>(args); // 使用static_cast强转类型,得到线程的名字while (true){ // 任何一个线程可以通过pthread_self获取自己的线程idstd::cout << name << " " << "is running, tid: " << pthread_self() << std::endl;sleep(1);}
}int main()
{pthread_t tid;// 1.创建一个线程,1.取地址线程id 2.线程属性设为nullptr 3.线程要执行的函数 4.线程的名字(参数强转为void*)pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");std::cout << "new thread tid: " << tid << std::endl;// 2.线程一旦创建就需要join它pthread_join(tid, nullptr);return 0;
}

给用户提供的线程的id,不是内核中的LWP(轻量级进程),而是自己(pthread库)维护的一个值。

库内部承担对线程的管理

将id值转成16进制打印出:

std::string ToHex(pthread_t tid)
{char id[128];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}void *threadrun(void *args)
{std::string name = static_cast<const char *>(args); // 使用static_cast强转类型,得到线程的名字while (true){ // 任何一个线程可以通过pthread_self获取自己的线程idstd::string id = ToHex(pthread_self());std::cout << name << " " << "is running, tid: " << id << std::endl;sleep(1);}
}int main()
{pthread_t tid;// 1.创建一个线程,1.取地址线程id 2.线程属性设为nullptr 3.线程要执行的函数 4.线程的名字(参数强转为void*)pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");std::cout << "new thread tid: " << ToHex(tid) << std::endl;// 2.线程一旦创建就需要join它pthread_join(tid, nullptr);return 0;
}

线程id实际上是一个地址。

动态库在没有被加载的时候在哪里?在磁盘中。库是什么?文件

pthread库本身是一个文件libpthread.so,而我写的文件mythread也是一个文件在磁盘当中

多线程在启动之前,也必须先是一个进程,再动态的创建线程。

而创建线程,前提是把库加载到内存,映射到我进程的地址空间。

当线程动态库已经加载到内存,库中有pthread_create()方法,线程还不能够执行这个方法。需要先将库映射到堆栈之间的共享区当中,在共享区中构建了对应的动态库的起始地址经过页表再映射到内存的整个库中,建立好了映射,未来想要访问任意函数地址就通过页表映射找到库中方法,比如创建线程,就通过页表的映射,找到了内存库中创建线程的函数地址,在库中将线程创建好。

库如何管理线程的:先描述再组织。

在虚拟地址空间中:在动态库里,创建一个线程的时候,库会创建一个对应的线程控制块tcb在内存当中,线程控制块当中有struct pthread用于描述该线程的相关结构体字段属性、以及线程栈(每一个新线程都有自己独立的栈空间)。pthread_t tid就是每个线程控制块的起始地址。因此只要有tid,就能访问到这个线程的所有属性。线程的所有属性在库里被维护(不代表在库里开空间,空间还可以在堆上开,管理字段放库里)

这里可以联想到之前讲到的知识点,在文件管理的时候,C语言的额FILE*(是C标准库申请的),打开一个文件(fopen函数)会返回FILE*,那么FILE在哪里,在C标准库里面。用地址访问文件对象。

创建一个线程过后,在没有pthread_join的时候,执行流已经关闭,线程已经退出return/pthread_exit(num),此时会将num放置struct pthread属性当中,线程底层的内核LWP直接释放,该线程的属性一直被维护在库里,直到join通过线程id也就是线程控制块的地址,拿到线程退出的信息num。这就是为什么,不join会导致类似于僵尸进程的问题,因为在库中还保留该线程的信息,没有被释放。

struct tcb内部包含数据块:struct pthread other、char stack[N]等。每一个新线程栈实际上也是和主线程的栈一样存在虚拟地址空间内部的,是用户级空间,因此可以随我们访问。

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

Linux线程 = pthread库中的线程的属性集 +内核LWP(比率1:1)

如何保证一个新线程在执行程序时产生的临时变量存在自己的栈空间内

Linux有LWP的系统调用

int clone(int (*fn)(void *), void *stack, int flags, void *arg, ... /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

创建轻量级进程:clone(也是创建子进程fork()函数的底层实现)

让创建的lwp去执行我设置的回调函数,所形成的临时变量放置于我所指明的新线程栈空间内。 

fn 

  • 子进程/线程启动函数(类似线程入口函数)

  • 返回值为子进程的退出状态码

st ack

  • 必须提供子进程独立的栈空间地址

  • 示例:char stack[STACK_SIZE] = {0};

flags (关键控制位)

标志位作用
CLONE_VM共享虚拟内存空间(实现线程核心特性)
CLONE_FS共享文件系统属性(根目录、umask等)
CLONE_FILES共享文件描述符表
CLONE_SIGHAND共享信号处理函数表
CLONE_THREAD将新进程放入父进程的线程组
CLONE_SYSVSEM共享System V信号量

 arg

  • 传递给fn函数的参数

clone创建的执行流默认是和主进程共享地址空间。

因为,主线程和新线程共享地址空间,全局变量(存在全局区)对于多线程来说是是被共享的,因此主线程和新线程都能访问到同一个全局变量。

示例:证明clone创建的默认执行流是和主进程共享地址空间的

#include <iostream>
#include <string>
#include<stdio.h>
#include <unistd.h>
#include <pthread.h>int gval = 100;//全局变量
std::string ToHex(pthread_t tid)
{char id[128];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}void *threadrun(void *args)
{std::string name = static_cast<const char *>(args); // 使用static_cast强转类型,得到线程的名字while (true){ // 任何一个线程可以通过pthread_self获取自己的线程idstd::string id = ToHex(pthread_self());std::cout << name << " " << "is running, tid: " << id <<",gval:"<< gval << ",&gval:" << &gval << std::endl;sleep(1);gval++;}
}int main()
{pthread_t tid;// 1.创建一个线程,1.取地址线程id 2.线程属性设为nullptr 3.线程要执行的函数 4.线程的名字(参数强转为void*)pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");while(true){   //主线程不对gval做修改std::cout << "main thread, gval:" << gval << ",&gval:" << &gval << std::endl;sleep(1);}std::cout << "new thread tid: " << ToHex(tid) << std::endl;// 2.线程一旦创建就需要join它pthread_join(tid, nullptr);return 0;
}

主线程,新线程访问到同一个全局变量 

__thread 让每个线程各自私一份的同一个名称的变量

Linux适用,只支持内置类型

简单封装线程

准备三个文件:

Thread.hpp

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>namespace ThreadModle
{// 线程要执行的方法typedef void (*func_t)(const std::string &name); // 函数指针类型class Thread{public:Thread(){}~Thread(){}void Start(){}void Stop(){}void Join(){}private:std::string _name; // 线程名pthread_t _tid;    // 线程所对应的idbool _isrunning;   // 线程此时是否正在运行func_t _func; // 线程要执行的回调函数};
}

Main.cc

#include<iostream>
#include"Thread.hpp"int main()
{Thread<int> t;//类模版创建一个线程对象t.Start();//启动线程的方法t.Stop();//停止线程的方法t.join();//回收线程return 0;
}

Makefile

testthread:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f testthread

实现线程控制的几个方法

Start()

ThreadRoutine是类内的成员方法,这就意味着,这个方法默认带了当前对象的this指针(类型是Thread*),而创建线程的函数要求所传的该参数必须是一个返回值是void*,参数只有一个void*的函数指针。因此在类当中想要创建线程执行类的成员方法,是不可能的。

解决办法:加static,static定义的类成员函数是没有this指针的,这个成员函数就属于类,不属于对象了。 

带来的问题:不能在ThreadRoutine再调用_func(),因为_func是私有的类内部成员属性

解决办法:在pthread_create函数传参数时将当前对象传进ThreadRoutine,再强转args成对应的对象。再写一个成员函数Excute()用于调用回调函数,在ThreadRoutine直接用当前对象调用这个成员函数,就相当于调用回调函数,增加代码可读性,Excute()还能用来判断我的线程是否已经开始running:_isrunning = true

namespace ThreadModle
{// 线程要执行的方法typedef void (*func_t)(const std::string &name); // 函数指针类型class Thread{public:void Excute() //调用回调函数的成员方法{_isrunning = true;_func(_name);}public:Thread(){}~Thread(){}static void *ThreadRoutine(void* args)//创建的新线程都会执行这个方法{//执行回调方法,每一个线程都能执行一系列的方法Thread* self = static_cast<Thread*>(args);//获得了当前对象self->Excute();}bool Start(){                       //创建线程成功就返回0int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);// ThreadRoutine线程的固定历程if(n != 0) return false;return true;}void Stop(){}void Join(){}private:std::string _name; // 线程名pthread_t _tid;    // 线程所对应的idbool _isrunning;   // 线程此时是否正在运行func_t _func; // 线程要执行的回调函数};}
Stop()与Join()

首先,如果线程已经启动了才能stop,因此要先判断线程是否已经启动。如果线程已经结束了才join,因此要先判断线程是否已经结束。

        void Stop(){if(_isrunning){pthread_cancel(_tid);//取消线程_isrunning = false; //设置状态为false线程停止}}void Join(){pthread_join(_tid, nullptr);}

想要得到线程返回结果,可以修改回调函数的返回值为我想要的类型(返回结果),

    typedef std::string (*func_t)(const std::string &name); // 函数指针类型

再在Thread类中封装一个成员属性result:

        std::string result;

以及从Excute当中调用_func()的时候获取返回结果:

        void Excute() //调用回调函数的成员方法{_isrunning = true;_result = _func(_name);}

简单的线程封装成品:

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>namespace ThreadModle
{// 线程要执行的方法typedef void (*func_t)(const std::string &name); // 函数指针类型class Thread{public:void Excute() // 调用回调函数的成员方法{std::cout << _name << ",is running" << std::endl;_isrunning = true;_func(_name);_isrunning = false;}public:Thread(const std::string &name, func_t func) : _name(name), _func(func){std::cout << "create " << name << " done" << std::endl;}~Thread(){// Stop();// Join();}static void *ThreadRoutine(void *args) // 创建的新线程都会执行这个方法{// 执行回调方法,每一个线程都能执行一系列的方法Thread *self = static_cast<Thread *>(args); // 获得了当前对象self->Excute();return nullptr;}bool Start(){                                                                  // 创建线程成功就返回0int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this); // ThreadRoutine线程的固定历程if (n != 0)return false;return true;}std::string Status(){if(_isrunning) return "running";else return "sleep";}void Stop(){if (_isrunning){pthread_cancel(_tid); // 取消线程_isrunning = false;   // 设置状态为false线程停止std::cout << _name << " Stop" << std::endl;}}void Join(){pthread_join(_tid, nullptr);std::cout << _name << " Joined" << std::endl;}private:std::string _name; // 线程名pthread_t _tid;    // 线程所对应的idbool _isrunning;   // 线程此时是否正在运行func_t _func; // 线程要执行的回调函数// std::string _result;};}

Main.cc

 void Print(const std::string &name){int cnt = 1;while (true){std::cout << name << "is running, cnt: " << cnt++ << std::endl;sleep(1);}}

运行结果:

 stop之后只剩一个线程:join后,都结束

 通过以上代码,能够感受线程在C++11里,实际上就是对原生线程的一种封装

创建一批线程:对一批线程进行管理

管理原生线程,先描述,再组织(这里用数组下标就管理了线程)对vector的增删查改

#include<iostream>
#include"Thread.hpp"
#include<vector>
#include<unistd.h>
using namespace ThreadModle; const int gnum = 10;void Print(const std::string &name)
{int cnt = 1;while (true){std::cout << name << ",is running, cnt: " << cnt++ << std::endl;sleep(1);}
}
int main(){// 我在管理原生线程, 先描述,在组织// 构建线程对象std::vector<Thread> threads;for (int i = 0; i < gnum; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(name, Print);sleep(1);}// 统一启动for (auto &thread : threads){thread.Start();}sleep(10);// 统一结束for (auto &thread : threads){thread.Stop();}// 等待线程等待for (auto &thread : threads){thread.Join();}return 0;}

运行结果: 线程统一启动,10s后,线程集体结束,再集体被Joined

线程互斥

线程能够看到的资源--共享资源

往往我们需要对这个共享资源进行保护

进程线程间的互斥相关背景概念

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题。

抢票程序:

可重入VS线程安全

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

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

常见锁概念

死锁

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

死锁四个必要条件

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

请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

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

避免死锁算法:死锁检测算法(了解)银行家算法(了解)

以下所写的抢票系统,哪个线程先抢,哪个线程后抢,是不确定的,整个线程的调度以及运行过程,完全是由调度器决定的。

#include<iostream>
#include"Thread.hpp"
#include<vector>
#include<unistd.h>
using namespace ThreadModle; int tickets = 10000;void route(const std::string &name)
{while(true){if(tickets > 0) //只有票数大于0的时候才需要抢票{// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;}else{break;}}
}int main()
{Thread t1("thread-1", route);Thread t2("thread-2", route);Thread t3("thread-3", route);Thread t4("thread-4", route);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();
}

票数是10000张,因此不能让票数减到负数,多线程在并发访问共享资源时,错误或异常的问题:不仅抢到的负票,还抢到了同一张负票。

1.为什么会抢到负数

判断的过程是否是计算?是 ---> 计算的结果是真假,是一种逻辑运算(计算机的运算类型还有算数运算)计算由CPU(应用程序被CPU调度)来做,tickets有空间和内容,刚开始存放在内存当中的一个变量。当需要做这个逻辑运算的时候,第一步,把数据(tickets的)从内存移动到CPU中的寄存器(eax),还需要有一个寄存器将符号的另一端的值(0,立即数)放进另一个寄存器,CPU对两个寄存器中的值做逻辑判断,是真为1,是假为0。得到结果后,CPU再控制执行流是执行if还是else。

在执行判断的时候,会有我们设定的多个线程进入函数里进行抢票,每一个线程都要执行对应的判断逻辑。CPU一般一直在执行某个线程代码。CPU中的寄存器只有一套,而寄存器中的数据可有多套,每套数据属于线程私有,当线程备切换的时候,线程会带走自己的数据,线程回来的时候,会恢复寄存器中自己的一套数值。

假如现在是四个线程,并且只剩一张票了,线程a现在将tickets放进一个寄存器里,0也放进另一个寄存器,线程a正在被调度且已经判断tickets = 1 > 0得到值是1,正准备执行抢票(printf)的时候(还未对tickets进行 --),被切换了,此时线程a会保存自己的上下文数据,以及判断的结果res  = 1,线程b被叫醒进来抢票,此时的tickets仍然是1,逻辑判断后,res = 1,此时(还未抢票和--tickets)有可能线程b也被切换。线程c、d来到,他们都执行如上操作,票数只有1张,但是却进来了四个线程,线程a此时被唤醒,抢票后票数--,b进来,再--,tickets早已被-为负数。

tickets:1.重读数据,2.--数据,3.写回数据

总结:

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  • --ticket 操作本身就不是一个原子操作

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

1. 什么是互斥锁?

  • 定义:互斥锁(Mutual Exclusion Lock)是一种同步机制,用于在多线程编程中保护共享资源,确保同一时间只有一个线程可以访问临界区(Critical Section)。

  • 核心作用:防止多个线程同时修改共享数据,避免数据竞争(Race Condition)导致的不一致性。

2. 互斥锁的核心接口(C语言,pthread库)

在 Linux 中,互斥锁通过 pthread 库实现。以下是主要接口函数:

1. 初始化互斥锁

(1) 静态初始化
  • 适用于全局或静态互斥锁。

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    • 特性:快速初始化,无需手动销毁pthread_mutex_destroy

(2) 动态初始化
  • 可自定义互斥锁属性(如设置递归锁)。

    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);  // 第二个参数为属性,NULL 表示默认
    • 必须销毁:使用后需调用 pthread_mutex_destroy

操作成功都是返回1,操作失败,返回-1

2. 加锁与解锁

(1) 阻塞加锁
  • 若锁已被占用,当前线程会阻塞,直到锁被释放。

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    • 返回值:成功返回 0,失败返回错误码(如未初始化的锁返回 EINVAL)。

(2) 非阻塞加锁
  • 尝试加锁,若锁被占用,立即返回错误码 EBUSY

    int pthread_mutex_trylock(pthread_mutex_t *mutex);
(3) 解锁
  • 释放锁,允许其他线程获取。

    int pthread_mutex_unlock(pthread_mutex_t *mutex);

3. 销毁互斥锁

释放动态初始化的锁资源。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

在最前面的抢票系统:

3.所谓的对临界资源进行保护,本质是对临界区代码进行保护

全局的 tickets叫做共享资源,多线程未来都会执行同route,在线程执行的代码之中,tickets临界资源会被我们加以保护,这种被保护的全局资源就叫做临界资源,在多线程所执行的代码中一定会存在访问临界资源的代码,访问临界资源的代码就叫做临界区,其他代码叫做非临界区。

我们对所有资源进行访问,本质都是通过代码进行访问,因此要保护资源本质就是想办法把访问资源的代码进行保护。

在临界区的代码只能串行执行

现在我们在刚才的抢票系统中添加锁(加锁和解锁):

注意:

1.加锁的范围和粒度(临界区包含的代码的长度)一定要尽量小。

串行周期如果长,会导致整个系统的效率降低。

因此不将锁加在循环外面(这会导致一个线程将所有的票都抢完,下一个线程才能进来,这是不合理的,不符合我们期待的多线程并发需求),解锁也不能只加在break之后,会导致其他线程不能进来。而一个线程抢完票之后应该立马让另外的线程进来抢票,因此tickets--之后也要解锁:

pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;void route(const std::string &name)
{while(true){pthread_mutex_lock(&gmutex);//加锁if(tickets > 0) //只有票数大于0的时候才需要抢票{// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;pthread_mutex_unlock(&gmutex);}else{pthread_mutex_unlock(&gmutex);break;}}
}

2.任何线程,要进行抢票,都得先申请锁,原则上,不应该有例外

3.所有的线程申请锁,前提是所有的线程都能看到这把锁,所锁本身也是共享资源,因此加锁的过程,必须是原子的。

4.原子性,要么不做,要做就做完,没有中间状态

5.如果线程申请锁失败了,线程被阻塞

6.如果线程申请成功了,就会继续向后运行

7.如果线程申请锁成功了,就开始执行临界区代码了,在执行临界区代码的期间,可以被切换吗,是可以被切换的(对于CPU来说加锁解锁也不过就是像运算一样的代码),但是其他线程无法进入。假如我现在线程1正在执行,被切换走了,其他2,3,4线程能进来吗,不能进来,因为我虽然被切换了,但是我没有释放锁。一个线程在持有锁的状态下,可以放心的执行完临界区代码,几遍被切换,其他线程无法进来,在我回来时又继续执行代码。

结论:所以对于其他线程,要么我没有申请锁,要么我释放了锁,对其他线程才有意义!这就相当于,我访问临界区,对其他线程就是原子的(要么是我解锁了,要么就是我没解锁 ,我在中间发生任何事都对他们无关)。

修改线程封装:

封装一个线程数据(包括线程的名字以及线程的锁(锁传地址)),未来想要创建一个线程,一方面在创建线程的时候传递线程名,传递一个回调方法,再传递一个线程参数(线程数据也就是)。

  class ThreadData{public://未来给线程传递的数据类型ThreadData(const std::string &name, pthread_mutex_t *lock):_name(name), _lock(lock){}private:std::string name;pthread_mutex_t *lock; };// 线程要执行的方法typedef void (*func_t)(ThreadData *td); // 函数指针类型

创建多线程:

// 创建threadnum个线程
static int threadnum = 4;
int main()
{//创建的是局部锁pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);// 创建多线程std::vector<Thread> threads;for (int i = 0; i < threadnum; i++){std::string name = "thread-" + std::to_string(i + 1);ThreadData *td = new ThreadData(name, &mutex); //将线程名字与锁地址传递给ThreadDatathreads.emplace_back(name, route, td);//创建线程,将线程名字要执行的回调函数,以及回调函数的参数(线程参数)}//统一启动for (auto &thread : threads){thread.Start();}// 等待线程等待for (auto &thread : threads){thread.Join();}pthread_mutex_destroy(&mutex);
}

实际情况下最好写上private然后写Get函数:

route函数:

void route(ThreadData *td)
{//检验是否访问到的是同一把锁// std::cout << td->_name <<",mutex address: " << td->_lock << std::endl;// sleep(1);while (true){pthread_mutex_lock(td->_lock); // 加锁if (tickets > 0) // 只有票数大于0的时候才需要抢票{// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;pthread_mutex_unlock(td->_lock);}else{pthread_mutex_unlock(td->_lock);break;}}
}

对锁进行保护:

新建一个LockGuard.hpp文件:

#pragma once#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};

route代码:注意看其中的注释

void route(ThreadData *td)
{while (true)//是一个代码块{   //LockGuard是一个类型,定义出来的;临时对象,会调用他的构造函数,自动进行加锁,//while循环结束或者break结束,该对象临时变量被释放,析构函数被调用,解锁//RAII风格的锁LockGuard lockguard(td->_lock);//定义一个临时对象,对区域进行保护if (tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);tickets--;}else{break;}}
}

从原理角度理解这个锁:

pthread_mutex_lock(&mutex);

如何理解申请锁成功,允许你进入临界区

如何理解申请锁失败,不允许你进入临界区

允许我进入临界区的本质就是申请锁成功,pthread_mutex_lock()函数会返回

申请锁失败(锁没有就绪),pthread_mutex_lock()函数不返回,线程就是阻塞了。

pthread_mutex_lock()属于pthread库,线程也属于pthread库,所以在这个函数实现的时候,就是在做一个判断,申请锁是否成功,再设置线程的状态,然后把线程放在全局的队列当中。

一旦申请成功的线程把锁pthread_mutex_ulock(),对应的在队列当中的线程就会被唤醒,在重新pthread_mutex_lock()内部被重新唤醒,重新申请锁。

最典型的就是scanf(检测键盘是否输入数据,没输入数据就被阻塞)

从实现角度理解锁:

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

加锁(lock)逻辑

初始化
movb $0, %al 将 al 寄存器设为 0,为后续操作做准备。

原子交换
xchgb %al, mutex 通过原子操作交换 al 寄存器和内存中 mutex 的值。

  • 若 mutex 原值为 1(未锁定),交换后 mutex 变为 0(锁定),al 变为 1

  • 若 mutex 原值为 0(已锁定),交换后 mutex 仍为 0al 变为 0

条件判断

  •  al > 0:表示成功获取锁(原 mutex 为 1),返回 0(成功)

  • 否则:锁已被占用,线程挂起等待,之后通过 goto lock 重新尝试获取锁。

解锁(unlock)逻辑

  • 释放锁
    movb $1, mutex 将 mutex 设为 1,表示释放锁。

  • 唤醒线程
    注释提示“唤醒等待Mutex的线程”,表明释放锁时会通知其他等待线程继续竞争锁。

  • 返回成功
    返回 0(操作成功)

1.CPU的寄存器只有一套,被所有的线程共享,但是寄存器里面的数据,属于执行流的上下文,属于执行流私有的数据

2.CPU在执行代码的时候,一定要有对应的执行载体 线程&&进程

3.数据在内存中,被所有线程共享的。

结论:把数据从内存移动到寄存器,本质是把数据从共享,变成线程的私有

结语:

       随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

相关文章:

【Linux】线程ID、线程管理、与线程互斥

&#x1f4da; 博主的专栏 &#x1f427; Linux | &#x1f5a5;️ C | &#x1f4ca; 数据结构 | &#x1f4a1;C 算法 | &#x1f310; C 语言 上篇文章&#xff1a; 【Linux】线程&#xff1a;从原理到实战&#xff0c;全面掌握多线程编程&#xff01;-CSDN博客 下…...

服务器简介(含硬件外观接口介绍)

服务器&#xff08;Server&#xff09;是指提供资源、服务、数据或应用程序的计算机系统或设备。它通常比普通的个人计算机更强大、更可靠&#xff0c;能够长时间无间断运行&#xff0c;支持多个用户或客户端的请求。简单来说&#xff0c;服务器就是专门用来存储、管理和提供数…...

自动驾驶---决策规划之导航增强端到端

1 背景 自动驾驶算法通常包括几个子任务&#xff0c;包括3D物体检测、地图分割、运动预测、3D占用预测和规划。近年来&#xff0c;端到端方法将多个独立任务整合到多任务学习中&#xff0c;优化整个系统&#xff0c;包括中间表示&#xff0c;以实现最终的规划任务。随着端到端技…...

Datawhale AI春训营 世界科学智能大赛--合成生物赛道:蛋白质固有无序区域预测 小白经验总结

一、报名大赛 二、跑通baseline 在魔塔社区创建实例&#xff0c;根据教程完成速通第一个分数~ Datawhale-学用 AI,从此开始 三、优化实例&#xff08;这里是我的学习优化过程&#xff09; 1.先将官方给的的模型训练实例了解一遍&#xff08;敲一敲代码&#xff09; 训练模…...

基于Java(Struts2 + Hibernate + Spring)+MySQL实现的(Web)在线预约系统

基于Struts2 Hibernate Spring的在线预约系统 1.引言 1.1编写目的 针对医院在线预约挂号系统&#xff0c;提供详细的设计说明&#xff0c;包括系统的需求、功能模块、界面设计、设计方案等&#xff0c;以辅助开发人员顺利进行系统的开发并让项目相关者可以对这个系统进行分…...

PHP获取大文件行数

在PHP中获取大文件的行数时&#xff0c;直接读取整个文件到内存中可能会导致内存溢出&#xff0c;特别是对于非常大的文件。因此&#xff0c;最有效的方法是逐行读取文件并计数。以下是一些实现方法&#xff1a; 方法一&#xff1a;使用 fgets() fgets() 函数逐行读取文件&am…...

2024年网站开发语言选择指南:PHP/Java/Node.js/Python如何选型?

2024年网站开发语言选择指南&#xff1a;PHP/Java/Node.js/Python如何选型&#xff1f; 一、8大主流Web开发语言技术对比 1. PHP开发&#xff1a;中小型网站的首选方案 最新版本&#xff1a;PHP 8.3&#xff08;2023年11月发布&#xff09;核心优势&#xff1a; 全球78%的网站…...

Win7模拟器2025中文版:重温经典,掌上电脑体验

随着科技的快速发展&#xff0c;现代操作系统变得越来越高级&#xff0c;但许多用户仍然怀念经典的Windows 7系统。如果你也想重温那种熟悉的操作体验&#xff0c;Win7模拟器2025中文版 是一个不错的选择。这款软件能够让你在手机上轻松实现Windows 7系统的模拟&#xff0c;带来…...

HTML5+CSS3小实例:CSS立方体

实例:CSS立方体 技术栈:HTML+CSS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0&q…...

使用 Vite 快速搭建现代化 React 开发环境

1.检查环境 说明&#xff1a;检测环境&#xff0c;node版本为18.20.6。 2.创建命令 说明&#xff1a;创建命令&#xff0c;选择对应的选项。 npm create vitelatest 3.安装依赖 说明&#xff1a;安装相关依赖。 npm i...

Linux网络编程——基于ET模式下的Reactor

一、前言 上篇文章中我们已经讲解了多路转接剩下的两个接口&#xff1a;poll和epoll&#xff0c;并且知道了epoll的两种工作模式分别是 LT模式和ET模式&#xff0c;下来我们就实现的是一个简洁版的 Reactor&#xff0c;即半同步半异步I/O&#xff0c;在linux网络中&#xff0c…...

【现代深度学习技术】循环神经网络04:循环神经网络

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上&#xff0c;结合当代大数据和大算力的发展而发展出来的。深度学习最重…...

1. 认识DartGoogle为Flutter选择了Dart语言已经是既

1. 认识Dart Google为Flutter选择了Dart语言已经是既定的事实&#xff0c;无论你多么想用你熟悉的语言&#xff0c;比如JavaScript、TypeScript、ArkTS等来开发Flutter&#xff0c;至少目前都是不可以的。 Dart 是由谷歌开发的计算机编程语言&#xff0c;它可以被应用于 Web/…...

学习设计模式《三》——适配器模式

一、基础概念 适配器模式的本质是【转换匹配&#xff0c;复用功能】&#xff1b; 适配器模式定义&#xff1a;将一个类的接口转换为客户希望的另外一个接口&#xff1b;适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 适配器模式的目的&#xff1a;复用…...

【Java面试系列】Spring Boot微服务架构下的分布式事务处理与性能优化 - 2025-04-19详解 - 3-5年Java开发必备知识

【Java面试系列】Spring Boot微服务架构下的分布式事务处理与性能优化 - 2025-04-19详解 - 3-5年Java开发必备知识 引言 在微服务架构中&#xff0c;分布式事务处理和性能优化是面试中高频出现的主题。随着系统规模的扩大&#xff0c;如何保证数据一致性和系统性能成为开发者…...

Elasticsearch只返回指定的字段(用_source)

在Elasticsearch中&#xff0c;当你想要查询文档但不返回所有字段&#xff0c;只返回指定的字段&#xff08;比如这里的id字段&#xff09;&#xff0c;你可以使用_source参数来实现这一点。但是&#xff0c;有一点需要注意&#xff1a;Elasticsearch的_source字段默认是返回的…...

【Linux “sed“ 命令详解】

本章目录: 1. 命令简介sed 的优势&#xff1a; 2. 命令的基本语法和用法基本语法&#xff1a;参数说明&#xff1a;常见用法场景&#xff1a;示例1&#xff1a;替换文本示例2&#xff1a;删除空行示例3&#xff1a;从命令输出中处理内容 3. 命令的常用选项及参数常用命令动作&a…...

JMETER使用

接口测试流程: 1.获取接口文档&#xff0c;熟悉接口业务 2.编写接口测试用例以及评审 正例:输入正常的参数&#xff0c;验证接口能否正常返回 反例:权限异常(为空、错误、过期)、参数异常(为空、长度异常、类型异常)、其他异常(黑名单、调用次数限制)、兼容异常(一个接口被多种…...

JavaWeb 课堂笔记 —— 13 MySQL 事务

本系列为笔者学习JavaWeb的课堂笔记&#xff0c;视频资源为B站黑马程序员出品的《黑马程序员JavaWeb开发教程&#xff0c;实现javaweb企业开发全流程&#xff08;涵盖SpringMyBatisSpringMVCSpringBoot等&#xff09;》&#xff0c;章节分布参考视频教程&#xff0c;为同样学习…...

离线安装elasticdump并导入和导出数据

离线安装elasticdump 在 CentOS 或 RHEL 系统上安装 elasticdump&#xff0c;你可以使用 npm&#xff08;Node.js 的包管理器&#xff09;来安装&#xff0c;因为 elasticdump 是一个基于 Node.js 的工具。以下是步骤 先在外网环境下安装 下载nodejs和npm&#xff08;注意x8…...

WhatTheDuck:一个基于浏览器的CSV查询工具

今天给大家介绍一个不错的小工具&#xff1a;WhatTheDuck。它是一个免费开源的 Web 应用程序&#xff0c;允许用户上传 CSV 文件并针对其内容执行 SQL 查询分析。 WhatTheDuck 支持 SQL 代码自动完成以及语法高亮。 WhatTheDuck 将上传的数据存储为 DuckDB 内存表&#xff0c;继…...

关于数字信号与图像处理——基于Matlab的图像增强技术

本篇博客是在做数字信号与图像处理实验中的收获。 具体内容包括&#xff1a;根据给定的代码放入Matlab中分别进行两次运行测试——比较并观察运行后的实验结果与原图像的不同点——画出IJ的直方图&#xff0c;并比较二者差异。接下来会对每一步进行具体讲解。 题目&#xff1a…...

MySQL数据库 - 锁

锁 此笔记参考黑马教程&#xff0c;仅学习使用&#xff0c;如有侵权&#xff0c;联系必删 文章目录 锁1. 概述1.1 介绍1.2 分类 2. 全局锁2.1 介绍2.2 语法2.3 特点&#xff08;弊端&#xff09; 3. 表级锁3.1 介绍3.2 表锁3.3 元数据锁&#xff08;meta data lock&#xff0…...

免费多平台运行器,手机畅玩经典主机大作

软件介绍 飞鸟模拟器是一款面向安卓设备的免费游戏平台&#xff0c;支持PS2/PSP/NDS等十余种经典主机游戏运行。 该软件突破传统模拟器复杂操作模式&#xff0c;采用智能核心加载技术&#xff0c;用户只需双击主程序即可开启游戏之旅&#xff0c;真正实现"即下即玩"…...

计算机软考中级 知识点记忆——排序算法 冒泡排序-插入排序- 归并排序等 各种排序算法知识点整理

一、&#x1f4cc; 分类与比较 排序算法 最优时间复杂度 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 应用场景与特点 算法策略 冒泡排序 O(n) O(n) O(n) O(1) 稳定 简单易实现&#xff0c;适用于小规模数据排序。 交换排序策略 插入排序 O(n) O(n) O…...

STC32G12K128单片机GPIO模式SPI操作NorFlash并实现FatFS文件系统

STC32G12K128单片机GPIO模式SPI操作NorFlash并实现FatFS文件系统 Norflash简介NorFlash操作驱动代码文件系统测试代码 Norflash简介 NOR Flash是一种类型的非易失性存储器&#xff0c;它允许在不移除电源的情况下保留数据。NOR Flash的名字来源于其内部结构中使用的NOR逻辑门。…...

uniapp-x 二维码生成

支持X&#xff0c;二维码生成&#xff0c;支持微信小程序&#xff0c;android&#xff0c;ios&#xff0c;网页 - DCloud 插件市场 免费的单纯用爱发电的...

当HTTP遇到SQL注入:Java开发者的攻防实战手册

一、从HTTP请求到数据库查询:漏洞如何产生? 危险的参数拼接:Servlet中的经典错误 漏洞代码重现: public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String category = request.getParameter("…...

[dp20_完全背包] 介绍 | 零钱兑换

目录 1. 完全背包 题解 背包必须装满 2.零钱兑换 题解 1. 完全背包 链接&#xff1a; DP42 【模板】完全背包 描述 你有一个背包&#xff0c;最多能容纳的体积是V。 现在有n种物品&#xff0c;每种物品有任意多个&#xff0c;第i种物品的体积为vivi ,价值为wiwi。 &a…...

精打细算 - GPU 监控

精打细算 - GPU 监控 在上一篇,咱们历经千辛万苦,终于让应用程序在 Pod 的“驾驶舱”里成功地“点火”并用上了 GPU。太棒了!但是,车开起来是一回事,知道车速多少、油耗多少、引擎水温是否正常,则是另一回事,而且同样重要,对吧? 我们的 GPU 应用跑起来了,但新的问题…...

故障诊断 | CNN-BiGRU-Attention故障诊断

效果一览 摘要 在现代工业生产中,设备的稳定运行至关重要,故障诊断作为保障设备安全、高效运行的关键技术,其准确性和及时性直接影响着生产效率与成本[[doc_refer_1]][[doc_refer_2]]。随着工业设备复杂性的不断增加,传统故障诊断方法已难以满足实际需求。深度学习技术凭借…...

单片机AIN0、AIN1引脚功能

目录 1. 模拟-数字转换器&#xff08;ADC&#xff09; 2. 交流电源&#xff08;AC&#xff09; 总结 这两部分有什么区别&#xff1f; 在这个电路图中&#xff0c;两个部分分别是模拟-数字转换器&#xff08;ADC&#xff09;和交流电源&#xff08;AC&#xff09;。以下是这…...

交换机与路由器的主要区别:深入分析其工作原理与应用场景

在现代网络架构中&#xff0c;交换机和路由器是两种至关重要的设备。它们在网络中扮演着不同的角色&#xff0c;但很多人对它们的工作原理和功能特性并不十分清楚。本文将深入分析交换机与路由器的主要区别&#xff0c;并探讨它们的工作原理和应用场景。 一、基本定义 1. 交换…...

uniApp小程序保存定制二维码到本地(V3)

这里的二维码组件用的 uv-ui 的二维码 可以按需引入 QRCode 二维码 | 我的资料管理-uv-ui 是全面兼容vue32、nvue、app、h5、小程序等多端的uni-app生态框架 <uv-qrcode ref"qrcode" :size"280" :value"payCodeUrl"></uv-qrcode>&l…...

手机投屏到电视方法

一、投屏软件 比如乐播投屏 二、视频软件 腾讯视频、爱奇艺 三、手机无线投屏功能 四、有线投屏 五、投屏器...

桌面应用UI开发方案

一、基于 Web 技术的跨平台方案 Electron Python/Go 特点&#xff1a; 技术栈&#xff1a;前端使用 HTML/CSS/JS&#xff0c;后端通过 Node.js 集成 Python/Go 模块或服务。 跨平台&#xff1a;支持 Windows、macOS、Linux 桌面端&#xff0c;适合开发桌面应用。 生态成熟&…...

FFmpeg+Nginx+VLC打造M3U8直播

一、视频直播的技术原理和架构方案 直播模型一般包括三个模块&#xff1a;主播方、服务器端和播放端 主播放创造视频&#xff0c;加美颜、水印、特效、采集后推送给直播服务器 播放端&#xff1a; 直播服务器端&#xff1a;收集主播端的视频推流&#xff0c;将其放大后推送给…...

山东科技大学深度学习考试回忆

目录 一、填空&#xff08;五个空&#xff0c;十分&#xff09; 二、选择题(五个&#xff0c;十分&#xff09; 三、判断题&#xff08;五个&#xff0c;五分&#xff09; 四、论述题&#xff08;四个&#xff0c;四十分&#xff09; 五、计算题&#xff08;二个&#xff…...

【Flutter动画深度解析】性能与美学的完美平衡之道

Flutter的动画系统是其UI框架中最引人注目的部分之一&#xff0c;它既能创造令人惊艳的视觉效果&#xff0c;又需要开发者对性能有深刻理解。本文将深入剖析Flutter动画的实现原理、性能优化策略以及设计美学&#xff0c;帮助你打造既流畅又美观的用户体验。 一、Flutter动画核…...

【嵌入式】——Linux系统远程操作和程序编译

目录 一、虚拟机配置网络设置 二、使用PuTTY登录新建的账户 1、在ubuntu下开启ssh服务 2、使用PuTTY连接 三、树莓派实现远程登录 四、树莓派使用VNC viewer登录 五、Linux使用talk聊天程序 1、使用linux自带的talk命令 2、使用c语言编写一个talk程序 一、虚拟机配置网络…...

零、HarmonyOS应用开发者基础学习总览

零、HarmonyOS应用开发者基础认证 1 整体学习内容概览 1 整体学习内容概览 通过系统化的课程学习&#xff0c;熟练掌握 DevEco Studio&#xff0c;ArkTS&#xff0c;ArkUI&#xff0c;预览器&#xff0c;模拟器&#xff0c;SDK 等 HarmonyOS 应用开发的关键概念&#xff0c;具…...

记录一次项目中使用pdf预览过程以及遇到问题以及如何解决

背景 项目中现有的pdf浏览解析不能正确解析展示一些pdf文件&#xff0c;要么内容一直在加载中展示不出来&#xff0c;要么展示的格式很凌乱 解决 方式一&#xff1a;&#xff08;优点&#xff1a;比较无脑&#xff0c;缺点&#xff1a;不能解决遇到的一些特殊问题&#xff0…...

致远OA——自定义开发rest接口

文章目录 :apple: 业务流程 &#x1f34e; 业务流程 代码案例&#xff1a; https://pan.quark.cn/s/57fa808c823f 官方文档&#xff1a; https://open.seeyoncloud.com/seeyonapi/781/https://open.seeyoncloud.com/v5devCTP/39/783.html 登录系统 —— 后台管理 —— 切换系…...

STL之vector基本操作

写在前面 我使用的编译器版本是 g 11.4.0 &#xff08;Ubuntu 22.04 默认版本&#xff09;&#xff0c;支持C17的全部特性&#xff0c;支持C20的部分特性。 vector的作用 我们知道vector是动态数组&#xff08;同时在堆上存储数组元素&#xff09;&#xff0c;我们在不确定数…...

dac直通线还是aoc直通线? sfp使用

"DAC直通线" 和 "AOC直通线" 都是高速互连线缆&#xff0c;用于数据中心、服务器、交换机等设备之间的高速互连。它们的选择主要取决于以下几个方面&#xff1a; &#x1f50c; DAC&#xff08;Direct Attach Cable&#xff0c;直连铜缆&#xff09; 材质&…...

【Linux篇】探索进程间通信:如何使用匿名管道构建高效的进程池

从零开始&#xff1a;通过匿名管道实现进程池的基本原理 一. 进程间通信1.1 基本概念1.2 通信目的1.3 通信种类1.3.1 同步通信1.3.2 异步通信 1.4 如何通信 二. 管道2.1 什么是管道2.2 匿名管道2.2.1 pipe()2.2.2 示例代码&#xff1a;使用 pipe() 进行父子进程通信2.2.3 管道容…...

Mixture-of-Experts with Expert Choice Routing:专家混合模型与专家选择路由

摘要 稀疏激活的专家混合模型(MoE)允许在保持每个token或每个样本计算量不变的情况下,大幅增加参数数量。然而,糟糕的专家路由策略可能导致某些专家未被充分训练,从而使得专家在特定任务上过度或不足专业化。先前的研究通过使用top-k函数为每个token分配固定数量的专家,…...

ai学习中收藏网址【1】

https://github.com/xuwenhao/geektime-ai-course课程⾥所有的代码部分&#xff0c;通过 Jupyter Notebook 的形式放在了 GitHub 上 https://github.com/xuwenhao/geektime-ai-course 图片创作 https://www.midjourney.com/explore?tabtop 创建填⾊本 How to Create Midjour…...

【滑动窗口】最⼤连续 1 的个数 III(medium)

⼤连续 1 的个数 III&#xff08;medium&#xff09; 题⽬描述&#xff1a;解法&#xff08;滑动窗⼝&#xff09;&#xff1a;算法思路&#xff1a;算法流程&#xff1a; C 算法代码&#xff1a;Java 算法代码&#xff1a; 题⽬链接&#xff1a;1004. 最⼤连续 1 的个数 III …...

ClawCloud的免费空间(github用户登录可以获得$5元/月的免费额度)

免费的空间 Welcome to ClawCloud Lets create your workspace 官网&#xff1a;ClawCloud | Cloud Infrastructure And Platform for Developers 区域选择新加坡 然后这个页面会变成新加坡区域&#xff0c;再按一次确定&#xff0c;就创建好了工作台。 初始界面&#xff0…...