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

lesson8-Linux多线程

Linux线程概念

  •  线程在进程内部执行,是OS调度的基本单位
  • OS是可以做到让进程进行资源的细粒度划分的

  •  物理内存是以4kb为单位
  • 我们的.exe可执行程序本来就是按照地址空间的方式进行编译的

 页表映射 - 详细图

 理解线程

  •  线程在进程的地址空间内运行,
  •  进程内部具有多个执行流的,而线程只有一个执行流,
  • 线程独有一组寄存器,栈等等

CPU的视角

  • Linux下,PCB <= 其他OS内的PCB的,所以Linux下的进程:统一称之为轻量级进程
  • CPU其实不关心执行流是进程还是线程,只关心PCB

Linux没有真正意义上的线性结构,Linux是用进程PCB模拟线程的,

Linux并不能直接给我们提供线程的接口,只能提供轻量级进程的接口!

  •   它也在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用pthread线程库 -- 原生线程库

为什么线程切换的成本更低

  • CPU内部是有L1~L3cache 对应内存的代码和数据,根据局部性原理,预读CPU内部!!!
  •  线程是不用切换页表 && 改变地址空间,
  • 进程需要切换cache就会立即失效,新进程过来,只能重新缓存,

创建线程

pthread_create

 

  •  这段代码也可以证明CPU是用LWP来区分线程

线程ID及进程地址空间布局

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID 不是一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,

 pthread_self

  •  可以获得线程自身的ID

 pthread_t类型的理解

  • 对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID
  • 本质就是一个进程地址空间上的一个地址。

 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:
  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

 pthread_cancel

 线程等待

为什么需要线程等待?
  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

pthread_join 

分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

pthread_detach

 Linux线程互斥

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用原子性
  • 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{char *id = (char*)arg;while ( 1 ) {if ( ticket > 0 ) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;} else {break;}}
}int main( void )
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

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

为什么可能无法获得争取结果?

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

 要解决以上问题,需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

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

pthread_mutex_lock && pthread_mutex_unlock

 

