Linux线程原理以及基础操控函数使用(1)

news2024/11/25 12:34:19

线程原理

这一个课程的笔记

LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)

img

进程:独立地址空间,拥有PCB

线程:有独立的PCB,但没有独立的地址空间(共享)

区别:在于是否共享地址空间。 独居(进程);合租(线程)。

Linux下: 线程:最小的执行单位

​ 进程:最小分配资源单位,可看成是只有一个线程的进程。

可以使用ps -Lf pid查看一个进程的线程

image-20240328225804010

LWP: 执行的最小单位, 轻量级线程, 实际等于tid

Linux内核线程实现原理

类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。

  1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
  3. 进程可以蜕变成线程
  4. 线程可看做寄存器和栈的集合
  5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

察看LWP号:ps –Lf pid 查看指定线程的lwp号。

img

三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元

x86使用页表实现虚拟内存原理分析—使用代码分析-CSDN博客

对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。

​ 实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。

​ 如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。

​ 因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

线程共享资源

  1. 文件描述符表
  2. 每种信号的处理方式 , 这一个信号的处理方式这几个线程都一样, 实际处理的是获取到的那一个, 但是线程的mask不一样, 可以改变mask使得某一个线程处理这一个信号

不推荐线程以及信号混合使用!!!

  1. 当前工作目录
  2. 用户ID和组ID
  3. 内存地址空间 (.text/.data/.bss/heap/共享库), 这一个里面没有栈

线程非共享资源

  1. 线程id
  2. 处理器现场和栈指针(内核栈, 用于处理高特权级时候使用)
  3. 独立的栈空间(用户空间栈, 普通用户使用)
  4. errno变量, 这是一个全局变量
  5. 信号屏蔽字
  6. 调度优先级

线程优、缺点

​ 优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便(全局变量, 堆区, 以及rodata共享, 可以直接使用)

​ 缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好

​ 优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

线程控制

以下的函数是使用pthread 库进行线程级别的操作。

pthread_self获取线程ID

获取的线程ID是一个posix标准的ID, 只用于这一个进程里面的区分, 这是这一个线程库使用的变量

对应gettid, 这一个tid是进程里面标示身份的, 一个多线程的任务的所有线程的PID是一样的, tid用于区分不同的线程(内核用于时间片轮转), 这一个是系统级的(lwp:轻量级线程, 实际代表的也是这一个)

gettid() returns the caller’s thread ID (TID). In a single-threaded
process, the thread ID is equal to the process ID (PID, as returned by
getpid(2)). In a multithreaded process, all threads have the same PID,
but each one has a unique TID. For further details, see the discussion
of CLONE_THREAD in clone(2).

可以使用这一个命令获取TID

The pthread_self() function returns the ID of the calling thread. This
is the same value that is returned in *thread in the pthread_create(3)
call that created this thread.

Thread IDs are guaranteed to be unique only within a process. A thread
ID may be reused after a terminated thread has been joined, or a
detached thread has terminated.

使用这一个命令获取这一个线程在这一个库里面的编号

pthread_create函数

创建一个新线程。 其作用,对应进程中fork() 函数。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

​ 返回值:成功:0; 失败:错误号 -----Linux环境下,所有线程特点,失败均直接返回错误号。

参数:

​ pthread_t:当前Linux中可理解为:typedef unsigned long int pthread_t;

参数1:传出参数,保存系统为我们分配好的线程ID

参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数。设置这一个线程的优先级, 线程的栈空间大小

The attr argument points to a pthread_attr_t structure whose contents
are used at thread creation time to determine attributes for the new
thread; this structure is initialized using pthread_attr_init(3) and
related functions. If attr is NULL, then the thread is created with
default attributes.

这一个参数使用pthread_attr_init这一个函数

参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束

参数4:线程主函数执行期间所使用的参数

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。

start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait(2)得到子进程的退出状态,稍后详细介绍pthread_join。

