Linux:多线程的操作

news2024/10/5 21:14:14

多线程操作

  • 进程与线程
  • 线程的创建 create_pthread
    • 创建线程池
    • 给线程传入对象的指针
  • 线程等待 pthread_join
  • 退出线程 pthread_exit
    • 线程等待参数 retval 与 线程退出参数 retval
  • 线程中断 pthread_cancel
  • 获取线程编号 pthread_self
  • 线程分离 pthread_detach

进程与线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位,共享进程的数据,拥有自己的一部分数据

线程私有的属性:线程的ID、一组寄存器(上下文数据)栈(独立的栈结构)、调度优先级

进程的多个线程共享同一块地址空间,对堆区、栈区都是共享的

线程共享进程的资源有:文件描述符表、每种信号的处理方式(默认动作、忽略动作、自定义动作)、当前工作目录

线程的创建 create_pthread

Linux下没有真正意义的线程,而是用进程模拟的线程(LWP)。对此,Linux不会提供直接创建线程的系统调用,只会提供创建轻量级进程的接口

在用户看来会很变扭,进程是进程,线程是线程就要区分开来。

所以出现了用户级线程库 pthread对Linux接口进行封装,给用户提供进行线程控制的接口

pthread 线程库在任何版本的Linux操作系统都会存在, pthread也被称为原生线程库)

可以通过 man 的3号手册来查看线程库的使用,这里不作演示

接下来介绍一些线程库的接口使用:

使用原生线程库需要包含头文件:#include <pthread>

  • 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t* attr,
					void* (*start_routine)(void*), void* arg);

pthread_create 函数参数介绍

thread:线程 id 地址,pthread_t 为无符号整数
attr:线程属性(线程优先级)
start_routine:函数指针,执行对应的函数功能(可以对函数进行传参),也被称为回调函数
arg:是指向任意数据的指针,将参数传递给 start_routine 函数
返回值:线程创建成功返回0,失败错误码被设置

示例:

#include <iostream>
#include <pthread>
#include <unistd.h>

void* thread_run(void* arg)
{
	while(true)
	{
		std::cout << "new thread running" << std::endl;
		sleep(1);
	}
	return nullptr;
}

int main()
{
	pthread_t t;
	pthread_create(&t, nullptr, thread_run, nullptr);//创建线程,t是输出型参数
	
	//主进程
	while(true)
	{
		std::cout << "main thread running, new thread id:" << t << std::endl;
		sleep(1);
	}
	
	return 0;
}

上面代码直接编译的话会出现链接报错,这是因为这个多线程是一个库,直接编译 g++ 会找不到这个库,需要指定编译器去找线程库。
在这里插入图片描述

对此,在编译时,使用 g++ 进行编译要加上 -lpthread 选项

g++ -o threadTest threadTest .c -std=c++11 -lpthread

在这里插入图片描述

可以通过 ldd 对编译好的可执行文件来查看线程库的位置:

ldd threadTest

在这里插入图片描述
执行程序可以看到,主线程与子线程同时运行:

在这里插入图片描述

在这里插入图片描述
此时输出的线程id会很大,很奇怪。其实这些线程的id是地址,创建的线程会被线程库管理起来,形成数组,每个对应的线程id 其实就是数组的下标。

创建的线程是不能确定先后顺序的. Linux下的线程是轻量级的进程,进程创建执行的先后顺序是由调度器决定的,对此线程谁先谁后的问题也要看调度器来决定的

创建线程池

下面来创建一个线程池,让每一个线程都执行 thread_run 这个函数,打印对应的创建编号

#include <iostream>
#include <pthread>
#include <unistd.h>

#define NUM 10

void* thread_run(void* arg)
{
	char* name = (char*)arg;
	
	while(true)
	{
		std::cout << "new thread running,thread name is:" << name << std::endl;
		sleep(1);
	}
	return nullptr;
}

