Webserver(1): C++实现线程池

在实现线程池之前,首先对线程池中所需要用到的互斥锁、条件变量和信号量进行了简单的封装。

互斥锁、条件变量和信号量封装

locker.h头文件如下(已详细注释)

/*
这里面对互斥锁,条件变量和信号量进行了封装
保证工作队列的线程同步与数据安全
*/#ifndef LOCKER_H
#define LOCKER_H
/*
这是一个简单的C或C++头文件保护(header guard)机制,用于防止头文件被多次包含(include)。
#ifndef LOCKER_H:#ifndef是预处理指令,用于检查LOCKER_H这个宏是否已经定义。如果LOCKER_H没有被定义,那么后面的代码(直到#endif)会被编译器包含(include)。
#define LOCKER_H:这行代码定义了一个宏LOCKER_H。一旦这个宏被定义,再次遇到#ifndef LOCKER_H时,由于LOCKER_H已经被定义,所以其后的代码不会被再次包含。
#endif:这是结束#ifndef预处理的指令。
这种机制确保了在同一个编译单元中,头文件只被包含一次,避免了由于多次包含同一个头文件而可能导致的各种问题,如重复定义、多重继承等。
*/#include <pthread.h>
#include <exception>   //异常处理
#include <semaphore.h> //信号量
#include <stdexcept>   //std::runtime_error 是定义在 <stdexcept> 头文件中的一个异常类//可以使用互斥量(mutex 是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。
//任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败
//一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。
/*初始化互斥量后,你可以使用 pthread_mutex_lock 函数来锁定互斥量,使用 pthread_mutex_unlock 函数来解锁互斥量。锁定互斥量的线程将独占对共享资源的访问,直到它解锁该互斥量。其他尝试锁定该互斥量的线程将被阻塞,直到互斥量被解锁。* */
//1.互斥锁类,应该确保一个线程在访问资源的时候,另外的线程不能同时访问这些资源
class Locker{
public://1.1 构造函数,对互斥量进行初始话Locker(){//这段代码确实是在检查互斥量是否被成功初始化,并在初始化失败时抛出异常。成功初始化返回0if(pthread_mutex_init(&m_mutux, NULL) != 0){throw std::runtime_error("Failed to initialize mutex");}}//1.2 析构函数,对互斥量进行消耗~Locker(){pthread_mutex_destroy(&m_mutux);}//1.3 上锁函数bool lock(){return pthread_mutex_lock(&m_mutux) == 0;  //上锁成功返回0}//1.4 解锁函数bool unlock(){return pthread_mutex_unlock(&m_mutux) == 0;}//1.5 get函数获取互斥量pthread_mutex_t * get(){return &m_mutux;}/** 在C++中,pthread_mutex_t 是一个结构体类型,通常用于POSIX线程编程中的互斥量。* 当你通过函数返回一个 pthread_mutex_t 类型的值时,你实际上是在返回这个结构体的一个副本。* 然而,对于互斥量这样的类型,返回其副本通常是没有意义的,因为互斥量的状态(如锁定或未锁定)不能通过简单地复制结构体来传递。因此,当你想从一个函数返回一个互斥量以便在其他地方使用时,通常会返回指向互斥量的指针。这样,调用者可以通过这个指针来操作原始的互斥量对象,而不是它的一个副本。* */
private:pthread_mutex_t m_mutux;  //互斥量
};//2. 条件变量类
/*条件变量(Condition Variables)是线程同步的一种机制,它允许一个或多个线程等待某个条件成立,或者在某个条件成立后唤醒一个或多个等待的线程。条件变量通常与互斥锁(Mutex)一起使用,以避免竞争条件和保证线程安全。条件变量的类型 pthread_cond_tint pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);等待,调用了该函数,线程会阻塞。当这个函数调用阻塞等待的时候,会对互斥锁进行解锁,否则生产者拿不到互斥锁。解除阻塞时,重新加锁int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);- 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。int pthread_cond_signal(pthread_cond_t *cond);- 唤醒一个或者多个等待的线程int pthread_cond_broadcast(pthread_cond_t *cond);- 唤醒所有的等待的线程*//*条件变量(Condition Variable)是操作系统提供的一种线程间同步机制,用于在多线程环境中实现线程的等待和唤醒操作。它通常与互斥锁(Mutex)结合使用,用于实现复杂的线程同步。条件变量的原理如下:线程在进入临界区前先获取互斥锁。当某个条件不满足时,线程调用条件变量的等待(wait)函数,
并释放之前获取到的互斥锁,然后进入阻塞状态等待被唤醒。当其他线程满足了该条件时,调用条件变量的通知或广播(broadcast)函数来唤醒一个或多个等待中的线程。被唤醒的线程重新获得互斥锁,并检查条件是否满足。如果满足,则继续执行;如果不满足,则再次进入等待状态。条件变量的作用是用于多线程之间关于共享数据状态变化的通信。当一个动作需要另外一个动作完成时才能进行,
即:当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量。操作系统是主动调用者,而条件变量其实是操作系统预留出的接口。
因而这里主要是去考虑记录谁在等待、记录谁要唤醒、如何唤醒的问题。条件变量是一种等待机制,每一个条件变量对应一个等待原因与等待队列。* */
class Cond{
public://2.1 构造函数,初始化Cond(){if(pthread_cond_init(&m_cond, NULL) != 0){throw std::runtime_error("Failed to initialize Condition Variables");}}//2.2 析构函数~Cond(){pthread_cond_destroy(&m_cond);}//2.3 条件变量要配合互斥锁使用,因此需要传递一个互斥锁指针类型bool wait(pthread_mutex_t * mutex){return pthread_cond_wait(&m_cond, mutex) == 0;}//2.4 timewait,还要传递一个时间tbool timewait(pthread_mutex_t * mutex, struct timespec t){return pthread_cond_timedwait(&m_cond, mutex, &t) == 0;}//2.5 唤醒一个或者多个等待的线程bool signal(){return pthread_cond_signal(&m_cond) == 0;}//2.6 唤醒所有的等待的线程bool broadcast(){return pthread_cond_broadcast(&m_cond) == 0;}private:pthread_cond_t m_cond;
};//3. 信号量类
/*信号量(Semaphore)是一种用于控制多个线程或进程对共享资源访问的同步机制。
它可以看作是一个计数器,用于表示可用资源的数量。信号量的主要操作包括P操作(等待)和V操作(释放)。P操作(Wait):当一个线程或进程需要访问共享资源时,它首先会执行P操作。这个操作会将信号量的值减1,
表示一个资源被占用。如果信号量的值大于0,表示还有可用资源,
线程或进程可以继续执行;如果信号量的值为0,表示没有可用资源,线程或进程将被阻塞,直到有资源可用。V操作(Signal):当一个线程或进程完成对共享资源的访问后,它会执行V操作。这个操作会将信号量的值加1,
表示一个资源被释放。如果有其他线程或进程正在等待该资源(即被P操作阻塞),那么它们将被唤醒并继续执行。信号量的类型 sem_tint sem_init(sem_t *sem, int pshared, unsigned int value);- 初始化信号量- 参数:- sem : 信号量变量的地址- pshared : 0 用在线程间 ,非0 用在进程间- value : 信号量中的值,生产+1,消费-1int sem_destroy(sem_t *sem);- 释放资源int sem_wait(sem_t *sem);- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)int sem_trywait(sem_t *sem);int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);int sem_post(sem_t *sem);- 对信号量解锁,调用一次对信号量的值+1int sem_getvalue(sem_t *sem, int *sval);信号量的工作原理基于两种基本操作:P(等待)操作和V(发送信号)操作。P操作用于获取信号量,即减少信号量值。如果信号量值大于0,表示资源可用,进程或线程可以访问该资源,并将信号量值减1;如果信号量值等于0,表示资源已被占用,进程或线程需要等待其他进程或线程释放资源,并将自己挂起,直到信号量值变为正数。V操作用于释放信号量,即增加信号量值。如果有进程或线程正在等待该信号量,则唤醒其中一个进程或线程,使其继续执行。
*/
class Sem{
public:Sem(){if(sem_init(&m_sem, 0, 0) != 0){throw std::runtime_error("Failed to initialize Semaphore");}}~Sem(){sem_destroy(&m_sem);}// int sem_wait(sem_t *sem); - 调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)bool wait(){return sem_wait(&m_sem) == 0;}// int sem_post(sem_t *sem); - 调用一次对信号量的值+1bool post(){return sem_post(&m_sem) == 0;}private:sem_t m_sem;  //信号量
};#endif //LOCKER_H

