嵌入式Linux-线程的回收/取消/分离

news2024/9/25 1:17:41

1. 线程的回收

1.1 回收线程的概念

春节七天连假已经过完啦,也该回收一下我们放假的线程了!
听过很多回收旧手机、旧冰箱和旧彩电…,那么回收线程又是什么呢?
在父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;而在线程当中,也需要如此,通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;pthread_join()函数原型如下所示:

#include <pthread.h>

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

参数含义:

参数含义
threadpthread_join()等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程
retval如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到retval 所指向的内存区域;如果目标线程被 pthread_cancel()取消,则将 PTHREAD_CANCELED 放在retval 中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL

调用pthread_join()函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则pthread_join()立刻返回。如果多个线程同时尝试调用pthread_join()等待指定线程的终止,那么结果将是不确定的。

若线程并未分离则必须使用pthread_join()来等待线程终止,回收线程资源;如果线程终止后,其它线程没有调用pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程(这部分如果看不懂的地方可以看一下我博客有关僵尸进程的介绍)。

当然,如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收。

1.2 回收进程与回收线程的区别

我们来比对一下,有关进程回收和线程回收的区别吧,主要也是探讨关于pthread_join()执行的功能类似于针对进程的waitpid()调用:

  1. 线程之间关系是对等的。进程中的任意线程均可调用pthread_join()函数来等待另一个线程的终止。譬如,如果线程A创建了线程B,线程B再创建线程C,那么线程A可以调用pthread_join()等待线程C的终止,线程C也可以调用pthread_join()等待线程A的终止;这与进程间层次关系不同,父进程如果使用fork()创建了子进程,那么它也是唯一能够对子进程调用wait()的进程,线程之间不存在这样的关系。
  2. 不能以非阻塞的方式调用pthread_join()。对于进程,调用waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>

static void *new_thread_start(void *arg)
{
	 printf("新线程 start\n");
	 sleep(2);
	 printf("新线程 end\n");
	 pthread_exit((void *)10);
}

int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

主线程调用 pthread_create()创建新线程之后,新线程执行 new_thread_start()函数,而在主线程中调用pthread_join()阻塞等待新线程终止,新线程终止后,pthread_join()返回,将目标线程的退出码保存在*tret 所指向的内存中。测试结果如下:
在这里插入图片描述

2. 取消线程

2.1 取消线程的概念

在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用pthread_exit()退出,或在线程start函数执行return语句退出。
有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。

2.2 取消线程函数

通过调用pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示:

#include <pthread.h>

int pthread_cancel(pthread_t thread);

发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void *)-1)的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制,所以pthread_cancel()并不会等待线程终止,仅仅只是提出请求。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
 	printf("新线程--running\n");
 	for ( ; ; )
	sleep(1);
	return (void *)0;
}

int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

解读程序: 主线程创建新线程,新线程 new_thread_start()函数直接运行 for 死循环;主线程休眠一段时间后,调用pthread_cancel()向新线程发送取消请求,接着再调用 pthread_join()等待新线程终止、获取其终止状态,将线程退出码打印出来。测试结果如下:
在这里插入图片描述
由打印结果可知,当主线程发送取消请求之后,新线程便退出了,而且退出码为-1,也就是PTHREAD_CANCELED.

2.3 取消状态以及类型

默认情况下,线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以选择不被取消或者控制如何被取消,通过**pthread_setcancelstate()pthread_setcanceltype()**来设置线程的取消性状态和类型。

#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

使用这些函数需要包含头文件<pthread.h>,pthread_setcancelstate()函数会将调用线程的取消性状态设置为参数state中给定的值,并将线程之前的取消性状态保存在参数oldstate指向的缓冲区中,如果对之前的状态不感兴趣,Linux允许将参数oldstate设置为NULL;pthread_setcancelstate()调用成功将返回0,失败返回非0值的错误码。

pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作,这两步是一个原子操作。参数state必须是以下值之一:

  1. PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的。
  2. PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为PTHREAD_CANCEL_ENABLE。

在新线程的new_thread_start()函数中调用pthread_setcancelstate()函数将线程的取消性状态设置为PTHREAD_CANCEL_DISABLE,我们来试试,此时主线程还能不能取消新线程,示例代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
	 /* 设置为不可被取消 */
	 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
	 for ( ; ; ) 
	 {
		 printf("新线程--running\n");
		 sleep(2);
	 }
	 return (void *)0;
}
int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

在这里插入图片描述
测试结果确实如此,将一直重复打印"新线程–running",因为新线程是一个死循环(测试完成按 Ctrl+C退出)。