int main()
{
	pthread_t tids[NUM];
	for(int i = 0; i < NUM; i++)
	{
		char thname[64];
		snprintf(thname, sizeof(thname), "thread-%d", i + 1);
		pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参
	}
	
	//主进程
	while(true)
	{
		std::cout << "main thread running" << std::endl;
		sleep(1);
	}
	
	return 0;
}

编译运行:

在这里插入图片描述

结果很不对,输出的结果都是一样的。

在给线程回调函数进行传参时,传入的是 thname 地址。thname 字符数组是属于主线程的,属于临时变量。前面提到线程会共享进程中的数据。对此,每个线程都会对这个变量进行读写,导致最终显示的结果都是一样的。

解决方式如下:

对 thname 变量在堆上申请空间,待到回调函数使用完后对这个资源进行释放:

void* thread_run(void* arg)
{
	char* name = (char*)arg;
	
	while(true)
	{
		std::cout << "new thread running,thread name is:" << name << std::endl;
		sleep(1);
	}
	
	delete name; //释放空间
	return nullptr;
}

int main()
{
	pthread_t tids[NUM];
	for(int i = 0; i < NUM; i++)
	{
		char* thname = new char[64]; //堆上开辟空间
		
		snprintf(thname, 64, "thread-%d", i + 1);
		pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参
	}
	//主进程
	while(true)
	{
		std::cout << "main thread running" << std::endl;
		sleep(1);
	}
	
	return 0;
}

编译运行:
在这里插入图片描述
创建线程前,每次对 thname 进行资源申请,回调函数之后对资源进行释放,可以很好的避免资源共享情况发生。从结果也可以看出不同线程的执行先后顺序也是不确定的。

给线程传入对象的指针

创建线程时,不仅仅只可以传入内置类型变量的指针,还可以传入自定义类型变量的指针

示例:构建 ThreadDate 类,其内部包含线程的基本信息。在类中实现输入型参数和输出型参数,方便我们获取线程处理后的数据结果

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <ctime>

#define NUM 3

enum { OK=0, ERROR };

struct ThreadDate
{
	//构造
	ThreadDate(const string& name, pthread_t tid, time_t createTime, size_t top = 0)
		:_name(name), 
		_tid(tid), 
		_createTime((uint64_t)createTime),
		_status(OK),
		_top(top),
		_result(0)
	{}

	~ThreadDate(){}

	//成员变量
	//输入型变量
	string _name;
	pthread_t _tid;
	uint64_t _createTime; //创建时间
	//输出型变量
	int _status; //线程退出状态
	size_t _top;//累加到最大值
	int _result;
};

下面通过实例化这个类,来演示线程中传入对象:

int main()
{
	pthread_t tids[NUM];
	// 创建线程池
	for (int i = 0; i < NUM; i++)
	{
		char *thname = new char[64];
		snprintf(thname, 64, "thread-%d", i + 1);
		//定义ThreadDate类,传入到线程中
		ThreadDate* tdate = new ThreadDate(std::string(thname), i+1, time(nullptr), (100+ i * 5));
		pthread_create(tids + i, nullptr, thread_run, tdate); //将tdate对象进行传参
	}

	void *ret = nullptr; // 用于保存子线程退出的信息

	for (size_t i = 0; i < NUM; i++)
	{
		int n = pthread_join(tids[i], &ret); //传入ret指针的地址
		if(n != 0) std::cerr << "pthread_join error" << std::endl;

		ThreadDate* td = static_cast<ThreadDate*>(ret); //指针类型转换
		if(td->_status == OK) //输出对象内容
			std::cout << td->_name << " 计算的结果是: " << td->_result << " (它要计算的是[1, " << td->_top << "])" << std::endl;
		
		//释放资源
		delete td;
	}
	return 0;
}

在这里插入图片描述

线程等待 pthread_join

上面实现的代码中,我们将主线程用死循环的方式,一直维持进程的运行。

如果去掉死循环,线程还能继续执行下去吗?