2.线程池

线程池是一种用于管理和重用线程并发编程技术。在软件开发中,线程池被用来处理大量的并发任务,以提高系统性能和资源利用率。

主要的组成部分包括:

  1. 线程池管理器(Thread Pool Manager):负责创建、销毁和管理线程池中的线程。它通常提供了添加任务、删除任务、调整线程池大小等接口,用于管理线程池的状态。

  2. 工作队列(Work Queue)用于存储需要执行的任务。当有任务需要执行时,线程从工作队列中获取任务并执行。工作队列可以是有限大小的队列,用于控制系统资源的使用。

  3. 线程池(Thread Pool):包含一组预先创建的线程,这些线程可以重复使用来执行任务。通过维护一组可重用的线程,线程池可以减少线程的创建和销毁开销,提高系统的性能和响应速度。

  4. 任务(Task):需要在线程池中执行的工作单元。任务可以是任意类型的计算、I/O 操作或其他类型的工作。

线程池的工作流程通常如下:

  • 初始时,线程池会创建一定数量的线程,并将它们置于等待状态
  • 当有任务需要执行时,任务被添加到工作队列中。
  • 线程池中的线程会不断地从工作队列中获取任务,并执行这些任务。
  • 执行完任务后,线程会再次回到等待状态,等待下一个任务的到来
  • 线程池不再需要时,可以销毁线程池中的线程,释放资源