在介绍一下pthread_setcanceltype()函数
如果线程的取消性状态为PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用pthread_setcanceltype()函数来设置,它的参数type指定了需要设置的类型,而线程之前的取消性类型则会保存在参数oldtype所指向的缓冲区中,如果对之前的类型不敢兴趣,Linux 下允许将参数oldtype设置为NULL。同样pthread_setcanceltype()函数调用成功将返回0,失败返回非0值的错误码。

pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。参数type必须是以下值之一:

  1. PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point)为止,这是所有新建线程包括主线程默认的取消性类型。
  2. PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消性类型应用场景很少,不再介绍!

当某个线程调用fork()创建子进程时,子进程会继承调用线程的取消性状态和取消性类型,而当某线程调用exec函数时,会将新程序主线程的取消性状态和类型重置为默认值,也就是PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERRED。

2.4 取消点

若将线程的取消性类型设置为PTHREAD_CANCEL_DEFERRED时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。

那什么是取消点呢?所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的,究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。

取消点函数包括哪些呢?下表给大家简单地列出了一些:
在这里插入图片描述

除了表 中所列函数之外,还有大量的函数,系统实现可以将其作为取消点,这里便不再一一列举出来了,大家也可以通过 man 手册进行查询,命令为"man 7 pthreads":

man 7 pthreads

在这里插入图片描述
线程在调用这些函数时,如果收到了取消请求,那么线程便会遭到取消;除了这些作为取消点的函数之外,不得将任何其它函数视为取消点(亦即,调用这些函数不会招致取消)。

2.5 线程可取消性的检测

假设线程执行的是一个不含取消点的循环(譬如 for 循环、while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它.

在实际应用程序当中,确实会遇到这种情况,线程最终运行在一个循环当中,该循环体内执行的函数不存在任何一个取消点,但实际项目需求是:该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用 pthread_testcancel()函数,该函数目的很简单,就是产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。其函数原型如下所示:

#include <pthread.h>

void pthread_testcancel(void);

下面我们通过一个小demo看看如何吧:

  1. 主线程创建一个新的进程,新进程的取消性状态和类型置为默认,新进程最终执行的是一个不含取消点的循环;主线程向新线程发送取消请求,示例代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
	 printf("新线程--start run\n");
	 for ( ; ; ) {
	 }
	 return (void *)0;
}

int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);	
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

解读代码:
新线程的 new_thread_start()函数中是一个 for 死循环,没有执行任何函数,所以是一个没有取消点的循环体,主线程调用 pthread_cancel()是无法将其终止的;
在这里插入图片描述

  1. 在new_thread_start 函数的 for 循环体中执行 pthread_testcancel()函数,接下来我们再试试:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
	 printf("新线程--start run\n");
	 for ( ; ; ) {
	 pthread_testcancel();
	 }
 	 return (void *)0;
}
int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	  exit(0);
}

如果 pthreadtestcancel()可以产生取消点,那么主线程便可以终止新线程,测试结果如下:
在这里插入图片描述
从这里我们能清晰的反应得到,新线程是有取消点可以正常退出的!!!

3. 分离线程

3.1 如何分离线程

默认情况下,当线程终止时,其它线程可以通过调用pthread_join()获取其返回状态、回收线程资源,有时,程序员并不关心♥线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用pthread_detach()将指定线程进行分离,也就是分离线程,pthread_detach()函数原型如下所示:

#include <pthread.h>

int pthread_detach(pthread_t thread);

使用该函数需要包含头文件<pthread.h>,参数 thread 指定需要分离的线程,函数 pthread_detach()调用成功将返回 0;失败将返回一个错误码。

一个线程既可以将另一个线程分离,同时也可以将自己分离,譬如:

pthread_detach(pthread_self());

tips: 一旦线程处于分离状态,就不能再使用 pthread_join()来获取其终止状态,此过程是不可逆的,一旦处于分离状态之后便不能再恢复到之前的状态。处于分离状态的线程,当其终止后,能够自动回收线程资源。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg)
{
	 int ret;
	 /* 自行分离 */
	 ret = pthread_detach(pthread_self());
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
		 return NULL;
 	 }
	 printf("新线程 start\n");
	 sleep(2); //休眠 2 秒钟
	 printf("新线程 end\n");
	 pthread_exit(NULL);
}
int main(void)
{
	 pthread_t tid;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1); //休眠 1 秒钟
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, NULL);
	 if (ret)
	 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
	 pthread_exit(NULL);
}