#include <iostream>
#include <thread>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <cassert>
#include <cstdio>using namespace std;int tickets = 10000; // 在并发访问的时候,导致了我们数据不一致的问题!临界资源#define THREAD_NUM 10// 线程数量class ThreadData
{
public:ThreadData(const std::string& n, pthread_mutex_t* pm) :tname(n), pmtx(pm){}
public:std::string tname;pthread_mutex_t* pmtx;
};void* getTickets(void* args)
{ThreadData* td = (ThreadData*)args;while (true){// 抢票逻辑int n = pthread_mutex_lock(td->pmtx);// 加锁assert(n == 0);// 临界区if (tickets > 0) // 1. 判断的本质也是计算的一种{usleep(rand() % 1500);printf("%s: %d\n", td->tname.c_str(), tickets);tickets--; // 2. 也可能出现问题n = pthread_mutex_unlock(td->pmtx);// 解锁assert(n == 0);}else {n = pthread_mutex_unlock(td->pmtx);// 解锁assert(n == 0);break;}// 抢完票,其实还需要后续的动作usleep(rand() % 2000);}delete td;return nullptr;
}int main()
{time_t start = time(nullptr);pthread_mutex_t mtx;// 局部变量 + pthread_mutex_init初始化pthread_mutex_init(&mtx, nullptr);srand((unsigned long)time(nullptr) ^ getpid() ^ 0x147);// 让随机数更随机pthread_t t[THREAD_NUM];// 多线程抢票的逻辑for (int i = 0; i < THREAD_NUM; i++){std::string name = "thread ";name += std::to_string(i + 1);ThreadData* td = new ThreadData(name, &mtx);pthread_create(t + i, nullptr, getTickets, (void*)td);}for (int i = 0; i < THREAD_NUM; i++){pthread_join(t[i], nullptr);}pthread_mutex_destroy(&mtx);time_t end = time(nullptr);cout << "cast: " << (int)(end - start) << "S" << endl;
}

  • pthread_mutex_t 就是原生线程库提供的一个数据类型 
  •  如果多线程访问同一个全局变量,并对它进行数据计算,多线程不会互相影响
    • 可以定义一个全局变量 pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
  • 加锁保护:加锁的时候,一定要保证加锁的力度,越小越好!!
  • 加锁之后就是串行执行的了,线程在临界区,切换也不会有什么问题,
    • 锁只有一把,线程切换走了,持有锁也会被带走,而其他抢票的线程要执行临界区的代码
    • 又要申请锁,这时锁是无法申请成功的,也就不会让其他线程进入临界区,保证了临界区中数据的一致性

对锁的理解 

  •  在上那段代码中,锁只有一把,拿1的汇编只有1条
  • 一个线程被切换走,它会将自己的数据对应执行到的汇编 

补充一点: 可重入函数就是安全的

常见锁概念 

死锁

  •  一把锁也可能发生死锁问题

死锁四个必要条件

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

避免死锁

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

Linux线程同步

条件变量

  •  当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

  • 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

同步概念与竞态条件 

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

 引入同步: 主要是为了解决访问临界资源合理性问题的,使线程按照一定顺序,进行临界资源的访问,线程同步

方案一: 条件变量

  • 当我们申请临界资源前,先要做临界资源是否存在的检测,而检测的本质: 也是访问临界资源
  • 所以对临界资源的检测,也一定需要在加锁和解锁之间的

 

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>#define TNUM 4
typedef void (*func_t)(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond);
volatile bool quit = false;class ThreadData
{
public:ThreadData(const std::string &name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond):name_(name), func_(func), pmtx_(pmtx), pcond_(pcond){}
public:std::string name_;func_t func_;pthread_mutex_t *pmtx_;pthread_cond_t *pcond_;
};void func1(const std::string &name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){// wait一定要在加锁和解锁之间进行wait!// v2: pthread_mutex_lock(pmtx);// if(临界资源是否就绪-- 否) pthread_cond_waitpthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞std::cout << name << " running -- 播放" << std::endl;pthread_mutex_unlock(pmtx);}
}void func2(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞if(!quit) std::cout << name << " running  -- 下载" << std::endl;pthread_mutex_unlock(pmtx);}
}
void func3(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞std::cout << name << " running  -- 刷新" << std::endl;pthread_mutex_unlock(pmtx);}
}
void func4(const std::string &name,pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx); //默认该线程在执行的时候,wait代码被执行,当前线程会被立即被阻塞std::cout << name << " running  -- 扫码用户信息" << std::endl;pthread_mutex_unlock(pmtx);}
}void *Entry(void *args)
{ThreadData *td = (ThreadData*)args; //td在每一个线程自己私有的栈空间中保存td->func_(td->name_, td->pmtx_, td->pcond_); // 它是一个函数,调用完成就要返回!delete td;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_cond_t cond;// 条件变量pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);// 初始化条件变量pthread_t tids[TNUM];func_t funcs[TNUM] = {func1, func2, func3, func4};// 函数指针数组for (int i = 0; i < TNUM; i++){std::string name = "Thread ";name += std::to_string(i + 1);ThreadData *td = new ThreadData(name, funcs[i], &mtx, &cond);pthread_create(tids + i, nullptr, Entry, (void*)td);}sleep(5);// ctrl new threadint cnt = 10;while(cnt){std::cout << "resume thread run code ...." << cnt-- << std::endl;pthread_cond_signal(&cond);// 唤醒等待sleep(1);}std::cout << "ctrl done" << std::endl;quit = true;pthread_cond_broadcast(&cond);// 唤醒等待for(int i = 0; i < TNUM; i++){pthread_join(tids[i], nullptr);std::cout << "thread: " << tids[i] << "quit" << std::endl;}pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}

生产者消费者模型 

 

 321原则

  • 3种关系:
    • 生产者和生产者(竞争,互斥)
    • 消费者和消费者(竞争,互斥)
    • 生产者和消费者(互斥,同步)
  • 2种角色:
    • 生产者和消费者
  • 1个交易场所
    • 超市

BlockQueue.hpp

#pragma once#include <iostream>
#include <queue>
#include <mutex>
#include <pthread.h>
#include "lockGuard.hpp"const int gDefaultCap = 5;// 默认容量template <class T>
class BlockQueue
{
private:bool isQueueEmpty(){return bq_.size() == 0;}bool isQueueFull(){return bq_.size() == capacity_;}
public:// 构造BlockQueue(int capacity = gDefaultCap) : capacity_(capacity){pthread_mutex_init(&mtx_, nullptr);pthread_cond_init(&Empty_, nullptr);pthread_cond_init(&Full_, nullptr);}void push(const T &in) // 生产者{// 1. 先检测当前的临界资源是否能够满足访问条件lockGuard lockgrard(&mtx_); // 自动调用构造函数while (isQueueFull()){pthread_cond_wait(&Full_, &mtx_);}// 2. 访问临界资源,100%确定,资源是就绪的!bq_.push(in);pthread_cond_signal(&Empty_);// 局部变量 出了作用域会自动调用lockgrard 析构函数} void pop(T *out){lockGuard lockguard(&mtx_);while (isQueueEmpty())pthread_cond_wait(&Empty_, &mtx_);*out = bq_.front();bq_.pop();pthread_cond_signal(&Full_);}~BlockQueue(){pthread_mutex_destroy(&mtx_);pthread_cond_destroy(&Empty_);pthread_cond_destroy(&Full_);}
private:std::queue<T> bq_;     // 阻塞队列int capacity_;         // 容量上限pthread_mutex_t mtx_;  // 通过互斥锁保证队列安全pthread_cond_t Empty_; // 用它来表示bq 是否空的条件pthread_cond_t Full_;  //  用它来表示bq 是否满的条件
};

ConProd.cc

#include "BlockQueue.hpp"
#include "Task.hpp"#include <pthread.h>
#include <unistd.h>
#include <ctime>int myAdd(int x, int y)
{return x + y;
}void* consumer(void *args)
{BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;while(true){// 获取任务Task t;bqueue->pop(&t);// 完成任务std::cout << pthread_self() <<" consumer: "<< t.x_ << "+" << t.y_ << "=" << t() << std::endl;// sleep(1);}return nullptr;
}// 生产者
void* productor(void *args)
{BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;while(true){// 制作任务int x = rand()%10 + 1;usleep(rand()%1000);int y = rand()%5 + 1;Task t(x, y, myAdd);// 生产任务bqueue->push(t);// 输出消息std::cout <<pthread_self() <<" productor: "<< t.x_ << "+" << t.y_ << "=?" << std::endl;sleep(1);}return nullptr;
}int main()
{srand((uint64_t)time(nullptr) ^ getpid() ^ 0x32457);BlockQueue<Task> *bqueue = new BlockQueue<Task>();pthread_t c[2],p[2];pthread_create(c, nullptr, consumer, bqueue);pthread_create(c + 1, nullptr, consumer, bqueue);pthread_create(p, nullptr, productor, bqueue);pthread_create(p + 1, nullptr, productor, bqueue);pthread_join(c[0], nullptr);// 等待pthread_join(c[1], nullptr);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);delete bqueue;return 0;
}

Task.hpp

#pragma once#include <iostream>
#include <functional>typedef std::function<int(int, int)> func_t;class Task
{public:Task(){}Task(int x, int y, func_t func):x_(x), y_(y), func_(func){}int operator ()(){return func_(x_, y_);}
public:int x_;int y_;func_t func_;
};

lockGuard.hpp

#pragma once#include <iostream>
#include <pthread.h>
// 定义一把锁
class Mutex
{
public:Mutex(pthread_mutex_t *mtx):pmtx_(mtx){}void lock() {std::cout << "要进行加锁" << std::endl;pthread_mutex_lock(pmtx_);}void unlock(){std::cout << "要进行解锁" << std::endl;pthread_mutex_unlock(pmtx_);}~Mutex(){}
private:pthread_mutex_t *pmtx_;
};// RAII风格的加锁方式
class lockGuard
{
public:lockGuard(pthread_mutex_t *mtx):mtx_(mtx){mtx_.lock();}~lockGuard(){mtx_.unlock();}
private:Mutex mtx_;
};

Makefile

cp:ConProd.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f cp
  • pthread_cond_wait第二个参数是一个锁,当成功调用wait之后,传入的锁,会被自动释放!
  • 从哪里阻塞挂起,就从哪里唤醒, 被唤醒的时候,我们还是在临界区被唤醒的啊
  • RAII风格的加锁(封装了一个锁的类)

POSIX信号量

信号量的概念

 

  • 共享资源->任何时候都只有一个执行流在进行访问,
  • 如果一个共享资源,不当做一个整体,而让不同的执行流访问不同的局域的话,那就可以并发
  • 我们用信号量来表示这段共享资源中还剩多少个资源,

 信号量本质:

  • 是一个计数器,访问临界资源的时候,必须先申请信号量资源(sem--,预订资源,P),
  • 使用完毕信号量资源(sem++,释放资源,V)

 环形队列实现生产者消费者模型 

  • 生产者不能将消费者套圈,消费者不能超过生产者,
  • 为空: 一定要让生产者先运行
  • 为满: 一定要让消费者先运行

 生产者: 最关心的是空间资源->spaceSem->N

  •  P(spaceSem),将会在特定位置生产,然后V(dataSem)

消费者: 最关心的是数据资源->dataSem->0

  • P(dataSem),消费特定的数据,然后V(spaceSem)

Makefile

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

 ringQueue.hpp

#ifndef _Ring_QUEUE_HPP_
#define _Ring_QUEUE_HPP_#include <iostream>
#include <vector>
#include <pthread.h>
#include "sem.hpp"const int g_default_num = 5;// 多线程
template<class T>
class RingQueue
{
public:RingQueue(int default_num = g_default_num): ring_queue_(default_num), num_(default_num),c_step(0),p_step(0),space_sem_(default_num),data_sem_(0){// 初始化锁pthread_mutex_init(&clock, nullptr);pthread_mutex_init(&plock, nullptr);}~RingQueue(){// 销毁锁pthread_mutex_destroy(&clock);pthread_mutex_destroy(&plock);}// 生产者: 空间资源, 生产者们的临界资源是什么?下标void push(const T &in){// 先申请信号量(0)space_sem_.p();// --pthread_mutex_lock(&plock); // ?// 一定是竞争成功的生产者线程 -- 就一个!ring_queue_[p_step++] = in;p_step %= num_;pthread_mutex_unlock(&plock);data_sem_.v();// ++ }// 消费者: 数据资源, 消费者们的临界资源是什么?下标void pop(T *out){data_sem_.p();pthread_mutex_lock(&clock);// 一定是竞争成功的消费者线程 -- 就一个!*out = ring_queue_[c_step++];c_step %= num_;pthread_mutex_unlock(&clock);space_sem_.v();}// void debug()// {//     std::cerr << "size: " << ring_queue_.size() << " num: " << num << std::endl;// } 
private:std::vector<T> ring_queue_;int num_;int c_step; // 消费下标int p_step; // 生产下标Sem space_sem_;// 记录空间的信号量Sem data_sem_;// 记录空间数据的信号量pthread_mutex_t clock;// 消费者的锁pthread_mutex_t plock;// 生产者的锁
};#endif

sem.hpp

#ifndef _SEM_HPP_
#define _SEM_HPP_#include <iostream>
#include <semaphore.h>class Sem
{
public:Sem(int value){// 信号量初始化sem_init(&sem_, 0, value);}void p(){// 等待信号量,会将信号量的值减1sem_wait(&sem_);}void v(){// 发布信号量,会将信号量的值加1sem_post(&sem_);}~Sem(){// 销毁相互量sem_destroy(&sem_);}
private:sem_t sem_;
};#endif

 testMain.cc

#include "ringQueue.hpp"
#include <cstdlib>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>// 消费者
void *consumer(void *args)
{// 生产者和消费者看到的是同一个rqRingQueue<int> *rq = (RingQueue<int> *)args;while(true){sleep(1);int x;// 1. 从环形队列中获取任务或者数据rq->pop(&x);// 2. 进行一定的处理 -- 不要忽略它的时间消耗问题std::cout << "生产: " << x << " [" << pthread_self() << "]" << std::endl;} 
}// 生产者
void *productor(void *args)
{// 生产者和消费者看到的是同一个rqRingQueue<int> *rq = (RingQueue<int> *)args;while(true){// sleep(1);// 1. 构建数据或者任务对象 -- 一般是可以从外部来 -- 不要忽略它的时间消耗问题int x = rand() % 100 + 1;std::cout << "消费:" << x << " [" << pthread_self() << "]" << std::endl;// 2. 推送到环形队列中rq->push(x); // 完成生产的过程}
}int main()
{srand((uint64_t)time(nullptr) ^ getpid());// 产生随机数RingQueue<int> *rq = new RingQueue<int>();// 多线程模式pthread_t c[3],p[2];pthread_create(c, nullptr, consumer, (void*)rq);pthread_create(c+1, nullptr, consumer, (void*)rq);pthread_create(c+2, nullptr, consumer, (void*)rq);pthread_create(p, nullptr, productor, (void*)rq);pthread_create(p+1, nullptr, productor, (void*)rq);for(int i = 0; i < 3; i++) pthread_join(c[i], nullptr);for(int i = 0; i < 2; i++) pthread_join(p[i], nullptr);return 0;
}

 多生产多消费的意义

  • 将数据或者任务生产前和拿到之后处理,才是最耗费时间的。

  • 生产的本质:私有的任务-> 公共空间中

  • 消费的本质:公共空间中的任务-> 私有化

信号量的意义

  • 信号量本质是一把计数器, 可以不用进入临界区,就可以得知资源情况,甚至可以减少临界区内部的判断!
  • 信号量可以提前预设资源的情况,而且在pv变化过程中,我们可以在外部就能知晓临界资源的情况

线程池

Makefile

thread_pool:testMain.ccg++ -o $@ $^ -std=c++11 -lpthread #-DDEBUG_SHOW
.PHONY:clean
clean:rm -f thread_pool
  • -DDEBUG_SHOW命令中定义了一个宏,多和条件变量配合使用

 Task.hpp

#pragma once#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"typedef std::function<int(int, int)> func_t;class Task
{
public:Task(){}Task(int x, int y, func_t func):x_(x), y_(y), func_(func){}void operator ()(const std::string &name){// std::cout << "线程 " << name << " 处理完成, 结果是: " << x_ << "+" << y_ << "=" << func_(x_, y_) << std::endl;logMessage(WARNING, "%s处理完成: %d+%d=%d | %s | %d",name.c_str(), x_, y_, func_(x_, y_), __FILE__, __LINE__);}
public:int x_;int y_;// int type;func_t func_;
};

lockGuard.hpp

#pragma once#include <iostream>
#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t *mtx):pmtx_(mtx){}void lock() {// std::cout << "要进行加锁" << std::endl;pthread_mutex_lock(pmtx_);}void unlock(){// std::cout << "要进行解锁" << std::endl;pthread_mutex_unlock(pmtx_);}~Mutex(){}
private:pthread_mutex_t *pmtx_;
};// RAII风格的加锁方式
class lockGuard
{
public:lockGuard(pthread_mutex_t *mtx):mtx_(mtx){mtx_.lock();}~lockGuard(){mtx_.unlock();}
private:Mutex mtx_;
};

log.hpp

#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./threadpool.log"// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif(level== DEBUG) return;
#endif// va_list ap;// va_start(ap, format);// while()// int x = va_arg(ap, int);// va_end(ap); //ap=nullptrchar stdBuffer[1024]; //标准部分time_t timestamp = time(nullptr);// struct tm *localtime = localtime(&timestamp);snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; //自定义部分va_list args;va_start(args, format);// vprintf(format, args);vsnprintf(logBuffer, sizeof logBuffer, format, args);va_end(args);FILE *fp = fopen(LOGFILE, "a");// printf("%s%s\n", stdBuffer, logBuffer);fprintf(fp, "%s%s\n", stdBuffer, logBuffer);fclose(fp);
}

 

thread.hpp 

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>// typedef std::function<void* (void*)> fun_t;
typedef void* (*fun_t)(void*);class ThreadData
{
public:void* args_;std::string name_;
};class Thread
{
public:Thread(int num, fun_t callback, void* args) : func_(callback){char nameBuffer[64];snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);name_ = nameBuffer;tdata_.args_ = args;tdata_.name_ = name_;}void start(){pthread_create(&tid_, nullptr, func_, (void*)&tdata_);}void join(){pthread_join(tid_, nullptr);}std::string name(){return name_;}~Thread(){}private:std::string name_;fun_t func_;ThreadData tdata_;pthread_t tid_;
};

threadPool.hpp

#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"const int g_thread_num = 3;
// 本质是: 生产消费模型
template <class T>
class ThreadPool
{
public:pthread_mutex_t* getMutex(){return &lock;}bool isEmpty(){return task_queue_.empty();}void waitCond(){pthread_cond_wait(&cond, &lock);}T getTask(){T t = task_queue_.front();task_queue_.pop();return t;}private:ThreadPool(int thread_num = g_thread_num) : num_(thread_num){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);for (int i = 1; i <= num_; i++){threads_.push_back(new Thread(i, routine, this));}}ThreadPool(const ThreadPool<T>& other) = delete;const ThreadPool<T>& operator=(const ThreadPool<T>& other) = delete;public:// 考虑一下多线程使用单例的过程static ThreadPool<T>* getThreadPool(int num = g_thread_num){// 可以有效减少未来必定要进行加锁检测的问题// 拦截大量的在已经创建好单例的时候,剩余线程请求单例的而直接访问锁的行为if (nullptr == thread_ptr){lockGuard lockguard(&mutex);// 但是,未来任何一个线程想获取单例,都必须调用getThreadPool接口// 但是,一定会存在大量的申请和释放锁的行为,这个是无用且浪费资源的// pthread_mutex_lock(&mutex);if (nullptr == thread_ptr){thread_ptr = new ThreadPool<T>(num);}// pthread_mutex_unlock(&mutex);}return thread_ptr;}// 1. run()void run(){for (auto& iter : threads_){iter->start();// std::cout << iter->name() << " 启动成功" << std::endl;logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");}}// 线程池本质也是一个生产消费模型// void *routine(void *args)// 消费过程static void* routine(void* args){ThreadData* td = (ThreadData*)args;ThreadPool<T>* tp = (ThreadPool<T> *)td->args_;while (true){T task;{lockGuard lockguard(tp->getMutex());while (tp->isEmpty())tp->waitCond();// 读取任务task = tp->getTask(); // 任务队列是共享的-> 将任务从共享,拿到自己的私有空间}task(td->name_);// lock// while(task_queue_.empty()) wait();// 获取任务// unlock// 处理任务}}// 2. pushTask()void pushTask(const T& task){lockGuard lockguard(&lock);task_queue_.push(task);pthread_cond_signal(&cond);}// test func// void joins()// {//     for (auto &iter : threads_)//     {//         iter->join();//     }// }~ThreadPool(){for (auto& iter : threads_){iter->join();delete iter;}pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}private:std::vector<Thread*> threads_;int num_;std::queue<T> task_queue_;static ThreadPool<T>* thread_ptr;static pthread_mutex_t mutex;// 方案2://  queue1,queue2//  std::queue<T> *p_queue, *c_queue//  p_queue->queue1//  c_queue->queue2//  p_queue -> 生产一批任务之后,swap(p_queue,c_queue),唤醒所有线程/一个线程//  当消费者处理完毕的时候,你也可以进行swap(p_queue,c_queue)//  因为我们生产和消费用的是不同的队列,未来我们要进行资源的处理的时候,仅仅是指针pthread_mutex_t lock;pthread_cond_t cond;
};
template <typename T>
ThreadPool<T>* ThreadPool<T>::thread_ptr = nullptr;template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;

  • 进入{}创建lockGuard对象,并调用构造函数
  • 出{}调用lockGuard对象的析构函数

 

  •  这里使用双重判断,主要为了拦截大量的在已经创建好单例的时候,
  • 剩余线程请求单例的而直接访问锁的行为

testMain.cc

#include "threadPool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>// void *run(void *args)
// {
//     while(true)
//     {
//         ThreadPool<Task>::getThreadPool();
//     }
// }int main()
{// logMessage(NORMAL, "%s %d %c %f \n", "这是一条日志信息", 1234, 'c', 3.14);srand((unsigned long)time(nullptr) ^ getpid());// ThreadPool<Task> *tp = new ThreadPool<Task>();// ThreadPool<Task> *tp = ThreadPool<Task>::getThreadPool();// 那么,如果单例本身也在被多线程申请使用呢??ThreadPool<Task>::getThreadPool()->run();//thread1,2,3,4while(true){//生产的过程,制作任务的时候,要花时间int x = rand()%100 + 1;usleep(7721);int y = rand()%30 + 1;Task t(x, y, [](int x, int y)->int{return x + y;});// std::cout << "制作任务完成: " << x << "+" << y << "=?" << std::endl;logMessage(DEBUG, "制作任务完成: %d+%d=?", x, y);logMessage(DEBUG, "制作任务完成: %d+%d=?", x, y);logMessage(DEBUG, "制作任务完成: %d+%d=?", x, y);logMessage(DEBUG, "制作任务完成: %d+%d=?", x, y);// 推送任务到线程池中ThreadPool<Task>::getThreadPool()->pushTask(t);sleep(1);}return 0;
}

threadpool.log

[NORMAL] [1675916038] Thread-1 启动成功
[NORMAL] [1675916038] Thread-2 启动成功
[NORMAL] [1675916038] Thread-3 启动成功
[WARNING] [1675916038] Thread-1处理完成: 21+20=41 | Task.hpp | 20
[WARNING] [1675916039] Thread-2处理完成: 69+30=99 | Task.hpp | 20
[WARNING] [1675916040] Thread-3处理完成: 97+21=118 | Task.hpp | 20
[WARNING] [1675916041] Thread-1处理完成: 74+21=95 | Task.hpp | 20
[WARNING] [1675916042] Thread-2处理完成: 30+24=54 | Task.hpp | 20
[WARNING] [1675916043] Thread-3处理完成: 95+17=112 | Task.hpp | 20
[WARNING] [1675916044] Thread-1处理完成: 76+27=103 | Task.hpp | 20
[WARNING] [1675916045] Thread-2处理完成: 67+7=74 | Task.hpp | 20
[WARNING] [1675916046] Thread-3处理完成: 38+4=42 | Task.hpp | 20
[WARNING] [1675916047] Thread-1处理完成: 62+19=81 | Task.hpp | 20
[WARNING] [1675916048] Thread-2处理完成: 93+21=114 | Task.hpp | 20
[WARNING] [1675916049] Thread-3处理完成: 64+13=77 | Task.hpp | 20
[WARNING] [1675916050] Thread-1处理完成: 81+3=84 | Task.hpp | 20
[WARNING] [1675916051] Thread-2处理完成: 86+26=112 | Task.hpp | 20
[WARNING] [1675916052] Thread-3处理完成: 24+21=45 | Task.hpp | 20
[WARNING] [1675916053] Thread-1处理完成: 69+6=75 | Task.hpp | 20

相关文章:

lesson8-Linux多线程

Linux线程概念 线程在进程内部执行,是OS调度的基本单位OS是可以做到让进程进行资源的细粒度划分的物理内存是以4kb为单位的我们的.exe可执行程序本来就是按照地址空间的方式进行编译的页表映射 - 详细图 理解线程 线程在进程的地址空间内运行, 进程内部具有多个执行流的,而线程…...

python的django框架从入门到熟练【保姆式教学】第四篇

在前三篇博客中&#xff0c;我们介绍了Django的模型层、数据库迁移、视图层和URL路由。本篇博客将介绍Django的模板层&#xff0c;讲解如何使用模板来创建美观的Web页面。 模板层&#xff08;Template&#xff09; Django的模板层是Django应用程序的另一个核心组件。模板是一…...

Codeforces Round 852 (Div. 2)

A Yet Another Promotion 题意&#xff1a;要买n千克物品&#xff0c;第一天的价格为a&#xff0c;第二天的价格为b。第一天有促销活动&#xff0c;每买m千克物品&#xff0c;可以额外获得1千克物品。问最少花费多少可以获得至少n千克的物品。 思路&#xff1a;分类讨论&…...

【PTA Data Structures and Algorithms (English)】7-2 Reversing Linked List

Given a constant K and a singly linked list L, you are supposed to reverse the links of every K elements on L. For example, given L being 1→2→3→4→5→6, if K3, then you must output 3→2→1→6→5→4; if K4, you must output 4→3→2→1→5→6. Input Specif…...

Jetpack Compose 学习汇总

关于 Jetpack Compose 的学习本想只是简单的快速学习一下&#xff0c;结果万万没想到&#xff0c;竟然一下子折腾了好几个月。。。 下面将之前记录的 Jetpack Compose 相关的学习博文进行一个汇总链接整理&#xff0c;方便我以后自己查阅&#xff0c;也希望能帮到一些有正在学…...

【OpenCv】c++ 图像初级操作 | 图像灰度化

文章目录一、图像1、图像信息2、图像种类1&#xff09;二值图像&#xff1a;2&#xff09;灰度图:3&#xff09;彩色图&#xff1a;二、图像转化1、分离彩色图三个通道2、图像灰度化处理一、图像 1、图像信息 Q&#xff1a;图像在计算机中怎么储存&#xff1f; A&#xff1a;…...

VIT(vision transformer)onnx模型解析

背景&#xff1a;transformer在CV领域的应用论文下载链接&#xff1a;https://arxiv.org/abs/2010.11929Pytorch实现代码&#xff1a; pytorch_classification/vision_transformer(太阳花的小绿豆博主实现的代码)有一些大神在研究关于CNNtransformer或者纯用transformer实现。原…...

红黑树的介绍和实现

文章目录1. 红黑树1.1 红黑树的概念1.2 红黑树的性质1.3 红黑树节点的定义1.4 红黑树的插入1.5 红黑树的验证1.6 红黑树与AVL树的比较1. 红黑树 1.1 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以…...

C/C++每日一练(20230310)

目录 1. 用栈实现队列 ★★ 2. 单词搜索 II ★★★ 3. 直线上最多的点数 ★★★ 1. 用栈实现队列 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; v…...

Go语言基础知识

常量//定义方式 const a int12;//指定变量类型 const b12;//不指定变量类型&#xff0c;由编译时go自动确认 const(//多行定义方式a12b23 ) //说到const&#xff0c;不得不得不提到的一个参数iota,初始值为0&#xff0c;在用const多行定义的方式中&#xff0c; 如果第一行定义了…...

案例06-没有复用思想的接口和sql--mybatis,spring

目录一、背景二、思路&方案问题1优化问题2优化三、总结四、升华一、背景 写这篇文章的目的是通过对没有复用思想接口的代码例子优化告诉大家&#xff0c;没有复用思想的代码不要写&#xff0c;用这种思维方式和习惯来指导我们写代码。 项目中有两处没有复用思想代码&#…...

如何将项目部署到服务器:从选择服务器到维护应用程序的全流程指南

将项目部署到服务器是一个重要的技能&#xff0c;对于开发人员来说&#xff0c;它是必不可少的。在本文中&#xff0c;我将介绍一些关于如何将项目部署到服务器的最佳实践。一、选择服务器在部署项目之前&#xff0c;你需要先选择一个适合你的服务器。如果你已经有一个可用的服…...

怎么做才能不丢消息?

现在主流的消息队列产品都提供了非常完善的消息可靠性保证机制&#xff0c;可以做到在消息传递的过程中&#xff0c;即使发生网络中断或者硬件故障&#xff0c;也能确保消息的可靠传递、不丢消息。 绝大部分丢消息的原因都是由于开发者不熟悉消息队列&#xff0c;没有正确使用…...

前端基础(十六)_数组对象

数组对象 1、创建数组 // 字面量创建const arr [1, 2, 3, 4, 5, 6]// 构造函数创建const arr2 new Array(1, 2, 3, 4, 5, 6)const arr3 Array(1, 2, 3, 4, 5, 6)2.push (从数组末尾添加元素) a.数组.push(要添加进数组的数组项) b.作用&#xff1a;将要添加的数组项 添加到…...

数据结构-带头双向循环链表

前言&#xff1a; 链表有很多种&#xff0c;上一章结&#xff0c;我复盘了单链表&#xff0c;这一章节&#xff0c;主要针对双链表的知识点进行&#xff0c;整理复盘&#xff0c;如果将链表分类的话&#xff0c;有很多种&#xff0c;我就学习的方向考察的重点&#xff0c;主要…...

3 问 6 步,极狐GitLab 帮助企业构建高效、安全、合规的 DevSecOps 文化

本文来源&#xff1a;about.gitlab.com 作者&#xff1a;Vanessa Wegner 译者&#xff1a;极狐(GitLab) 市场部内容团队 &#x1f512; 安全为何重要&#xff1f;此前&#xff0c;我们分享了&#xff1a; 1. 2023年DevOps发展趋势&#x1f449;重磅&#xff01;GitLab 提出五大…...

SPA(单页应用)知多少

单页面应用程序将所有的活动局限于一个Web页面中&#xff0c;在该Web页面初始化时加载相应的HTML、JavaScript 和 CSS。一旦页面加载完成&#xff0c;单页面应用不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用 JavaScript 动态的变换HTML的内容&#xff0c;从…...

Selenium实战【远程控制】【JAVA爬虫】

简介 Selenium RemoteWebDriver是Selenium WebDriver的一个扩展,它可以将测试运行在远程机器上的浏览器中。 使用RemoteWebDriver,可以在本地机器上编写测试脚本,然后将测试请求发送到远程机器上的浏览器中执行。这使得测试可以在多个不同的机器上并行运行,从而加快测试的…...

图片动画化应用中的动作分解方法

作者 | FesianXu 前言 最近基于AI的换脸应用非常的火爆&#xff0c;同时也引起了新一轮的网络伦理大讨论。如果光从技术的角度看&#xff0c;对于视频中的人体动作信息&#xff0c;通常可以通过泰勒展开分解成零阶运动信息与一阶运动信息&#xff0c;如文献[1,2]中提到的&…...

我又和redis超时杠上了

背景 经过上次redis超时排查&#xff0c;并联系云服务商解决之后&#xff0c;redis超时的现象好了一阵子&#xff0c;但是最近又有超时现象报出&#xff0c;但与上次不同的是&#xff0c;这次超时的现象发生在业务高峰期&#xff0c;在简单看过服务器的各项指标以后&#xff0…...

一文带你吃透MySQL数据库!

文章目录1. 索引2. 事务3. 存储引擎4. 锁机制5. MySQL其他知识点文章字数大约1.27万字&#xff0c;阅读大概需要42分钟&#xff0c;建议收藏后慢慢阅读&#xff01;&#xff01;&#xff01;1. 索引 为什么使用索引 通过创建唯一性索引&#xff0c;可以保证数据库表中每一行数据…...

[学习笔记] 2. 数据结构

数据结构视频地址&#xff1a;https://www.bilibili.com/video/BV1uA411N7c5 数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。简单来说&#xff0c;数据结构就是设计数据以何种方式组织并存储在计算机中。 比如:列表、集合与字…...

[学习笔记] 3. 算法进阶

算法进阶视频地址&#xff1a;https://www.bilibili.com/video/BV1uA411N7c5 1. 贪心算法 贪心算法&#xff08;又称贪婪算法&#xff09;&#xff0c;是指在对问题求解时&#xff0c;总是做出在当前看来是最好的选择。也就是说&#xff0c;不从整体最优上加以考虑 —— 所做…...

做自媒体真的能赚到钱吗?真的能赚到几十万吗?

自媒体在当今社会已经成为一个热门话题&#xff0c;越来越多的人开始尝试做自媒体&#xff0c;希望能够通过自媒体赚到钱。但是&#xff0c;做自媒体真的能赚到钱吗&#xff1f;能赚到几十万吗&#xff1f;下面我们来一一解答。 首先&#xff0c;做自媒体确实可以赚到钱。随着互…...

QT使用QListWidget显示多张图片

Qt系列文章目录 文章目录Qt系列文章目录前言一、QListWidget 和 QListView 的差异二、显示效果1.操作工作区界面1.主界面头文件2. 主界面实现界面2.左边图片目录展示界面1.图片目录头文件2.图片目录实现文件2.属性窗口区1.属性窗口头文件2.属性窗口实现文件3 源码下载前言 QLi…...

python 打印进度条

import time recv_size0 total_size1024while recv_size < total_size:time.sleep(0.1)recv_size1024#打印进度条percentrecv_size / total_sizeres int(50 * percent) * #print(\r[%-50s] %d%% % (res,int(100 * percent)),end) # end 打印以‘’结尾&#xff0c;打印% 需…...

【微小说】大学日记

感谢B站up主“看见晴晴了吗”的视频提供的灵感&#xff0c;链接&#xff1a;https://www.bilibili.com/video/BV1tA411m7Kc 整篇故事完全虚构&#xff0c;如有雷同纯属巧合。 2019年8月25日 星期天 晴 今天是我进入大学的第一天。早晨&#xff0c;我画了美美的妆&#xff0c;穿…...

ArrayList扩容机制解析

1.ArrayList的成员变量 首先我们先了解一下ArrayList的成员变量。 // 默认初始化大小 private static final int DEFAULT_CAPACITY 10;// 空数组&#xff08;用于空实例&#xff09; // 比如List<String> ls new ArrayList<>(0); private static final Object[…...

jsp-----web应用与开发

jsp基本语法 jsp页面的基本结构 定义变量 <%! %> 表达式&#xff1a;变量、常量、表达式 <% %>代码块、程序段【jsp程序代码即jsp脚本】 <% %>注释 隐藏注释 不会显示在客户的浏览器上&#xff0c;即jsp页面运行后页面上看不到注释内容。同时也不会出…...

洛谷 P1201 [USACO1.1]贪婪的送礼者Greedy Gift Givers

题目链接&#xff1a;P1201 [USACO1.1]贪婪的送礼者Greedy Gift Givers - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目描述 对于一群 n 个要互送礼物的朋友&#xff0c;GY 要确定每个人送出的钱比收到的多多少。在这一个问题中&#xff0c;每个人都准备了一些钱来送礼物…...

php设计模式-组合模式的运用

介绍 PHP的组合模式是一种设计模式&#xff0c;用于将对象组合成树形结构以表示“部分-整体”的层次结构。该模式允许客户端统一处理单个对象和组合对象&#xff0c;使得客户端在处理对象时不需要知道对象是否为单个对象还是组合对象。 在组合模式中&#xff0c;有两种类型的…...

一文教会你如何简单使用Fegin进行远程服务调用

文章目录1、fegin的基本介绍2、fegin的基本使用步骤3、项目中的实际运用4、测试前言在分布式微服务中&#xff0c;少不了会进行不同服务之间的相互调用&#xff0c;比如A服务要调用B服务中的接口&#xff0c;如何简单方便的实现呢&#xff1f;fegin可以来帮助。 1、fegin的基本…...

OpenAI——CLIPs(代码使用示例)

OpenAI——CLIPs(打通NLP与CV) Open AI在2021年1月份发布Contrastive Language-Image Pre-training(CLIP),基于对比文本-图像对对比学习的多模态模型&#xff0c;通过图像和它对应的文本描述对比学习&#xff0c;模型能够学习到文本-图像对的匹配关系。它开源、多模态、zero-s…...

什么样的人更适合创业?那类人创业更容易成功?

创业是一项充满风险和机遇的事业&#xff0c;成功的创业者需要具备一定的素质和能力。那么&#xff0c;什么样的人更适合创业&#xff1f;哪类人创业更容易成功呢&#xff1f;本文将为您介绍几个适合创业的人群和成功创业者的共同特点。 具有创新精神的人 创业需要不断创新&am…...

JavaApi操作ElasticSearch(强烈推荐)

ElasticSearch 高级 1 javaApi操作es环境搭建 在elasticsearch官网中提供了各种语言的客户端&#xff1a;https://www.elastic.co/guide/en/elasticsearch/client/index.html 而Java的客户端就有两个&#xff1a; 不过Java API这个客户端&#xff08;Transport Client&#…...

NFT的前景,元宇宙的发展

互联网的普及和数字技术的广泛应用&#xff0c;成为消费升级的新动力&#xff0c;在不断创造出更好的数字化生活的同时&#xff0c;也改变了人们的消费习惯、消费内容、消费模式&#xff0c;甚至是消费理念&#xff0c;数字经济时代的文化消费呈现出新的特征。 2020年有关机构工…...

C#基础教程20 预处理器指令

文章目录 C#预处理指令教程简介预处理指令格式指令名 参数预处理指令类型条件编译指令if#if 条件表达式宏定义指令总结C#预处理指令教程 简介 预处理指令是在编译代码之前进行的一种处理,可以让程序员在编译前根据需要对代码进行一些修改、调整或者控制。C#语言中的预处理指令…...

【FPGA】Verilog:时序电路设计 | 二进制计数器 | 计数器 | 分频器 | 时序约束

前言&#xff1a;本章内容主要是演示Vivado下利用Verilog语言进行电路设计、仿真、综合和下载 示例&#xff1a;计数器与分频器 ​​ 功能特性&#xff1a; 采用 Xilinx Artix-7 XC7A35T芯片 配置方式&#xff1a;USB-JTAG/SPI Flash 高达100MHz 的内部时钟速度 存储器&#…...

国外SEO策略指南:确保你的网站排名第一!

如果你想在谷歌等搜索引擎中获得更高的排名并吸引更多的流量和潜在客户&#xff0c;那么你需要了解一些国外SEO策略。 下面是一些可以帮助你提高谷歌排名的关键策略。 网站结构和内容优化 谷歌的搜索算法会考虑网站的结构和内容。 因此&#xff0c;你需要优化网站结构&…...

Tik Tok新手秘籍,做好五点可轻松起号

新手做TikTok需要有一个具体的规划布局&#xff0c;如果没有深思熟虑就上手开始的话&#xff0c;很有可能会导致功亏一篑&#xff0c;甚至是浪费时间。因此&#xff0c;想要做好 TikTok&#xff0c;就必须从最基本的运营细节开始&#xff0c;一步一步来&#xff0c;下面为大家分…...

【Linux】网络入门

&#x1f387;Linux&#xff1a; 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看见坚持…...

回溯法——力扣题型全解【更新中】

&#xff08;本文源自网上教程的笔记&#xff09; 回溯基础理论 回溯搜索法&#xff0c;它是一种搜索的方式。 回溯是递归的副产品&#xff0c;只要有递归就会有回溯。 所以以下讲解中&#xff0c;回溯函数也就是递归函数&#xff0c;指的都是一个函数。 回溯法的效率 虽然…...

【华为机试真题详解 Python实现】分奖金【2023 Q1 | 100分】

文章目录 前言题目描述输入描述输出描述示例 1题目解析参考代码前言 《华为机试真题详解》专栏含牛客网华为专栏、华为面经试题、华为OD机试真题。 如果您在准备华为的面试,期间有想了解的可以私信我,我会尽可能帮您解答,也可以给您一些建议! 本文解法非最优解(即非性能…...

netlink进行网卡重命名

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/socket.h> #include <linux/if.h> #include <linux/netlink.h>#define MAX_PAYLOAD 1024 // 最大负载长…...

2023年春【数据分析与挖掘】文献精读(一)-1:针对COVID-19,使用聚类方法有效提取生物特性关联进而识别预防COVID-19的药物

分享给大家——动漫《画江湖之不良人》第四季片尾,主人公 李星云所说的一段话: 悠悠众生,因果循环,大道至简,世间若尽是不如意事, 越是执着,便越是苦,不如安下心来,看该看的风景,做好该做之事。 初行娆疆,所悟如此, 就像曾经有一位紫衣姑娘,第一次来中原时,一样…...

【Go自学第三节】Go的范围(Range)用法

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值&#xff0c;在集合中返回 key-value 对。 在讲Go语言的range之前&#xff0c;我们先回顾下Python中range的用法 for i …...

【备战面试】每日10道面试题打卡-Day6

本篇总结的是计算机网络知识相关的面试题&#xff0c;后续也会更新其他相关内容 文章目录1、HTTP 与 HTTPS 有哪些区别&#xff1f;2、HTTPS的加密过程是什么&#xff1f;3、GET与POST有什么区别&#xff1f;4、讲讲HTTP各个版本的区别&#xff1f;5、HTTP与FTP的区别&#xff…...

Stable Diffusion 个人推荐的各种模型及设置参数、扩展应用等合集(不断更新中)

一、说明 | 表示或者 表示 以上 二、模型 适用风景、房子、车子等漫画类风格 模型的VAE不要用模型附带的&#xff0c;好像就是naifu的官方vae&#xff0c;很老了&#xff0c;用 vae-ft-mse-840000-ema-pruned.ckpt 或者是 kl-f8-anime2.ckpt&#xff1b; 嵌入模型要下载作者…...

Salesforce 2023财年逆风增长,现金流达历史最高!

在过去的一年里&#xff0c;Salesforce一直是华尔街最关注的公司之一。3月1日&#xff0c;CRM领域的全球领导者Salesforce公布了截至2023年1月31日的第四季度和整个财年的业绩。 Salesforce主席兼首席执行官Marc Benioff表示&#xff1a; Salesforce全年实现了314亿美元的收入…...

2023年3月全国数据治理工程师认证DAMA-CDGA/CDGP考试怎么通过?

弘博创新是DAMA中国授权的数据治理人才培养基地&#xff0c;贴合市场需求定制教学体系&#xff0c;采用行业资深名师授课&#xff0c;理论与实践案例相结合&#xff0c;快速全面提升个人/企业数据治理专业知识与实践经验&#xff0c;通过考试还能获得数据专业领域证书。 DAMA认…...