线程池的优势在于:

  • 降低线程创建和销毁的开销。通过重用线程,减少了频繁创建和销毁线程的性能开销。
  • 控制并发线程数量。线程池可以限制同时执行的线程数量,防止系统资源被过度占用。
  • 提高系统响应速度。通过并发执行多个任务,可以提高系统的并发处理能力和响应速度。

在C++中,this 是一个特殊的指针,它指向调用成员函数的对象。当你在一个类的非静态成员函数中使用 this 时,它实际上指向调用该函数的实例。

this 指针允许你访问对象的所有成员,包括私有(private)和保护(protected)成员。

以下是 this 指针的一些关键点:

  1. 隐含传递:当你调用一个类的非静态成员函数时,this 指针会自动作为第一个参数传递给该函数。虽然你不需要显式地传递它,但在函数内部,你可以使用 this 来引用调用该函数的对象。

  2. 类型this 指针的类型是指向类类型的指针。例如,如果你有一个名为 MyClass 的类,那么 this 的类型就是 MyClass*

  3. 使用场景this 指针通常用于以下情况:

    • 当成员函数的参数名和类的成员变量名相同时,为了避免歧义,可以使用 this 指针来明确指代类的成员变量。
    • 当你想在成员函数中返回对象本身(通常用于链式操作)时,可以使用 return *this;
    • 在某些情况下,你可能想将 this 指针传递给其他函数或方法。
      class MyClass {  
      public:  int value;  MyClass(int val) : value(val) {}  // 使用 this 指针来访问和修改成员变量  void setValue(int newVal) {  this->value = newVal; // this-> 是可选的,但在某些情况下可以帮助提高代码的可读性  }  // 返回对象本身,以便进行链式操作  MyClass* incrementValue() {  this->value++;  return this;  }  
      };  int main() {  MyClass obj(10);  obj.setValue(20);  obj.incrementValue()->incrementValue(); // 链式操作  return 0;  
      }

线程池代码threadpool.h