pthread_create成功返回后,新创建的线程的id被填写到thread参数所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid(2)可以获得当前进程的id,是一个正整数值。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self(3)可以获得当前线程的id。

attr参数表示线程属性,本节不深入讨论线程属性,所有代码例子都传NULL给attr参数,表示线程属性取缺省值,感兴趣的读者可以参考APUE。

示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <string.h>


void *thread_handler(void *arg){
	printf("-----------thread------------\n");
	printf("pid = %d, swl = %ld\n", 
			getpid(), pthread_self());
	return NULL;
}

int main(void){
	pthread_t tid;

	tid = pthread_self();

	printf("main pid = %d swl = %ld \n", getpid(), tid);

	int ret = pthread_create(&tid, NULL, thread_handler, NULL);
	if(ret != 0){
		printf("pthreat creat error, %d = %s\n", ret, strerror(ret));
		exit(0);
	}
	printf("tid now = %ld\n", tid);
	sleep(1);
	return 0;
}

由于pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印。

如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止

创建的线程的return实际效果是执行pthread_exit返回这一个返回值, main返回实际会结束这一个线程组, 相当于exit

线程属性

linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

typedef struct 

{

    int                    etachstate;   //线程的分离状态

    int                    schedpolicy;  //线程调度策略

    struct sched_param    schedparam; //线程的调度参数

    int                    inheritsched; //线程的继承性

    int                    scope;       //线程的作用域

    size_t                 guardsize;    //线程栈末尾的警戒缓冲区大小

    int                    stackaddr_set; //线程的栈设置

    void*                 stackaddr;    //线程栈的位置

    size_t                 stacksize;     //线程栈的大小

} pthread_attr_t; 

这是一个比较早的版本

主要结构体成员:

  1. 线程分离状态
  2. 线程栈大小(默认平均分配)
  3. 线程栈警戒缓冲区大小(位于栈末尾) 参 APUE.12.3 线程属性

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。

线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。

线程属性初始化

注意:应先初始化线程属性,再pthread_create创建线程

初始化线程属性

int pthread_attr_init(pthread_attr_t *attr); 成功:0;失败:错误号

销毁线程属性所占用的资源

int pthread_attr_destroy(pthread_attr_t *attr); 成功:0;失败:错误号

线程的分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。

非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

线程分离状态的函数:

设置线程属性,分离or非分离

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

获取程属性,分离or非分离

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

​ 参数: attr:已初始化的线程属性

detachstate: PTHREAD_CREATE_DETACHED(分离线程)

​ PTHREAD _CREATE_JOINABLE(非分离线程)

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

示例1: 使用属性设置是否分离

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>

void *pthread_func(void *arg){
	printf("pthread_func\n");
	return (void *)0x1234;
}

int main(void){
	pthread_t tid;
	pthread_attr_t attr;
	int ret;
	ret = pthread_attr_init(&attr);
	if(ret != 0){
		printf("pthread_attr_init %s\n", strerror(ret));
		exit(1);
	}
	ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	if(ret != 0){
		printf("pthread_attr_setdetachstate %s\n", strerror(ret));
		exit(1);
	}
	ret = pthread_create(&tid, &attr, (void *)pthread_func, NULL);
	if(ret != 0){
		printf("pthread_create %s\n", strerror(ret));
		exit(1);
	}
	....
}

设置一个分离属性

示例2: 使用属性设置栈大小

#include <pthread.h>