对上面的代码进行修改:在线程被创建后,维持 3 秒后主进程退出

int main()
{
	pthread_t tids[NUM];
	for(int i = 0; i < NUM; i++)
	{
		char* thname = new char[64]; //堆上开辟空间
		
		snprintf(thname, 64, "thread-%d", i + 1);
		pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参
	}
	
	//主进程
	sleep(3);
	
	return 0;
}

在这里插入图片描述

进程是资源的申请的主体,进程退出了,不管子进程还在进行什么操作都会终止运行

这样会造成什么后果?

会造成资源泄漏,如果此时的线程在堆区申请了资源还没来得及释放,会导致内存泄漏。

线程与子进程一样,线程退出后需要被回收处理。就拿子进程来说,当子进程退出后会处于僵尸状态,父进程如果没有等待子进程,对子进程的僵尸状态进行回收的话会造成资源的泄漏

有僵尸进程,但是有没有僵尸线程一说。与进程相似,线程退出后也会处于一种被回收的状态,没有及时回收线程的话,也会造成内存泄漏

对此,线程退出是需要进行等待的

下面来介绍一个函数接口:pthread_join 等待线程

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

参数介绍:

thread:等待的线程 id 号
retval:是一个指向指针的指针,用于存储被等待线程的返回值
返回值:等待成功返回0,失败错误码被返回

对上面的代码进行修改,写一个等待进程的版本:

void* thread_run(void* arg)
{
	char* name = (char*)arg;
	while(true)
	{
		std::cout << "new thread running,thread name is:" << name << std::endl;
		sleep(1);
	}
	delete name; //释放空间
	return nullptr;
}

int main()
{
	pthread_t tids[NUM];
	for(int i = 0; i < NUM; i++)
	{
		// char thname[64];
        char* thname = new char[64];
		// snprintf(thname, sizeof(thname), "thread-%d", i + 1);
		snprintf(thname, 64, "thread-%d", i + 1);
		pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参
	}

	for(size_t i = 0; i < NUM; i++)
	{
		pthread_join(tids[i], nullptr);//等待线程
	}

	return 0;
}

有了线程等待,可以很好的避免内存泄漏。主进程会等待所有的子线程,只有当所有的线程都退出后才会结束整个程序的运行。

退出线程 pthread_exit

如何控制线程的退出呢?

这里还是拿进程来说,也比较好举例(前面也说过线程是轻量级的进程)。进程退出的方式可以在main函数中使用 return 语句、在任意行代码处调用 exit 函数。

那么线程可以使用类似的方法吗?

先来看看 return 语句的作用,还是拿刚刚编写的代码来举例。这里我们直接往死循环内部编写 3 秒的停顿,之后直接执行break 语句,后续执行 return 语句。为了方便展示,下面只展示修改的代码:

void* thread_run(void* arg)
{
	char* name = (char*)arg;
	while(true)
	{
		std::cout << "new thread running,thread name is:" << name << std::endl;
		sleep(3);
		break;  //跳出循环
	}
	delete name; 
	return nullptr;
}

编译运行,来看看执行结果:

在这里插入图片描述
所有的线程都会打印一次,然后停顿卡住,到执行 return 语句后所有的线程都会退出。执行的效果也是符合我们的预期的。

下面来使用 exit 函数来测试线程退出情况,还是上面的代码,将 break 语句换成 exit 函数

void* thread_run(void* arg)
{
	char* name = (char*)arg;
	
	while(true)
	{
		std::cout << "new thread running,thread name is:" << name << std::endl;
		exit(10); //调用exit函数
	}

    delete name; //释放空间
	return nullptr;
}

下面来看看现象:
在这里插入图片描述

线程池只创建了一部分,然后直接终止了运行。在右边监视 threadTest 进程也没有任何显示。

exit 函数退出作用是整个 threadTest 进程,当某一子线程调用了 exit 函数的时候,就会导致整个进程都退出。这也是为什么会只创建了一些子线程,然后导致整个进程都结束运行了。