//线程池的实现
#ifndef THREADPOOL_H
#define THREADPOOL_H#include <pthread.h>
#include <list>
#include "locker.h"
#include <exception>
#include <cstdio>
#include <stdexcept>
#include <iostream>
using namespace std;//定义成模板类,是为了代码的复用,
//任务可能是不同的,T就是任务类
template<typename T>
class Threadpool{
public://1 构造函数,初始化线程数量, 请求队列中最多允许的,等待处理的请求数量Threadpool(int thread_number = 8, int max_requests = 10000);//2 析构函数~Threadpool();//3 向工作队列中去添加任务,append方法,类型为Tbool append(T * request);private://静态函数,不能访问非静态的成员变量,线程所要执行实现的功能static void* worker(void* arg);/*** */void run();private://1 线程的数量int m_thread_number;//2 线程池数组,存储创建线程的pid,大小与线程数量一致pthread_t * m_threads;//3 工作队列中最多允许的,等待处理的请求数量int m_max_requests;//4 工作队列std::list<T*> m_workqueue;/** 内存管理:使用指针允许你更灵活地管理内存。例如,如果你有一个大型对象或动态分配的对象,* 将其存储在std::list<T>中可能会导致不必要的内存复制,因为std::list在插入和删除元素时可能需要重新分配内存。* 使用指针可以避免这种复制,因为实际上你只是在复制指针(一个小的内存地址),而不是整个对象。* *///5 互斥锁Locker m_queue_mutex;//6 信号量用来判断是否有任务需要处理Sem m_queue_sem;//7 是否结束线程bool m_stop;
};//1 构造函数的类外初始化,在这个里面要创建出来线程
template<typename T>
Threadpool<T>::Threadpool(int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests),
m_stop(false), m_threads(nullptr)
{//1. 参数是否正确的判断if(thread_number <= 0 || max_requests <= 0){throw std::runtime_error("Failed to initialize Threadpool");}//2. 根据线程的数量创建出线程池数组,存储创建线程的pid,析构的时候需要销毁m_threads = new pthread_t[thread_number];if(!m_threads){throw std::runtime_error("m_threads Error");}//3. 创建thread_number,线程pid存储在m_threads中,并设置为线程分离for(int i = 0; i < thread_number; i++){cout<<"create" << i << " th thread"<<endl;//线程执行的代码在worker中,是个静态函数,创建的时候,并没有显示指定存储子线程的tid的变量,而是直接放在数组中if(pthread_create(m_threads + i, NULL, worker, this) != 0){//子线程创建失败,删掉这个数组m_threadsdelete[] m_threads;/** 在C++中,delete 和 delete[] 是用于释放动态分配的内存的运算符,但它们的使用场景有所不同。* delete:用于释放通过 new 运算符单个分配的对象。delete[]:用于释放通过 new[] 运算符分配的对象数组。* */throw std::runtime_error("pthread_create Error");}//设置线程分离if(pthread_detach(m_threads[i]) != 0){delete[] m_threads;throw std::runtime_error("pthread_detach Error");}}
}//2 析构函数
template<typename T>
Threadpool<T>::~Threadpool()
{delete[] m_threads;m_stop = true;
}//3 向工作队列中去添加任务,append方法,类型为T,并且需要确保线程同步
template<typename T>
bool Threadpool<T>::append(T *request)
{//1 上锁m_queue_mutex.lock();//2 如果工作队列中的大小大于最大的工作队列中最多允许的,等待处理的请求数量,解锁,返回错误,处理不了了if(m_workqueue.size() > m_max_requests){m_queue_mutex.unlock();return false;}//3 向工作队列中添加m_workqueue.push_back(request);m_queue_mutex.unlock(); //解锁m_queue_sem.post(); //信号量增加,说明工作队列中有了新任务
}//4 线程执行的代码在worker中,是个静态函数
/*
静态函数,它不能访问非静态的成员函数
if (pthread_create(m_threads + i, NULL, worker, NULL) != 0);
在创建线程的时候
if (pthread_create(m_threads + i, NULL, worker,this) != 0); this代表本类对象,是Threadpool类型对象* */
template<typename T>
void* Threadpool<T>::worker(void * arg)
{Threadpool * pool = (Threadpool *) arg;pool->run();return pool;
}//运行函数run,在工作队列中去任务,做任务
template<typename T>
void Threadpool<T>::run()
{while(!m_stop){//工作队列中的信号量-1,如果为0则阻塞在这m_queue_sem.wait();//加锁m_queue_mutex.lock();//如果工作队列为空就解锁if(m_workqueue.empty()){m_queue_mutex.unlock();continue;}//取第一个任务T* request = m_workqueue.front();m_workqueue.pop_front();m_queue_mutex.unlock();if(!request){continue;}request->process(); //调用process函数执行任务}}#endif //THREADPOOL_H/** C++中的静态函数* 在C++中,静态成员函数(Static Member Functions)是类的一部分,但它们与类的实例(对象)无关。* 与类关联,而非对象关联:静态成员函数属于类本身,而不是类的某个特定对象。因此,它们可以在没有创建类对象的情况下被调用。* 访问限制:静态成员函数只能直接访问静态成员变量和其他静态成员函数,不能访问类的非静态成员变量和非静态成员函数,除非通过类的实例或指针/引用。* 不隐藏this指针:静态成员函数不接收this指针,因此它们不能访问类的非静态成员,因为这些成员需要通过this指针来访问。* 调用方式:可以通过类名和作用域解析运算符::来调用静态成员函数,也可以通过类的对象来调用(尽管这样做并不常见)。
静态成员函数不能直接访问类的非静态成员变量,因为静态成员函数不与类的任何特定实例关联,而非静态成员变量是与类的实例关联的。但是,有一些方法可以间接地访问非静态成员变量:通过参数传递:你可以将非静态成员变量的引用或指针作为参数传递给静态成员函数。
这样,静态成员函数就可以通过这个参数来访问和修改非静态成员变量。通过类的实例:如果静态成员函数能够获得类的某个实例的引用或指针,那么它可以通过这个实例来访问非静态成员变量。
这通常是通过将实例作为参数传递给静态成员函数来实现的。* */

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

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

相关文章

微服务架构的流行框架之:Dubbo Spring Cloud

Dubbo Dubbo是一个高性能、轻量级的开源Java RPC&#xff08;Remote Procedure Call&#xff0c;远程过程调用&#xff09;框架&#xff0c;由阿里巴巴开发并贡献给了Apache基金会&#xff0c;成为Apache的顶级项目。Dubbo提供了RPC通信和服务治理的解决方案&#xff0c;使得构…

深入理解C语言中的变量和常量

变量和常量 1. 前言2. 预备知识2.1 printf和%d2.2 \n2.3 scanf2.4 scanf在vs中报错2.5 extern2.6 数组的初始化 3. 变量和常量的区别4. 变量4.1 定义变量的方法4.2 变量的分类4.2.1 局部优先 4.3 变量的使用4.4 变量的作用域4.4.1 局部变量的作用域4.4.2 全局变量的作用域 4.5 …

机器学习|KNN和Kmeans

KNN和Kmeans KNN KNN-K个最近的邻居&#xff0c;而K是可人先预设出来的。 所谓近朱者赤&#xff0c;近墨者黑。 可以选取离当前最近的K个样本来作为辅助判断&#xff0c;因为本样本和最近的K个样本应该是处于一种相似的状态。 以下是一个苹果和梨的识别任务。 图上会出现一个未…

深入Spring Boot核心技术:代码实战解析

第一章&#xff1a;揭开Spring Boot自动配置的面纱 自动配置原理 让我们首先通过一个简化的Spring Boot应用启动类来直观感受自动配置的工作原理&#xff1a; java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.Sprin…

【软件设计师】通俗易懂的去了解算法的时间复杂度

&#x1f413; 时间复杂度 常用排序的时间复杂度 时间频度 算法需要花费的时间&#xff0c;和它语句执行的次数是成正比的&#xff0c;所以会把一个算法种语句执行次数称为语句频度和时间频度、记作T(n)。 定义 时间复杂度就是找到一个无限接近时间频度T(n)同数量级的函数&am…

小脑萎缩患者生活指南:守护你的每一步

亲爱的读者朋友们&#xff0c;今天我们要聊一聊一个特殊但非常重要的群体——小脑萎缩患者。在这个充满挑战的旅程中&#xff0c;我们将一起探索如何用爱和智慧为患者打造一个更加安全、舒适的生活环境。 小脑萎缩是指小脑细胞逐渐减少&#xff0c;导致小脑体积缩小的一种病症…

全量知识系统问题及SmartChat给出的答复 之16 币圈生态链和行为模式

Q.42 币圈生态链和行为模式 我认为&#xff0c;上面和“币”有关的一系列概念和技术&#xff0c;按设计模式的划分 &#xff0c;整体应该都属于行为模式&#xff0c;而且应该囊括行为模式的所有各个方面。 而行为又可以按照三种不同的导向&#xff08;以目的或用途为导向、过…

互联网摸鱼日报(2024-03-04)

互联网摸鱼日报(2024-03-04) 36氪新闻 Sora来了&#xff0c;你又焦虑了吗&#xff1f; 最前线&#xff5c;安踏首家球鞋集合店落地北京三里屯 一位中国遥感科学家&#xff0c;决定“跨界”拯救濒危动物野骆驼 | 最前线 本周双碳大事&#xff1a;工信部等七部门发文推动制造…

mirthConnect忽略HTTPS SSL验证

mirthConnect SSL忽略验证 1、下载https网站证书 点击不安全---->证书无效 2、查看mirth 秘钥库口令 在mirthConnect 的conf目录下面keystore.storepass 3、导入证书到本地 在jdk的bin目录下面执行 keytool -importcert -file "下载的网站证书路径" -keysto…

LeetCode每日一题【c++版】- leetcode 225.用队列实现栈

题目描述 请你仅使用两个队列实现一个后入先出&#xff08;LIFO&#xff09;的栈&#xff0c;并支持普通栈的全部四种操作&#xff08;push、top、pop 和 empty&#xff09;。 实现 MyStack 类&#xff1a; void push(int x) 将元素 x 压入栈顶。int pop() 移除并返回栈顶元素…

Python中按指定数量分割列表字符串的方法

引言 处理列表数据时&#xff0c;有时我们需要将一个包含长字符串的列表分割成按照特定长度的小字符串的多个列表。这在文本处理、批量数据处理或者当我们需要将数据分块进行并行处理时非常常见。Python作为一个强大的编程语言&#xff0c;提供了很多方便的方法来实现这一功能…

CV论文--2024.3.4

1、Deep Networks Always Grok and Here is Why 中文标题&#xff1a;深度网络总是让人摸不着头脑&#xff0c;原因如下 简介&#xff1a;本文探讨了深度神经网络&#xff08;DNN&#xff09;中一种称为"延迟泛化"或"Grokking"的现象。在接近零的训练误差…

使用ssh密钥提交、拉取代码的介绍

网络世界中的数据并不安全 网络中无时无刻有大量的数据传输&#xff0c;传输过程中需要经过各种网络设备和物理媒介你的数据可能会在传输的某一个环节被一个“中间人”拦截&#xff0c;造成泄密&#xff0c;甚至会篡改你的数据&#xff0c;让你发出错误的信息 SSH 为 Secure …

MySQL 5.5、5.6、5.7的主从复制改进

主从复制面临的问题 MySQL一直以来的主从复制都是被诟病,原因是: 1、主从复制效率低 早期mysql的复制是通过将binlog语句异步推送到从库。从库启动一个IO线程将接收到的数据记录到relaylog中;另外启动一个SQL线程负责顺序执行relaylog中的语句实现对数据的拷贝。 这里的…

如何用Elementor创建WordPress会员网站

在下面的文章中&#xff0c;我们将向您展示如何使用Elementor和MemberPress在WordPress中轻松构建会员网站。这篇文章将涵盖WordPress会员网站设置过程、会员资格和受保护内容创建、重要页面和登录表单设计、电子邮件通知管理、报告等。 目录 什么是WordPress会员网站&#x…

【go从入门到精通】go基本类型和运算符用法

大家好&#xff0c;这是我给大家准备的新的一期专栏&#xff0c;专门讲golang&#xff0c;从入门到精通各种框架和中间件&#xff0c;工具类库&#xff0c;希望对go有兴趣的同学可以订阅此专栏。 --------------------------------------------------------------------------…

与字体有关的CSS

隐藏多余字体 text-overflow: ellipsis &#xff08;多余文本显示省略号&#xff09; 需要配合overflow使用 -webkit-box-orient: vertical; display: -webkit-box; -webkit-line-clamp: number &#xff08;超出多少行显示省略号&#xff09; 强制显示一行 whi…

.NET高级面试指南专题十四【 观察者模式介绍,最常用的设计模式之一】

简介&#xff1a; 观察者模式&#xff08;Observer Pattern&#xff09;是一种行为型设计模式&#xff0c;其目的是定义了一种一对多的依赖关系&#xff0c;当一个对象的状态发生变化时&#xff0c;所有依赖于它的对象都会得到通知并自动更新。 原理&#xff1a; 在观察者模式中…

从零开始搭建web组态

成果展示&#xff1a;by组态[web组态插件] 一、技术选择 目前只有两种选择&#xff0c;canvas和svg Canvas: 是一个基于像素的渲染引擎&#xff0c;使用JavaScript API在画布上绘制图像&#xff0c;它的优点包括&#xff1a; Canvas渲染速度快&#xff0c;适合处理大量图像和…

TIOBE 2024榜单启示:程序员如何把握未来编程趋势与机遇

程序员如何选择职业赛道&#xff1f; 程序员的职业赛道就像是一座迷宫&#xff0c;有前端的美丽花园&#xff0c;后端的黑暗洞穴&#xff0c;还有数据科学的神秘密室。你准备好探索这个充满挑战和机遇的迷宫了吗&#xff1f;快来了解如何选择职业赛道吧&#xff01; 方向一…