#define SIZE 0x100000
void *th_fun(void *arg)
{
	while (1) 
		sleep(1);
}
int main(void)
{
	pthread_t tid;
	int err, detachstate, i = 1;
	pthread_attr_t attr;
	size_t stacksize;
	void *stackaddr;

	pthread_attr_init(&attr);		
	pthread_attr_getstack(&attr, &stackaddr, &stacksize);//获取现在的栈的状态
	pthread_attr_getdetachstate(&attr, &detachstate);//获取这一个线程的线程是否分离

	if (detachstate == PTHREAD_CREATE_DETACHED)
		printf("thread detached\n");
	else if (detachstate == PTHREAD_CREATE_JOINABLE)
		printf("thread join\n");
	else
		printf("thread unknown\n");

	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//设置属性的状态

	while (1) {
		stackaddr = malloc(SIZE);//获取一块新的栈
		if (stackaddr == NULL) {
			perror("malloc");
			exit(1);
		}
		stacksize = SIZE;
		pthread_attr_setstack(&attr, stackaddr, stacksize);//设置这一个新的栈
		err = pthread_create(&tid, &attr, th_fun, NULL);
		if (err != 0) {
			printf("%s\n", strerror(err));
			exit(1);
		}
		printf("%d\n", i++);
	}
	pthread_attr_destroy(&attr);
	return 0;
}	

pthread_exit线程的退出

将单个线程退出

void pthread_exit(void *retval); 参数:retval表示线程退出状态,通常传NULL

​ 结论:线程中,禁止使用exit函数,会导致进程内所有线程全部退出。

​ 在不添加sleep控制输出顺序的情况下。pthread_create在循环中,几乎瞬间创建5个线程,但只有第1个线程有机会输出(或者第2个也有,也可能没有,取决于内核调度)如果第3个线程执行了exit,将整个进程退出了,所以全部线程退出了。

所以,多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。

注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <string.h>


void *thread_handler(void *arg){
	if((long)arg == 3){
		pthread_exit((void *)0);//可以单独退出某一个线程
	}
	printf("-----------thread%ld------------\n", (long)arg);
	printf("pid = %d, swl = %ld\n", 
			getpid(), pthread_self());
	return NULL;
}

int main(void){
	pthread_t tid;

	tid = pthread_self();

	printf("main pid = %d swl = %ld \n", getpid(), tid);
	for(long i = 0 ; i < 5 ;i ++){
		int ret = pthread_create(&tid, NULL, thread_handler, (void *)i);
		if(ret != 0){
			printf("pthreat creat error, %d = %s\n", ret, strerror(ret));
			exit(0);
		}
	}
	printf("parent pid = %d tid now = %ld\n",getpid(), tid);
	//return 0;
	pthread_exit((void *)0);//这个时候不需要sleep了
}

pthread_join获取返回值

阻塞等待线程退出,获取线程退出状态 其作用,对应进程中 waitpid() 函数。

​ int pthread_join(pthread_t thread, void **retval); 成功:0;失败:错误号

​ 参数:thread:线程ID (【注意】:不是指针);retval:存储线程结束状态。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

struct thrd {
	int val;
	char str[256];
};

void * tfn(void *arg){
	struct thrd *tval;//初始化一块结构体
	tval = malloc(sizeof(struct thrd));
	tval->val = 100;
	strcpy(tval->str, "hello world");
	return (void *)tval;
}

int main(void){
	pthread_t tid;
	int ret = pthread_create(&tid, NULL, tfn, NULL);
	if(ret != 0){
		printf("pthread creat error %d :%s", ret, strerror(ret));
		exit(1);
	}
	void *retval;
	ret = pthread_join(tid, &retval);//获取这一个的信息
    if(ret != 0){
        printf("pthread join error %d :%s", ret, strerror(ret));
        exit(1);
    }
	printf("get thread ret : %d %s\n", ((struct thrd*)retval)->val,
			((struct thrd*)retval)->str);//打印信息
    free(retval);
	pthread_exit(NULL);


	return 0;

}

image-20240330104236795

回收多个进程的时候需要使用数组

pthread_detach线程的分离

int pthread_detach(pthread_t thread); 成功:0;失败:错误号

​ 线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

​ 进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。

​ 也可使用 pthread_create函数参2(线程属性)来设置线程分离。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

struct thrd {
	int val;
	char str[256];
};