对此,在线程执行流中,非必要情况下,不要轻易的调用 exit 函数

不能使用 exit 函数,但是线程库中提供了一个API,用于退出某一线程:pthread_exit

void pthread_exit(void *retval);

参数介绍:

retval:指向线程退出状态的指针

当线程调用 pthread_exit 时,它会立即停止执行,并释放其栈空间。但是,线程的资源(如线程ID和线程属性)直到其他线程调用 pthread_join 来回收它时才会被完全释放

示例:

#define NUM 3

void* thread_run(void* arg)
{
	char* name = (char*)arg;
	
	while(true)
	{
		std::cout << "new thread running,thread name is:" << name << std::endl;
		sleep(4);
		break;
	}
    delete name; //释放空间
	
	pthread_exit(nullptr); //退出调用的线程
}

int main()
{
	pthread_t tids[NUM];
	for(int i = 0; i < NUM; i++)
	{
		// char thname[64];
        char* thname = new char[64];
		// snprintf(thname, sizeof(thname), "thread-%d", i + 1);
		snprintf(thname, 64, "thread-%d", i + 1);
		pthread_create(tids + i, nullptr, thread_run, thname);//创建线程池,将thname传参
	}

	//等待线程
	for(size_t i = 0; i < NUM; i++){ pthread_join(tids[i], nullptr); }
	return 0;
}

这里只创建了三个子线程的线程池,来看看运行的效果:
在这里插入图片描述

线程等待参数 retval 与 线程退出参数 retval

先来看看这两个API的接口声明:

int pthread_join(pthread_t thread, void **retval); //线程等待接口
void pthread_exit(void *retval); //线程退出接口

两个函数之间的 retval 参数有关联吗?答案是有的。

一般创建进程都是为了帮助我们去完成某些任务,线程也是如此,创建线程也是为了帮助进程完成一部分任务。进程在完成任务后正常退出,返回对应的退出码。当然,进程完成到一定的任务时也会直接退出。

下面是进程退出的几个情况:

  1. 在 main 函数中调用 return 语句,返回对应的退出码;
  2. 在进程中任意代码处调用 exit 函数。当然调用 exit 函数需要传参,进程退出的退出码也就是传入exit 函数参数的值;
  3. 收到OS的终止信号

进程的退出码、退出信号的返回,是方便我们去查看当前进程是不是完成了指定的任务。线程也是如此,线程退出是否正常我们也要知道。对此,上面提到的 两个 API 接口的参数作用就是用于获取线程退出的退出信息

线程退出接口 pthread_exit 一般是用在回调函数内部,也就是子线程中。我们可以先将 pthread_exit 功能想象成 exit 函数那般,在子线程退出后我们将子线程退出码带出来。

但是问题来了,为什么 pthread_exit 传入的参数是 void* retval 一级指针?

这个要结合 pthread_join 来看:

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

pthread_join 是等待线程的一个接口,会回收退出的子线程(线程的ID、线程的属性等)。pthread_join 的 retval 是一个输出型参数。

这里的 retval 如同在进程中调用的 wait 函数时,传入 status 参数,这个 status 也是输出型参数,会将 子进程的退出码、退出信号带出来。

retval 参数的作用就是将子线程的退出数据带出来,不同的是这里是二级指针。在使用前需要定义一个指针,然后将这个指针的地址传入 pthread_join 的 retval 参数中。在子线程调用 pthread_exit 函数时,传出对应的数据即可。

光说不做,假把戏。下面来看看测试案例:

void *thread_run(void *arg)
{
	char *name = (char *)arg;
	while (true)
	{
		std::cout << "new thread running,thread name is:" << name << std::endl;
		sleep(3);
		break;
	}

	delete name; // 释放空间
	pthread_exit((void*)1); //子线程退出,退出信息设置为1
}