代码解析:主线程创建新的线程之后,休眠1秒钟,调用pthread_join()等待新线程终止;新线程调用pthread_detach(pthread_self())将自己分离,休眠2秒钟之后pthread_exit()退出线程;主线程休眠1秒钟是能够确保调用pthread_join()函数时新线程已经将自己分离了,所以按照上面的介绍可知,此时主线程调用pthread_join()必然会失败,测试结果如下:

在这里插入图片描述
打印结果正如我们所料,主线程调用pthread_join()确实会出错,错误提示为“Invalid argument”。

本文参考正点原子的嵌入式LinuxC应用编程。

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

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

相关文章

尚硅谷谷粒商城Rabbit MQ

文章目录1. 概述2. 相关概念2.1 RabbitMQ简介&#xff1a;2.2核心概念2.2.1 Message2.2.2 Publisher2.2.3 Exchange2.2.4 Queue2.2.5 Binding2.2.6Connection2.2.7 Channel2.2.8 Consumer2.2.9Virtual Host2.2.10Broker3.Docker安装rabbit MQ4、RabbitMQ运行机制4.1AMQP 中的消…

【信管10.2】规划识别风险及定性分析

规划识别风险及定性分析了解完风险相关的知识以及项目风险的管理过程之后&#xff0c;我们就进入到每个风险过程的学习。风险管理过程的内容并不算少&#xff0c;直逼范围、进度、成本、质量四大核心模块&#xff0c;也是我们需要重点关注的内容。当年的论文我写得就是风险管理…

IDEA中Maven打包遇到的问题

问题1 问题描述 使用Maven进行打包&#xff0c;点击package&#xff0c;Run控制台的信息出现中文乱码的情况 解决方法 -DarchetypeCataloginternal -Dfile.encodingGBK问题2 问题描述 程序能够正常运行&#xff0c;但是使用Maven对程序进行打包&#xff0c;在编译过程中出现…

注册Github账号详细教程【超详细篇 适合新手入门】

前言 &#x1f4dc; “ 作者 久绊A ” 专注记录自己所整理的Java、web、sql等&#xff0c;IT技术干货、学习经验、面试资料、刷题记录&#xff0c;以及遇到的问题和解决方案&#xff0c;记录自己成长的点滴 目录 一、GitHub的简介 1、大概介绍 2、详细介绍 二、如何注册自己…

算法训练营 day29 回溯算法 组合总和III 电话号码的字母组合

算法训练营 day29 回溯算法 组合总和III 电话号码的字母组合 组合总和III 216. 组合总和 III - 力扣&#xff08;LeetCode&#xff09; 找出所有相加之和为 n 的 k 个数的组合&#xff0c;且满足下列条件&#xff1a; 只使用数字1到9 每个数字 最多使用一次 返回 所有可能的…

16.Map系列、集合嵌套、不可变集合

目录 一.Map 1.1 Map集合概述 1.2 Map集合体系 1.3 Map集合体系特点 1.4 Map集合实现类特点 1.5 Map集合的API 1.6 Map集合的遍历方式 1.6.1 键找值的方式遍历 1.6.2 键值对的方式遍历 1.6.3 Lambda表达式的方式 1.7 HashMap 1.7.1 HashMap的特点 1.7.2 底层原理 …

python求不同分辨率图像的峰值信噪比,一文搞懂

可以使用 Python 的 NumPy 和 OpenCV 库来实现这个任务。提前准备一张图片作为素材。 文章目录什么是峰值信噪比PSNR 峰值信噪比补充说明使用 OpenCV 库来实现这个任务PSNR 的计算值受图像的亮度影响计算不同分辨率图像的 PSNRpython 求不同分辨率图像的峰值信噪比 | 其他知识点…

Java面试题:finalize的原理和工作缺点是什么

finalize是 Object 中的一个方法&#xff0c;如果子类重写它&#xff0c;垃圾回收时此方法会被调用&#xff0c;可以在其中进行资源释放和清理工作。其次将资源释放和清理放在 finalize 方法中非常不好&#xff0c;非常影响性能&#xff0c;严重时甚至会引起 OOM&#xff0c;从…

LabVIEW对NI Linux RT应用程序性能进行基准测试

LabVIEW对NI Linux RT应用程序性能进行基准测试如果应用程序具有苛刻的性能要求&#xff0c;则应为应用程序创建性能基准测试&#xff0c;以确保它满足性能要求。性能要求高度依赖于应用程序&#xff0c;应确定哪些性能指标很重要。下面介绍了典型的实时应用程序性能指标。如果…