void * tfn(void *arg){
	struct thrd *tval;
	tval = malloc(sizeof(struct thrd));
	tval->val = 100;
	strcpy(tval->str, "hello world");
	return (void *)tval;
}

int main(void){
	pthread_t tid;
	int ret = pthread_create(&tid, NULL, tfn, NULL);
	if(ret != 0){
		printf("pthread creat error %d :%s", ret, strerror(ret));
		exit(1);
	}
	ret = pthread_detach(tid);			//进行分离
	if(ret != 0){
		printf("pthread detach error %d :%s", ret, strerror(ret));
		exit(1);
	}

	void *retval;
	ret = pthread_join(tid, &retval);	//获取这一个线程的返回值, 这个时候已经分离了, 会失败
	if(ret != 0){
		printf("pthread join error %d :%s", ret, strerror(ret));
		exit(1);
	}
	printf("get thread ret : %d %s\n", ((struct thrd*)retval)->val,
			((struct thrd*)retval)->str);
	pthread_exit(NULL);
	free(retval);

	return 0;
}

image-20240330125240212

pthread_cancel函数

杀死(取消)线程 其作用,对应进程中 kill() 函数。

​ int pthread_cancel(pthread_t thread); 成功:0;失败:错误号

【注意】:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点。

被取消的线程, 退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h中找到它的定义:**#define PTHREAD_CANCELED ((void *) -1)****。**因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>


void * tfn(void *arg){
	while(1){
		printf("thread pid = %d, tid = %lu\n", getpid(), pthread_self());
		sleep(1);
	}
	return NULL;
}

int main(void){
	pthread_t tid;
	int ret = pthread_create(&tid, NULL, tfn, NULL);
	if(ret != 0){
		printf("pthread creat error %d :%s", ret, strerror(ret));
		exit(1);
	}

	printf("main pid = %d, tid = %lu\n", getpid(), pthread_self());
	sleep(5);
	ret = pthread_cancel(tid);
	if(ret != 0){
		exit(1);
	}
	
	while(1);

}

image-20240330111848766

if the target
thread was canceled, then PTHREAD_CANCELED is placed in the location
pointed to by retval

这一个进程使用pthread_join回收的值是-1

long retv;
pthread_join(tid, (void **)&retv);
printf("ret = %ld\n", retv);

image-20240330112923210

**注: **

如果这一个子线程使用的是一个while(1);之类的死循环是无法退出的, 因为没有取消点(进入内核)

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>


void * tfn(void *arg){
	while(1){
	}
	return NULL;
}

int main(void){
	pthread_t tid;
	int ret = pthread_create(&tid, NULL, tfn, NULL);
	if(ret != 0){
		printf("pthread creat error %d :%s", ret, strerror(ret));
		exit(1);
	}

	printf("main pid = %d, tid = %lu\n", getpid(), pthread_self());
	sleep(5);
	ret = pthread_cancel(tid);
	if(ret != 0){
		exit(1);
	}
	long retv;
	pthread_join(tid, (void **)&retv);//会在这里卡死
	printf("ret = %ld\n", retv);

}

可以使用函数pthread_testcancel添加一个取消点

	while(1){
		pthread_testcancel();
 }

总结

和进程对比

线程进程
pthread_createfork
pthread_selfgetpid
pthread_exitexit
pthread_joinwait/waitpid
pthread_cancelkill
pthread_detach
终止线程方式

总结:终止某个线程而不终止整个进程,有三种方法:

  1. 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。
  2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
  3. 线程可以调用pthread_exit终止自己。

线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应调用pthread_exit
  2. 避免僵尸线程

pthread_join

pthread_detach

pthread_create指定分离属性

被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

  1. malloc和mmap申请的内存可以被其他线程释放
  2. 应避免在多线程模型中调用fork除非,马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
  3. 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

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

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

相关文章

算法6.4-6.6DFS