int main()
{
	pthread_t tids[NUM];
	for (int i = 0; i < NUM; i++)
	{
		char *thname = new char[64];
		snprintf(thname, 64, "thread-%d", i + 1);
		pthread_create(tids + i, nullptr, thread_run, thname); // 创建线程池,将thname传参
	}

	void *ret = nullptr; // 用于保存子线程退出的信息

	for (size_t i = 0; i < NUM; i++)
	{
		int n = pthread_join(tids[i], &ret); //传入ret指针的地址
		if(n != 0) std::cerr << "pthread_join error" << std::endl;
		std::cout << "子线程:thread->" << i+1 << ",退出码为:" << (uint64_t)ret << std::endl;
	}
	
	return 0;
}

这里需要注意就是传指针的问题:

定义 ret 一级指针,传参到 pthread_join 内部时,传入的是 ret 指针的地址。pthread_exit 传参需要传入指针类型,对此上面代码需要对 1 进行 void* 类型的强转。在输出子线程退出信息时,ret 是指针,经过子线程的等待,ret内部值已经被设置为了除了低位的第一位为1其他全为 0 的二进制序列,在通过 uint64_t 类型强转即可将数据打印输出!

在这里插入图片描述

还要提一点就是:在获取线程的退出码时,是不需要考虑异常的。如果一个线程中出现了异常,那么就会带动的整个主进程退出。主进程都退出了还需要考虑等待进程的异常吗?是不需要的。对此,在多线程中是不需要考虑异常的!异常问题通常是由进程来考虑。

线程中断 pthread_cancel

在实际开发需求中,如果想要将创建的线程中断运行需要用到 API:pthread_cancel

int pthread_cancel(pthread_t thread);

参数介绍:

thread:传入的线程编号

示例:我们先来创建一个正常线程,再执行一段任务后线程会自动退出:

void* thread_run(void* args)
{
	//静态类型转换
	const char* str = static_cast<const char*>(args);

	int cnt = 5;
	while(cnt)
	{
		cout << str << "is runing :" << cnt-- << endl;
		sleep(1);
	}

	//退出线程
	pthread_exit((void*)1);
}	

int main()
{
	//创建线程
	pthread_t tid;
	pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");

	//等待线程
	void* ret = nullptr;
	pthread_join(tid, &ret);

	return 0;
}

在这里插入图片描述

修改上述代码,在线程执行两秒任务后,直接调用 pthread_cancel 接口,查看现象:

void* thread_run(void* args)
{
	//静态类型转换
	const char* str = static_cast<const char*>(args);

	int cnt = 5;
	while(cnt)
	{
		cout << str << "is runing :" << cnt-- << endl;
		sleep(1);
	}

	//退出线程
	pthread_exit((void*)1);
}	

int main()
{
	//创建线程
	pthread_t tid;
	pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");

	//2秒后,中断线程
	sleep(2);
	pthread_cancel(tid);

	//等待线程
	void* ret = nullptr;
	pthread_join(tid, &ret);

	return 0;
}

在这里插入图片描述
可以看到当线程执行两秒后直接中断

获取线程编号 pthread_self

pthread_t pthread_self(void);

谁调用这个接口就获取谁的线程 id 编号,示例:

void* thread_run(void* args)
{
	//静态类型转换
	const char* str = static_cast<const char*>(args);

	int cnt = 5;
	while(cnt)
	{
		cout << str << "is runing :" << cnt-- << "obtain self id ->" << pthread_self() << endl; //获取线程id
		sleep(1);
	}

	//退出线程
	pthread_exit((void*)1);
}	

int main()
{
	//创建线程
	pthread_t tid;
	pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");
	//等待线程
	void* ret = nullptr;
	pthread_join(tid, &ret);
	cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;
	return 0;
}

在这里插入图片描述

线程分离 pthread_detach

新线程被创建,默认情况下是 joinable 的,线程退出,主进程需要对这个线程进行 pthread_join 操作。不对线程进行等待的操作就会造成内存泄漏,无法释放资源

