linux 应用开发笔记---【线程】

1.概念:

线程是参与系统调度的最小单位,它被包含在进程中,是进程的实际运行单位

一个进程可以创建多个线程,多个线程并发运行,每个线程执行不同的任务

2.如何创建线程

当一个程序启动的时候,一个进程被os创建,同时一个线程也立刻开始运行,这个线程被叫做主线程,main()函数是主线程的入口函数

任何一个进程都包含了一个主线程,只有主线程的进程被称为单线程进程

多线程:除了主线程以外,还包含其他的线程,其他线程通常是由主线程创建来的

主线程的作用:

1.创建其他的子线程

2.执行清理工作

3.线程的特点:

线程是程序执行的最基本的单位,进程不能运行,真正运行的是进程中的线程,同一个进程的多个线程共享这个线程的全部系统资源,但同意进程的多个线程有各自的调用栈,自己的寄存器环境,自己的线程本地存储

1.线程不单独存在,被包含在进程里面

2.线程是参与系统调度的基本单位

3.可并发执行,同一个进程的多个线程可以同时执行【宏观

4.共享进程资源,所以线程拥有相同的地址空间【进程的地址空间】这意味着,线程可以访问该地址空间的每一个虚地址,此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等

4.线程与进程

进程创建多个子进程可以实现并发多任务【本质上是多线程进程】,多线程同样也可以实现并发处理多任务的需求

多进程编程:

进程间切换开销大,多个进程同时运行,微观上是轮流切换运行,进程间切换开销远大于同一进程的多个线程切换的开销

进程间的通信比较麻烦,同一进程的多个线程通信非常方便

线程创建的速度远大于进程创建的速度

5.并发和并行

 

并发:交替做不同的事情【相比于串行,不再等待上一个任务完成之后再做下一个任务】

并行:同时做不同的事情

6.线程ID

线程和进程一样,都有属于自己的ID,

pthread_t pthread_self(void);创建ID,返回当前线程的线程ID
int pthread_equal(pthread_t t1, pthread_t t2);判断两个线程ID是否相等,相等返回非零, 不相等返回0

7.创建线程

在创建第一个进程的时候,也会自动生成一个线程,也就是主线程,主线程可以使用库函数 pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
#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("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());return (void *)0;
}
int main(void)
{pthread_t tid;int ret;ret = pthread_create(&tid, NULL, new_thread_start, NULL);if (ret) {fprintf(stderr, "Error: %s\n", strerror(ret));exit(-1);}printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());sleep(1);exit(0);
}

运行结果:

8.终止线程:

线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
线程调用 pthread_exit() 函数;
调用 pthread_cancel() 取消线程
注意:如果进程中的任意线程调用 exit()、_exit()或者_Exit(),那么将会导致整个进程终止
void pthread_exit(void *retval);该返回值可以通过另一个线程调用pthread_join()来获取如果在start函数中return也可以通过pthread_join()来获取pthread_exit()和在线程start函数中和return的作用是一样的,但是区别在于可以在任意位置终止线程
进程中调用wait()函数阻塞等待子进程的结束,然后回收子进程
线程中调用pthread_join()函数来阻塞等待线程的终止,并且获取线程的退出码,回收线程资源
int pthread_join(pthread_t thread, void **retval);pthread_t thread:指定等待的线程IDretval:如果不为NULL,则返回的是start函数的return的返回值或者通过pthread_exit()的指定返回值,将返回值复制到retval所指向的内存空间成功返回0,失败返回错误码

调用pthread_join()会等待指定的线程结束后回收线程,但多个线程同时尝试调用pthread_join()等待指定线程的终止,那结果将是不确定的

如果线程终止了,但是却没有pthread_join()函数去回收线程,则该线程变为了僵尸线程,会导致系统资源的浪费,如果积累太多,则程序无法创建新的线程

和wait()的进程相比的区别:
1.线程之间的关系是对等。每个进程的任意线程均可以调用pthread_join()来等待另一个线程的结束。和父子进程不同的是,父进程通过fork()创建了子进程,也只可以wait()这个子进程,不可以等待其他的。