一个不知名大学生&#xff0c;江湖人称菜狗 original author: Jacky Li Email : 3435673055qq.com Time of completion&#xff1a;2024.03.27 Last edited: 2024.03.27 目录 算法6.4-6.6DFS 第1关&#xff1a;算法6.5采用邻接矩阵表示图的深搜 任务描述 相关知识 编程要求…

数据结构——优先级队列及多服务台模拟系统的实现

一、优先级队列的定义和存储 优先级队列定义&#xff1a;优先级高的元素在队头&#xff0c;优先级低的元素在队尾 基于普通线性表实现优先级队列&#xff0c;入队和出队中必有一个时间复杂度O(n),基于二叉树结构实现优先级队列&#xff0c;能够让入队和出队时间复杂度都为O(log…

RabbitMQ--04--发布订阅模式 (fanout)-案例

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 发布订阅模式 (fanout)---案例前言RabbitListener和RabbitHandler的使用 1.通过Spring官网快速创建一个RabbitMQ的生产者项目2.导入项目后在application.yml文件中配…

Python编程-并发编程基础梳理与高级特性案例讲解

Python编程-并发编程基础梳理与高级特性案例讲解 同步、异步通信与IO阻塞 同步&#xff08;Synchronous&#xff09;和异步&#xff08;Asynchronous&#xff09;通信是两种不同的通信方式&#xff0c;涉及到处理任务的时序和相互等待的关系。同步通信简单直观&#xff0c;但可…

Unity 使用TrailRenderer制作拖尾效果

使用TrailRenderer实现拖尾效果&#xff0c;具体操作步骤如下&#xff1a; 1、创建一个空对象 在Unity场景中创建一个空对象 2、添加TrailRenderer组件 选择步骤1创建的空对象&#xff0c;然后在Inspector面板中点击“Add Component”按钮&#xff0c;搜索并添加TrailRende…

蓝桥杯第七届大学B组详解

目录 1.煤球数量&#xff1b; 2.生日蜡烛&#xff1b; 3.凑算式 4.方格填数 5.四平方和 6.交换瓶子 7.最大比例 1.煤球数量 题目解析&#xff1a;可以根据题目的意思&#xff0c;找到规律。 1 *- 1个 2 *** 3个 3 ****** 6个 4 ********** 10个 不难发现 第…

C++入门知识详细讲解

C入门知识详细讲解 1. C简介1.1 什么是C1.2 C的发展史1.3. C的重要性1.3.1 语言的使用广泛度1.3.2 在工作领域 2. C基本语法知识2.1. C关键字(C98)2.2. 命名空间2.2 命名空间使用2.2 命名空间使用 2.3. C输入&输出2.4. 缺省参数2.4.1 缺省参数概念2.4.2 缺省参数分类 2.5. …

Linux - 第三节

改变用户类型 su 仅单纯的进行身份变化 依旧处于普通用户里面 su - 进行重新登录更改身份 退出用exit / ctrld su 用户名 改成成其他身份 对一条命令进行提权 sudo command r:可读 w:可写 x:可执行 -:对应的权限位置&#xff0c;没有权限 去掉所有权限 chmod u…

Qt中继承QCheckBox的类结合QTableWidget实现多选并且每个多选的id都不一样

1.相关描述 继承QCheckBox的类MyCheckBox&#xff0c;利用QTableWidget的setCellWidget方式添加MyCheckBox类的对象 2.相关页面 3.相关代码 mycheckbox.h #ifndef MYCHECKBOX_H #define MYCHECKBOX_H#include <QCheckBox> #include <QObject>class MyCheckBox : pu…

Elvis Presley 英文阅读

Elvis Presley 官方翻译 One of the most popular American singers of thetwentieth century was Elvis Presley. He made theRock & Roll music popular around the world. He sold millions of records and made manysuccessful films, and he helped change the dir…

基于SSM医院病历管理系统

基于SSM医院病历管理系统的设计与实现 摘要 病历管理系统是医院管理系统的重要组成,在计算机技术快速发展之前&#xff0c;病人或者医生如果想记录并查看自己的健康信息是非常麻烦的&#xff0c;因为在以往病人的健康信息通常只保存在自己的病历卡或者就诊报告中&#xff0c;…