USBIP

USBIP USB/IP 是一个开源项目&#xff0c;已合入 Kernel&#xff0c;在 Linux 环境下可以通过使用 USB/IP 远程共享 USB 设备。 USB Client&#xff1a;使用USB的终端&#xff0c;将server共享的usb设备挂载到本地。 USB Server&#xff1a;分享本地的usb设备至远程。 USBIP…

Python的入门知识汇集

创建 Python的创始人为Guido van Rossum。1989年圣诞节期间,Guido为了打发圣诞节的无趣,决心开发一个新的脚本解释程序,做为ABC 语言的一种继承。之所以选中Python(大蟒蛇的意思)作为程序的名字,是因为他是一个叫Monty Python的喜剧团体的爱好者。 什么是Pyhton Pytho…

委派模式——从SLF4J说起

作者&#xff1a;vivo 互联网服务器团队- Xiong yangxin 将某个通用解决方案包装成成熟的工具包&#xff0c;是每一个技术建设工作者必须思考且必须解决的问题。本文从业内流行的既有工具包入手&#xff0c;解析实现思路&#xff0c;沉淀一般方法。为技术建设的初学者提供一些实…

Gorm连接以及CURD实战+测试

Gorm CRUD 前言 Gorm是go的一个ORM框架&#xff0c;官方文档地址为-> GORM 指南 本文将介绍与gorm有关的CRUD操作&#xff0c;操作数据库类型为mysql数据库 数据库连接 func Open(dialector Dialector, opts …Option) (db *DB, err error) 该函数用于进行gorm连接对应…

中国市场手机出货量跌穿3亿部,苹果也顶不住了,只有三星暗爽

多家市调机构都给出了2022年中国智能手机市场的数据&#xff0c;数据虽然有些出入&#xff0c;不过都认为中国市场的手机出货量跌穿了3亿部&#xff0c;创下近10年来的新低纪录&#xff0c;国产手机尤其惨&#xff0c;而曾逆势增长的苹果也开始出现下滑。市调机构IDC给出的数据…

词法分析器Flex学习1 - Flex识别关键字

以前曾写过2篇Flex和Bison入门应用的文章&#xff1b; https://blog.csdn.net/bcbobo21cn/article/details/112343850 https://blog.csdn.net/bcbobo21cn/article/details/106193648 我只记得Flex是词法分析器&#xff0c;Bison是语法分析器&#xff1b; 只是一些入门的介绍&…

基于SSM+Layui的图书管理系统(Java版 附源代码及数据库)

目录 功能要求 技术栈 项目架构 登录界面 系统首页 借阅管理 图书管理 读者管理 类型管理 公告管理 管理员管理 统计分析 数据库设计 源代码数据库资料 毕设项目专栏 功能要求 &#xff08;1&#xff09;对系统登陆后进行增删改查功能 &#xff08;2&#xff09;…

文档管理对人力资源部门的重要影响

文档管理对人力资源部门的重要影响 当人力资源的管理功能是基于纸张或依赖于 Excel 电子表格、共享驱动器和其他无法与其他系统共享数据的软件等技术时&#xff0c;人力资源管理既耗时又耗费劳动力。用数字工作流程取代纸质流程可以简化从招聘和入职到绩效评估的流程。 随着组…

Elasticsearch(七)--ES文档的操作(下)---删除文档

一、前言 上篇文章我们了解了ES的修改文档的操作&#xff0c;也同样分别通过ES的kibana客户端以及Java高级Rest客户端进行学习&#xff0c;那么本篇末尾要给大家介绍的是对文档的删除操作&#xff0c;同新修改文档&#xff0c;也有删除单条文档和批量删除文档操作&#xff0c;…

工赋开发者社区 | 工业5.0为何是下一个10年的制造业关键性变革方向?

近年来&#xff0c;全球经济发展面临下行压力&#xff0c;世界各国重新认识到制造业在拉动经济增长、创造就业机会等方面的作用。欧洲在这种压力下提出了全新的工业5.0发展概念&#xff0c;试图重振制造业并再次引领全球工业发展潮流。本文小编分享一篇来自KNOWHOW的文章&#…

【地铁上的Redis与C#】数据类型(八)--set类型基本操作

这篇文章&#xff0c;我们开始学习set类型&#xff0c;学习set类型前我们先来看一下List类型有什么缺点。 List的缺点 当需要存储大量数据并且要提供高效率的查询时&#xff0c;List是无法完全实现的&#xff0c;这是因为list的存储结构是链表的形式&#xff0c;链表读取数据…