如果不关心线程的返回值,那么等待就会变成一种负担

就是主线程自己为了等待子线程,难道不用去做自己的事情了吗?这个时候,我们可以告诉OS,当线程退出的时候,自己去释放资源。如何操作呢?需要用到下面这个 API :

int pthread_detach(pthread_t thread);

pthread_detach 功能是将一个线程分离出来,但是要记住一个点:被分离的线程在后续操作是不能被等待的!!如果对被分离的线程进行 pthread_join 操作,主进程是会报错的。报错出现后,就不会再对子线程进行等待操作,直接向后运行属于主进程的代码

线程分离好比现实生活中的:已婚与未婚,是属于一种属性。
线程分离,并不是字面上的意思将线程与进程分离开那种。分离是一种属性,没有被分离的线程,是 joinable 的。该线程需要被等待回收资源;已经被分离的线程,其内部属性会发生变化,表示这个线程不需要再被等待回收资源。

示例:创建一个子线程,在等待子线程之前对该子线程进行分离操作

#include <pthread.h>
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <string>

using namespace std;

void* threadRoution(void* arg)
{
    const char* tname = static_cast<const char*>(arg);
    int cnt = 5;
    while(cnt)
    {
        cout << tname << ":" << cnt-- << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    //创建线程
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoution, (void*)"thread 1");

    //对子线程进行分离操作
    pthread_detach(tid);

    //等待线程
    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    if(n != 0) cerr << "error:" << errno << strerror(n) << endl;
    return 0;
}

编译查看效果:

在这里插入图片描述

主进程在等待子线程时,发现该线程已经被分离。对此,不会再阻塞等待子线程,程序直接向后运行走,子线程也没有机会继续执行对应的功能,整个进程就退出了。

因此,线程分离的主要功能就是将子线程分离出来,让主进程有更多的时间去处理属于自己事情,也不需要对子线程的资源释放与否而担心。

不过在使用线程分离的时候,要注意执行流先后问题,不然会出现奇奇怪怪的现象。

下面来举个例子:在子线程内部去调用本线程的分离

void* threadRoution(void* arg)
{
    //将调用的线程分离开来
    pthread_detach(pthread_self());

    const char* tname = static_cast<const char*>(arg);

    int cnt = 5;

    while(cnt)
    {
        cout << tname << ":" << cnt-- << endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    //创建线程
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoution, (void*)"thread 1");

    int n = pthread_join(tid, nullptr);
    if(n != 0) cerr << "error:" << errno << strerror(n) << endl;

    return 0;
}

在这里插入图片描述

此时会发现,线程正常的跑,主进程也等待成功。

子线程调用分离没有用吗?其实不然,这是由于执行流先后问题

子线程被创建出来之前,主进程就执行到了 pthread_join 代码处,子线程还没有来得及分离,分离属性没有被修改,造成主进程阻塞等待子线程。对此,就算子线程将自己分离开来,主进程早就处于进行了等待状态,也就造成了子线程继续往后执行的现象。

提示:使用线程分离的接口,尽量在创建线程之后进行调用,防止奇奇怪怪的执行流的问题产生

线程操作就讲到这里,感谢大家的支持!!

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

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

相关文章

【学习笔记】Kali

纯个人总结&#xff0c;有什么不对的地方欢迎指正。 笔记根据个人学习进度持续更新… 一、 认识Kali 基础了解 Kali是一套基于Debian发行版的一款操作系统。&#xff08;这里讲一个误区&#xff0c;我以前一直以为kali就属于deepin下的操作系统&#xff0c;但是我知道deepin是…

记录一下:vue3+antd-vue a-form包含a-table实现表格行内校验, 清除 指定行 指定字段 的校验

问题描述下&#xff1a; 目标VLAN值可以输入&#xff0c; 也可以点击后面的按钮进行弹窗选择。由于检验原因&#xff0c;光标失焦但是没有填写就会校验爆红&#xff0c;然后点击选择之后由于没有失焦过程没有触发校验&#xff0c;依然还是红的。这个时候就需要清除 目标VLAN值 …