TikTok直播专线:解决出海网络问题痛点,提升商业效率

近年来&#xff0c;TikTok作为全球最受欢迎的社交媒体平台之一&#xff0c;成为商家获取商机与市场的重要平台。然而&#xff0c;尽管商家纷纷进入TikTok&#xff0c;试图借助其强大的社交网络进行产品推广和销售&#xff0c;但在TikTok平台进行直播时&#xff0c;往往会遇到网…

何时应用 RAG 与微调

充分发挥 LLM 的潜力需要在检索增强生成&#xff08;RAG&#xff09;和微调之间选择正确的技术。 让我们来看看何时对 LLM、较小的模型和预训练模型使用 RAG 与微调。我们将介绍&#xff1a; LLM 和 RAG 的简要背景RAG 相对于微调 LLM 的优势何时针对不同模型大小对 RAG 进行…

WebGIS 之 vue3+vite+ceisum

1.项目搭建node版本在16以上 1.1创建项目 npm create vite 项目名 1.2选择框架 vuejavaScript 1.3进入项目安装依赖 cd 项目名 npm install 1.4安装cesium依赖 pnpm i cesium vite-plugin-cesium 1.5修改vite.config.js文件 import { defineConfig } from vite import vue fr…

labelme AI 模型运用

一、lebelme 1、界面介绍 点击上图位置&#xff0c;选择对应的模型。这里我每个模型都测试了一下&#xff0c;EfficientSam这个模型最好用&#xff0c;准确率和速度都ok。 2、使用方法 目标框标注方法&#xff1a;点左上角【编辑】-> 【Create Ai-Mask】就可以标志了&…

在 Three.js 中,OBJExporter 是一个用于将 Three.js 中的场景导出为 OBJ 格式的类。

demo案例 在 Three.js 中&#xff0c;OBJExporter 是一个用于将 Three.js 中的场景导出为 OBJ 格式的类。下面是关于它的入参、出参、属性和方法的解释&#xff1a; 类名&#xff1a;OBJExporter 构造函数&#xff1a; THREE.OBJExporter()说明&#xff1a; 创建一个 OBJE…

2d导入人物素材进行分割后设置图层

1、设置分辨率大小 2、相机调整大小&#xff0c;要符合场景 3、选择2D sprite 编辑器 或者 点击这个也行 4、分割图像 5、设置过滤模式 6、图层设置

CQI-17:2021 V2 英文 、中文版。特殊过程:电子组装制造-锡焊系统评审标准

锡焊作为一个特殊的工艺过程&#xff0c;由于其材料特性的差异性、工艺参数的复杂性和过程控制的不确定性&#xff0c;长期以来一直视为汽车零部件制造业的薄弱环节&#xff0c;并将很大程度上直接导致整车产品质量的下降和召回风险的上升。 美国汽车工业行动集团AIAG的特别工…

MySQL 之 数据库操作 及 表操作

&#x1f389;欢迎大家观看AUGENSTERN_dc的文章(o゜▽゜)o☆✨✨ &#x1f389;感谢各位读者在百忙之中抽出时间来垂阅我的文章&#xff0c;我会尽我所能向的大家分享我的知识和经验&#x1f4d6; &#x1f389;希望我们在一篇篇的文章中能够共同进步&#xff01;&#xff01;&…

共享办公室行业面临的最大挑战是什么,未来有哪些可能的发展方向

共享办公室行业虽然发展迅速&#xff0c;但也面临着一些挑战和需要解决的问题。咱们来聊聊这行业的最大挑战和未来可能的发展方向。 面临的最大挑战&#xff1a; 市场竞争加剧&#xff1a;随着共享办公室的火热&#xff0c;越来越多的玩家进入市场&#xff0c;竞争变得异常激烈…