2.不可以用非阻塞的方式调用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)
{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);
}

运行结果:

9.取消线程

int pthread_cancel(pthread_t thread);
线程可以设置自己不被取消或者控制如何被取消,所以pthread_cancel()并不会等待线程终止,只是向线程发送一个请求

取消状态以及类型

int pthread_setcancelstate(int state, int *oldstate);⚫ PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以
新建线程以及主线程默认都是可以取消的。
⚫ PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求
挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLEeg. pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);当发送一个终止线程的请求时,线程时不会终止的,因为这个是“线程不可以被取消”
int pthread_setcanceltype(int type, int *oldtype);⚫ PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直
到线程到达某个取消点为止,这是所有新建线程包括主线程默认的取消性类型⚫ PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)
取消线程

取消点:
取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点

比如在执行到for,while永久循环的时候,线程就不会被请求终止

线程的可取消性检测
上述提到了当执行到一些永久循环的时候,当发送请求的时候,是无法终止线程,所以
void pthread_testcancel(void);eg.static void *new_thread_start(void *arg)
{printf("新线程--start run\n");for ( ; ; ) {pthread_testcancel();}return (void *)0;
}

10.分离线程

概念:也就是当线程结束的时候,希望线程终止的时候自动回收线程资源并且将其移除

int pthread_detach(pthread_t thread);线程的分离过程是不可逆的,处于分离状态的线程,当其终止后,则会自动的回收线程资源一旦分离,就可以通过pthread_join()来获取其终止状态pthread_detach(pthread_self());    //分离自己

11.线程清理函数

void pthread_cleanup_push(void (*routine)(void *), void *arg);将线程加入到清理函数栈,方法和栈一样,先入后出void pthread_cleanup_pop(int execute);当execute为0的时候,只会去清理函数栈中的最顶层的函数移除,如果非0,可以手动清除函数

12.线程属性

创建的每一个线程都可以配置线程栈的大小以及分配空间

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);eg. /* 对 attr 对象进行初始化 */pthread_attr_init(&attr);/* 设置栈大小为 4K */pthread_attr_setstacksize(&attr, 4096);......../* 销毁 attr 对象 */pthread_attr_destroy(&attr);

进程中创建的每一个线程都有自己的栈地址空间,被称为线程栈。也就是代表了每个线程都有属于自己的局部变量

可重入函数:

一个函数被同一个进程的多个不同的执行流同时调用【宏观】

线程同步 

作用:为了对共享资源的访问进行保护,解决数据一致性的问题,防止多个线程对共享资源的并发访问,所以要进行保护

互斥锁

当访问共享资源之前对对互斥锁进行上锁,在访问资源完成后,然后释放互斥锁(解锁),上锁后,任何其他试图再次对互斥锁已经加锁线程都会被阻塞,直到线程释放互斥锁。以此类推,别的企图继续加锁的设备必须要阻塞等待,上一个线程对共享资源解锁后,才可以进行加锁

互斥锁初始化:

1.使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2 、使用 pthread_mutex_init() 函数初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);当线程未被上锁,则直接上锁,若已经被上锁,则调用该函数,会被阻塞,等待互斥锁解锁后才可以上锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);对于锁定状态的互斥锁进行解锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg)
{int loops = *((int *)arg);int l_count, j;for (j = 0; j < loops; j++) {pthread_mutex_lock(&mutex); //互斥锁上锁l_count = g_count;l_count++;g_count = l_count;pthread_mutex_unlock(&mutex);//互斥锁解锁}return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{pthread_t tid1, tid2;int ret;/* 获取用户传递的参数 */if (2 > argc)loops = 10000000; //没有传递参数默认为 1000 万次elseloops = atoi(argv[1]);/* 初始化互斥锁 */pthread_mutex_init(&mutex, NULL);/* 创建 2 个新线程 */ret = pthread_create(&tid1, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}ret = pthread_create(&tid2, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}/* 等待线程结束 */ret = pthread_join(tid1, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}ret = pthread_join(tid2, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}/* 打印结果 */printf("g_count = %d\n", g_count);exit(0);
}

运行结果:​​​V

pthread_mutex_trylock()函数

当互斥锁已经被其他线程锁住的时候,调用该函数,不会导致线程阻塞,而是直接返回错误码。

int pthread_mutex_trylock(pthread_mutex_t *mutex);
 while(pthread_mutex_trylock(&mutex)); //以非阻塞方式上锁
{l_count = g_count;l_count++;g_count = l_count;pthread_mutex_unlock(&mutex);//互斥锁解锁
}

销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

被销毁的互斥锁则不可以进行上锁和解锁,需要再次初始化才可以

互斥锁死锁

当一个线程A一直锁住资源A,但想同时锁住线程B的资源B【线程A处于阻塞态】,线程B又想锁住线程A锁住的共享资源,这会导致两个线程都处于阻塞态,也就是产生了死锁

条件变量:

条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的
一个线程等待某个条件满足而被阻塞;
另一个线程中,条件满足时发出“信号”
生产者和消费者模型:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_avail = 0;
/* 消费者线程 */
static void *consumer_thread(void *arg)
{for ( ; ; ) {pthread_mutex_lock(&mutex);//上锁while (g_avail > 0)g_avail--; //消费pthread_mutex_unlock(&mutex);//解锁}return (void *)0;}/* 主线程(生产者) */
int main(int argc, char *argv[])
{pthread_t tid;int ret;/* 初始化互斥锁 */pthread_mutex_init(&mutex, NULL);/* 创建新线程 */ret = pthread_create(&tid, NULL, consumer_thread, NULL);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}for ( ; ; ) {pthread_mutex_lock(&mutex);//上锁g_avail++; //生产pthread_mutex_unlock(&mutex);//解锁}exit(0);
}
主线程生产者一直生产,新线程也一直在循环判断,会导致CPU资源的浪费 
当采用条件变量,当条件未到达,线程处于休眠状态,当满足条件,线程会被唤醒

条件变量初始化

1.宏初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

2.函数初始化

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);attr指针:指向一个 pthread_condattr_t 类型对象,pthread_condattr_t 数据类型用于描述条件变量的属性【和互斥锁一样】

销毁条件变量:

int pthread_cond_destroy(pthread_cond_t *cond);

通知和等待条件变量:

发送信号给一个线程或者多个线程,通知某个共享变量的状态发生了改变

int pthread_cond_broadcast(pthread_cond_t *cond);可以唤醒所有等待的线程int pthread_cond_signal(pthread_cond_t *cond);唤醒至少一个线程,更高效

等待:在收到一个通知之前一直处于阻塞状态

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);cond:条件
mutex:互斥锁的对象

条件变量的判断条件【必须while循环

当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到 互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。譬如示例代码 12.3.2 中, 如果有两个或更多个消费者线程,当其中一个消费者线程从 pthread_cond_wait() 返回后,它会将全局共享变量 g_avail 的值变成 0 ,导致判断条件的状态由真变成假
可能会发出虚假的通知

自旋锁

访问共享资源之前对自旋锁上锁,在访问完成后,释放自旋锁,互斥锁基于自旋锁实现的,自旋锁更为底层

和互斥锁的区别:

互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待
缺点:
当未获得锁的时候,会一直占用CPU的资源。试图对同一自旋锁两次必然会导致死锁,但是互斥锁却不会
使用场景:
自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;
因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能 被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了 CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁

 自旋锁初始化:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);pshared:
⚫ PTHREAD_PROCESS_SHARED:共享自旋锁。该自旋锁可以在多个进程中的线程之间共享
⚫ PTHREAD_PROCESS_PRIVATE:私有自旋锁。只有本进程内的线程才能够使用该自旋锁

销毁自旋锁:

int pthread_spin_destroy(pthread_spinlock_t *lock);