企业跨境文件传输的核心痛点,怎样保证稳定可靠的传输性能?

随着经济和国际贸易的发展&#xff0c;企业跨境文件传输日趋频繁&#xff0c;多个行业均存在高频且大量的跨境文件传输需求&#xff1a; 跨境电子商务&#xff1a; 跨境电商平台需要进行跨国的订单、支付和物流信息的传输&#xff0c;以便顺利完成交易和配送。涉及跨国企业的内…

Python爬虫实战(实战篇)—18获取【小红书】首页信息写入Excel(仅用于学习-附完整版代码)

文章目录 专栏导读背景1、分析首页页面2、分析获取信息2-1,获取:笔记类型2-2,获取:标题2-3,获取:用户信息2-4,获取:用户ID2-5,获取:用户头像2-6,获取:文章连接完整代码总结专栏导读 文章名称链接Python爬虫实战(实战篇)—16获取【百度热搜】数据—写入Ecel(附完整…

一篇文章看懂Redission原理

文章目录 ☃️可重入锁原理☃️锁重试和WatchDog机制☃️MutiLock原理 上一篇文章讲解了 Rediision的使用 ,这篇文章讲解其原理 ☃️可重入锁原理 在Lock锁中&#xff0c;他是借助于底层的一个voaltile的一个state变量来记录重入的状态的&#xff0c;比如当前没有人持有这把锁…

Vue配置项之生命周期(组件生命周期)

Lifecycle Hooks 首先说明一下&#xff0c;本人是前端小学生级别的菜鸡&#xff0c;吐槽的话请口下留情&#xff0c;在评论区指出错误或者补充不足&#xff0c;我会很喜欢&#xff0c;互喷不会进步&#xff0c;相互指点才会。。。。谢谢大家啦 目录 Lifecycle Hooks 目录创建…

指针的深入理解(3)(包括数组名的理解、一维数组传参的本质以及指针数组的相关知识及使用)

文章目录 1 数组名的理解2 使用指针访问数组3 一维数组传参的本质4 指针数组5 指针数组的使用 1 数组名的理解 当我们运行以下代码&#xff1a; #include <stdio.h> int main() {int arr[10] { 0 };printf("%p\n", &arr[0]);printf("%p\n", a…

踩坑!被node-sass折磨的一天

文章目录 被node-sass折磨的一天折磨过程了解原因注意事项 被node-sass折磨的一天 折磨过程 起因是要开发一个老项目&#xff0c;照常拉代码、下依赖、启动三步走 依赖开始下载不对了&#xff0c;以为是node版本问题&#xff0c;寻找node-sass对应的node版本 利用nvm&#…

IP地址乱成一团?用Shell一键搞定!

在日常的运维工作中&#xff0c;我们经常需要对各种数据进行处理和分析&#xff0c;其中包括对IP地址的管理和排序。排序后的IP地址列表可以帮助我们更好地进行日志分析、网络流量监控和故障排除。 本文将模拟一个运维场景&#xff0c;展示如何对IP地址进行排序&#xff0c;并探…

云原生应用开发培训,开启云计算时代的新征程

在云计算时代&#xff0c;云原生应用开发技术已经成为IT领域的热门话题。如果您想要转型至云原生领域&#xff0c;我们的云原生应用开发培训将帮助您开启新征程。 我们的课程内容涵盖了云原生技术的基础概念、容器技术、微服务架构、持续集成与持续发布&#xff08;CI/CD&#…

传统零售运营模式有什么缺点?新零售模式下的运营思维好在哪里?

随着科技的进步、消费者需求的多样化以及市场竞争的加剧&#xff0c;传统零售模式已逐渐难以满足现代消费者对购物体验的期待。新零售&#xff0c;作为一个创新的概念&#xff0c;应运而生&#xff0c;它不仅代表着零售行业的未来趋势&#xff0c;更是一种全新的商业思维和运营…

pytest中一个场景测试的demo

注意点1&#xff1a; allure.severity 是一个装饰器&#xff0c;用于设置测试用例的严重性级别。 allure.severity_level.CRITICAL 是Allure提供的严重性级别之一&#xff0c;表示这个测试用例极为重要。allure.severity_level.BLOCKER&#xff1a;阻塞级别的问题&#xff0c…

python发送http请求

python有个内置模块发送请求urllib的request的 openurl()方法&#xff0c;打开一个链接&#xff0c;就是发送一个请求&#xff0c;&#xff0c; 因为内置模块发请求不是那么好用&#xff0c;一般都会使用第三方的请求包&#xff0c;requests模块&#xff0c; 请求头 User-Ag…

Unity基础(一)unity的下载与安装

目录 一:下载与安装 1.官网下载地址 2.推荐直接下载UnityHub 3.选择编辑器版本(推荐长期支持版) 4.在UnityHub安装选择相应的模块 二:创建项目 简介: Unity 是一款广泛应用的跨平台游戏开发引擎。 它具有以下显著特点&#xff1a; 强大的跨平台能力&#xff1a;能将开发的游…

CPI降温仍猛砍降息预期!美联储继续按兵不动,预计今年仅降息一次

要点&#xff1a; 美联储继续保持利率不变&#xff0c;符合市场预期。 决议声明不再说降通胀缺乏进一步进展&#xff0c;改称取得适度的进... 要点&#xff1a; 美联储继续保持利率不变&#xff0c;符合市场预期。 决议声明不再说降通胀缺乏进一步进展&#xff0c;改称取得适度…

Spring Boot集成antlr实现词法和语法分析

1.什么是antlr&#xff1f; Antlr4 是一款强大的语法生成器工具&#xff0c;可用于读取、处理、执行和翻译结构化的文本或二进制文件。基本上是当前 Java 语言中使用最为广泛的语法生成器工具。Twitter搜索使用ANTLR进行语法分析&#xff0c;每天处理超过20亿次查询&#xff1…

【Redis】Redis常见问题——缓存更新/内存淘汰机制/缓存一致性

目录 回顾数据库的问题如何提高 mysql 能承担的并发量&#xff1f;缓存解决方案应对的场景 缓存更新问题定期生成如何定期统计定期生成的优缺点 实时生成maxmemory 设置成多少合适呢&#xff1f;项目类型上来说 新的问题 内存淘汰策略Redis淘汰策略为什么redis要内存淘汰内存淘…

乡村振兴的科技创新引领:加强农业科技研发,推广先进适用技术,提高农业生产效率,助力美丽乡村建设

目录 一、引言 二、农业科技研发的重要性 &#xff08;一&#xff09;提升农业生产效率 &#xff08;二&#xff09;促进农业产业升级 &#xff08;三&#xff09;保障粮食安全 三、加强农业科技研发的策略 &#xff08;一&#xff09;加大投入力度 &#xff08;二&…

9.8k star!一款小而美的开源物联网操作系统:RT-Thread

介绍 RT-Thread是一款主要由中国开源社区主导开发的开源实时操作系统&#xff08;RTOS&#xff09;。它不仅是一个实时操作系统内核&#xff0c;也是一个完整的应用系统&#xff0c;包含了实时、嵌入式系统相关的各个组件&#xff0c;如TCP/IP协议栈、文件系统、libc接口、图形…

Sass实战运用,如何利用好Sass

Sass&#xff08;Syntactically Awesome Stylesheets&#xff09;是一种CSS预处理器&#xff0c;它提供了许多强大的功能&#xff0c;如变量、嵌套规则、混合&#xff08;Mixins&#xff09;、函数等&#xff0c;使得CSS的编写更加高效、灵活和易于维护。以下是关于Sass实战运用…