自旋锁加锁和解锁 

int pthread_spin_lock(pthread_spinlock_t *lock);int pthread_spin_trylock(pthread_spinlock_t *lock);     非阻塞加锁int pthread_spin_unlock(pthread_spinlock_t *lock);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>static pthread_spinlock_t spin;//定义自旋锁
static int g_count = 0;static void *new_thread_start(void *arg)
{int loops = *((int *)arg);int l_count, j;for (j = 0; j < loops; j++) {pthread_spin_lock(&spin); //自旋锁上锁l_count = g_count;l_count++;g_count = l_count;pthread_spin_unlock(&spin);//自旋锁解锁}return (void *)0;
}
static int loops;
int main(int argc, char *argv[])
{pthread_t tid1, tid2;int ret;/* 获取用户传递的参数 */if (2 > argc)loops = 10000000; //没有传递参数默认为 1000 万次elseloops = atoi(argv[1]);/* 初始化自旋锁(私有) */pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);/* 创建 2 个新线程 */ret = pthread_create(&tid1, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}ret = pthread_create(&tid2, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}/* 等待线程结束 */ret = pthread_join(tid1, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}ret = pthread_join(tid2, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}/* 打印结果 */printf("g_count = %d\n", g_count);/* 销毁自旋锁 */pthread_spin_destroy(&spin);exit(0);
}

运行结果:

自旋锁会比互斥锁快一点

读写锁

三种状态:
1.读模式下的加锁状态(读加锁状态)

2.写模式下的加锁状态(写加锁状态)

3.不加锁

规则:

当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞
当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止

读写锁初始化

1.宏初始化

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

2.函数初始化

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

销毁读写锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

读写锁上锁和解锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
写加锁模式:其他线程调用会失败
读加锁模式:其他线程调用会成功int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
写加锁模式:其他线程调用会失败
读加锁模式:其他线程调用会失败非阻塞加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
解锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_rwlock_t rwlock;//定义读写锁
static int g_count = 0;
static void *read_thread(void *arg)
{int number = *((int *)arg);int j;for (j = 0; j < 10; j++) {pthread_rwlock_rdlock(&rwlock); //以读模式获取锁printf("读线程<%d>, g_count=%d\n", number+1, g_count);pthread_rwlock_unlock(&rwlock);//解锁sleep(1);}return (void *)0;
}
static void *write_thread(void *arg)
{int number = *((int *)arg);int j;for (j = 0; j < 10; j++) {pthread_rwlock_wrlock(&rwlock); //以写模式获取锁printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);pthread_rwlock_unlock(&rwlock);//解锁sleep(1);}return (void *)0;
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char *argv[])
{pthread_t tid[10];int j;/* 对读写锁进行初始化 */pthread_rwlock_init(&rwlock, NULL);/* 创建 5 个读 g_count 变量的线程 */for (j = 0; j < 5; j++)pthread_create(&tid[j], NULL, read_thread, &nums[j]);/* 创建 5 个写 g_count 变量的线程 */for (j = 0; j < 5; j++)pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);/* 等待线程结束 */for (j = 0; j < 10; j++)pthread_join(tid[j], NULL);//回收线程/* 销毁自旋锁 */pthread_rwlock_destroy(&rwlock);exit(0);
}

运行结果:

 读写锁的属性    

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
获取读写锁的共享属性int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
设置读写锁的共享属性pshared:
⚫ PTHREAD_PROCESS_SHARED:共享读写锁。该读写锁可以在多个进程中的线程之间共享;
⚫ PTHREAD_PROCESS_PRIVATE:私有读写锁。只有本进程内的线程才能够使用该读写锁,这是

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

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

相关文章

Cesium 3DTiles数据格式详解

目录 0 引言1 3DTiles1.1 起源1.2 后缀类型及特点1.2.1 b3dm1.2.2 i3dm1.2.3 pnts1.2.4 cmpt1.2.5 json1.2.6 总结 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;CesiumforUnreal专栏&#x1f4a5; 标题&#xff1a;Cesium 3DTiles数据格式详解❣…

【Netty】Netty核心API及使用

目录 Netty核心APIChannelHandler及其实现类ChannelPipelineChannelHandlerContextChannelOptionChannelFutureEventLoopGroup和实现类NioEventLoopGroupServerBootstrap和BootstrapUnpooled类 Netty入门案例引入依赖Netty服务端编写Netty客户端编写 Netty核心API ChannelHand…

智能优化算法应用:基于龙格-库塔算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于龙格-库塔算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于龙格-库塔算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.龙格-库塔算法4.实验参数设定5.算法结果…

FPGA模块——以太网芯片MDIO读写

FPGA模块——以太网MDIO读写 MDIO接口介绍MDIO接口代码&#xff08;1&#xff09;MDIO接口驱动代码&#xff08;2&#xff09;使用MDIO驱动的代码 MDIO接口介绍 MDIO是串行管理接口。MAC 和 PHY 芯片有一个配置接口&#xff0c;即 MDIO 接口&#xff0c;可以配置 PHY 芯片的工…

读书笔记产品经理学习笔记1-忘掉技术,先看客户需求

技术到产品思维的转换 以前做技术的时候&#xff0c;扮演的角色是怎样多快好省的完成开发。现在做产品了&#xff0c;你得自己定产品方案&#xff0c;让别人来开发。最重要的是先弄清楚客户的需求是什么&#xff0c;要解决什么问题&#xff0c;再看产品怎么设计&#xff0c;然…

你想改win11系统中窗口、菜单等的字体? 微软不想让你改

如果你感觉win11系统中显示的字体不好看&#xff0c;想换。等一等&#xff0c;微软本不想让你改。 Windows 11 在默认情况下并没有提供直接修改系统默认字体的选项&#xff0c;而需要进行注册表或其他高级设置来更改系统字体。这可能是因为微软希望保持系统的一致性和稳定性&a…

[Realtek sdk-3.4.14b]RTL8197FH-VG+RTL8812F WiFi使用功率限制功能使用说明

sdk说明 ** Gateway/AP firmware v3.4.14b – Aug 26, 2019**  Wireless LAN driver changes as:  Refine WiFi Stability and Performance  Add 8812F MU-MIMO  Add 97G/8812F multiple mac-clone  Add 97G 2T3R antenna diversity  Fix 97G/8812F/8814B MP issu…

可狱可囚的爬虫系列课程 07:BeautifulSoup4(bs4)库的使用

前面一直在讲 Requests 模块如何使用&#xff0c;那都是在请求阶段要做的事情&#xff0c;相信很多网友都在等一个能够开始爬网站信息的教程&#xff0c;今天它来了&#xff0c;今天我要给大家讲一个很简单易懂的库&#xff1a;BeautifulSoup4。 一、概述&安装 Beautiful…

KylinV10 安装 MySQL 教程(可防踩雷)

KylinV10 安装 MySQL 教程&#xff08;可防踩雷&#xff09; 1、直接用 apt 快捷安装 MySQL $ sudo apt-get update #更新软件源 $ sudo apt-get install mysql-server #安装mysql然后你会发现&#xff0c;KylinV10 安装畅通无阻&#xff0c;并没有设置密码的场景&#xff0c…

你相信光吗?2D 后效与光照技术分享!

“ 很多朋友提到后效&#xff0c;就会想起那些 3D 游戏大作&#xff0c;但实际上&#xff0c;后效在 2D 游戏开发中的应用也是非常广泛的。恰当地使用后效&#xff0c;可以使一款 2D 游戏的画质提升好几个台阶。今天邀请到了社区大佬 wing&#xff0c;给大家分享一下 2D 后效框…

MyBatis ORM映射

MyBatis只能自动维护库表”列名“与”属性名“相同时的对应关系&#xff0c;二者不同时无法自动ORM 因此需要使用到ORM映射。 共有两种解决办法&#xff1a;1.列的别名 2.结果映射 1.列的别名 在SQL中使用 as 为查询字段添加列别名&#xff0c;以匹配属性名 public List<…

nodejs+vue+微信小程序+python+PHP高校成绩分析系统-计算机毕业设计推荐

综合购物商城管理经历和对网上信息归纳整理的结果&#xff0c;在实际应用中&#xff0c;将用户分为两种&#xff1a;管理员和高校成绩分析系统综合网络空间开发设计要求。目的是将高校成绩分析从传统管理方式转换为在网上管理&#xff0c;完成高校成绩分析管理的方便快捷、安全…

pyCharm 创建一个FastApi web项目,实现接口调用

FastApi和Django区别 我这边演示项目使用的fastApi作为web框架&#xff0c;当然主流一般都是使用Django做web框架&#xff0c;但是Django是一个重量级web框架他有很多组件&#xff0c;如授权&#xff0c;分流等全套web功能。我这边呢只需要有个接口可以被别人调用&#xff0c;…

【超详细前后端项目搭建】前端vue3+ts项目(引入ElementPlus、Axios)、后端springboot搭建(创建接口操作mysql数据库)实现前后端联调

目录 前言一、前端项目1、使用vue脚手架创建项目1.1检查vue版本1.2 使用vue脚手架创建项目 2、删除项目多余文件&#xff0c;修改配置项目2.1、删除以下文件2.1、在views下创建index文件2.2、修改router/index.ts路由文件&#xff1a;2.3、修改App.vue文件&#xff1a;2.4、初始…

气候变化与环境保护:全球研究与未来趋势

导言 气候变化和环境保护是当今社会亟待解决的全球性难题。本文将深入探讨这一主要流行研究方向的发展历程、遇到的问题、解决过程&#xff0c;以及未来的可用范围&#xff0c;着重分析在各国的应用和未来的研究趋势&#xff0c;以探讨在哪些方面能够取得胜利&#xff0c;以及在…

PySpark中DataFrame的join操作

内容导航 类别内容导航机器学习机器学习算法应用场景与评价指标机器学习算法—分类机器学习算法—回归机器学习算法—聚类机器学习算法—异常检测机器学习算法—时间序列数据可视化数据可视化—折线图数据可视化—箱线图数据可视化—柱状图数据可视化—饼图、环形图、雷达图统…

期货开平规则(期货交易开平规则解析)

什么是期货开平规则 期货开平规则&#xff0c;简单来说是指期货交易中的开仓和平仓所遵循的一系列规定。具体而言&#xff0c;开仓是指买入或卖出期货合约&#xff0c;建立一个新的持仓&#xff1b;平仓则是指买入或卖出相应数量的期货合约&#xff0c;用以解除原有持仓。开平…

什么是数据仪表板?数据可视化仪表盘怎么制作?

在数据经济时代&#xff0c;分析数据是每个企业做出最佳决策的关键。但是&#xff0c;手动分析和解释大量数据是不可行的。数据可视化对于分析数据中存在的各种有价值信息至关重要&#xff0c;包括可见趋势和隐藏趋势等。仪表盘显示可视化趋势和信息&#xff0c;例如 KPI、趋势…

简易实现 STL--list

实现 list 的主要思想及过程 首先&#xff0c;实现过程中的所有代码必须放在自己定义的命名空间中。 定义一个结点的结构体类模板&#xff0c;结点的数据类型就应该是模板类型 T&#xff0c;定义的 next指针和 prev指针都应该是模板指针类型&#xff0c;并且结构体类中药有构…

2017年第六届数学建模国际赛小美赛A题飓风与全球变暖解题全过程文档及程序

2017年第六届数学建模国际赛小美赛 A题 飓风与全球变暖 原题再现&#xff1a; 飓风&#xff08;也包括在西北太平洋被称为“台风”的风暴以及在印度洋和西南太平洋被称为“严重热带气旋”&#xff09;具有极大的破坏性&#xff0c;往往造成数百人甚至数千人死亡。   许多气…