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:指定等待的线程ID

retval:如果不为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_ENABLE


eg. 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 万次
        else
        loops = 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 万次
        else
        loops = 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:私有读写锁。只有本进程内的线程才能够使用该读写锁,这是

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

小鹅通基于 TSE 云原生 API 网关的落地实践

导语 2023腾讯全球数字生态大会已于9月7-8日完美落幕&#xff0c;40专场活动展示了腾讯最新的前沿技术、核心产品、解决方案。 微服务与消息队列专场&#xff0c;我们邀请到了小鹅通的基础架构组负责人黄徐震为我们带来了《小鹅通基于 TSE 云原生网关的落地实践》的精彩演讲。…

TCP 核心工作机制

TCP 的核心知识&#xff1a;如何保证传输可靠 如何提高传输效率 如何保证传输可靠&#xff1a;确认应答机制 超时重传机制 如何提高传输效率&#xff1a;滑动窗口机制、流量控制机制、延时应答机制、捎带确认机制、拥塞控制机制 可靠机制 TCP的可靠性主要是通过 确认应答 …

多门店自助点餐+外卖二合一小程序系统源码:自助点餐+外卖配送 带完整搭建教程

互联网的普及和移动支付的便捷&#xff0c;餐饮行业也在经历着数字化转型。小编来给大家介绍一款多门店自助点餐外卖二合一小程序&#xff0c;带完整的搭建教程。 以下是部分代码示例&#xff1a; 系统特色功能一览&#xff1a; 1.多门店管理&#xff1a;支持一个平台管理多个…

华为OS与麒麟OS:华为自研操作系统的对决

导言 在移动操作系统领域&#xff0c;华为OS和麒麟OS代表了华为在自主研发方面的努力。本文将深入探讨这两个操作系统的特点、竞争关系以及它们在用户体验、生态系统建设等方面的差异。 1. 背景与起源 华为OS的诞生&#xff1a; 华为OS是华为公司为应对外部环境而自主…

【音视频 | AAC】AAC音频编码详解

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

网络基础【网线的制作、OSI七层模型、集线器、交换机介绍、路由器的配置】

目录 一.网线的制作 1.1.网线的标准 1.2.水晶头的做法 二.OSI七层模型、集线器、交换机介绍 集线器&#xff08;Hub&#xff09;&#xff1a; 交换机&#xff08;Switch&#xff09;&#xff1a; 三.路由器的配置 3.1.使用 3.2.常用的功能介绍 1、如何管理路由器 2、家…

SVN搭建指导

环境 centos 7.9 SVN安装方式一&#xff1a;yum 1.1 http服务 至今还没有搞定网页版&#xff0c;网页版需要搭建apache http服务。遇到如下问题&#xff1a; centos - svn: Could not open the requested SVN filesystem - Stack Overflow 在试了加777权限&#xff0c;加a…

Hal深入实战/perfetto-systrace实战/SurfaceFlinger合集-安卓framework开发实战开发

背景 hi&#xff0c;粉丝朋友们&#xff1a; 大家好&#xff01; 下面来介绍一下新的framework专题halperfettosurafceflinger&#xff0c;这个专题主要就是分为3大块&#xff0c;但是彼此直接又是相互关联的。 比如surfaceflingre模块深入分析需要用到hal相关的模块&#xff…

Git报错x509: certificate signed by unknown authority

下载报错&#xff1a; Error downloading object: model-00001-of-00008.safetensors (ed3ac49): Smudge error: Error downloading model-00001-of-00008.safetensors (ed3ac4983f682a999b0e4b6f072aad294c4fd9a7e968e90835ba5c4b466d3c7c): LFS: Get https://cdn-lfs.huggin…

百度侯震宇:AI原生与大模型将从三个层面重构云计算

12月20日&#xff0c;2023百度云智大会智算大会在北京举办&#xff0c;大会以「大模型重构云计算&#xff0c;Cloud for AI」为主题&#xff0c;深度聚焦大模型引发的云计算变革。 百度智能云表示&#xff0c;为满足大模型落地需求&#xff0c;正在基于「云智一体」战略重构…

〖大前端 - 基础入门三大核心之JS篇(58)〗- 面向对象案例

说明&#xff1a;该文属于 大前端全栈架构白宝书专栏&#xff0c;目前阶段免费&#xff0c;如需要项目实战或者是体系化资源&#xff0c;文末名片加V&#xff01;作者&#xff1a;哈哥撩编程&#xff0c;十余年工作经验, 从事过全栈研发、产品经理等工作&#xff0c;目前在公司…

饥荒Mod 开发(十七):手动保存和加载,无限重生

饥荒Mod 开发(十六)&#xff1a;五格装备栏 饥荒Mod 开发(十八)&#xff1a;Mod 添加配置选项 饥荒游戏会自动保存&#xff0c;本来是一个好的机制&#xff0c;但是当角色死亡的时候存档会被删除&#xff0c;又要从头开始&#xff0c;有可能一不小心玩了很久的档就直接给整没了…

C# NPOI导出dataset----Excel绘制Chart图表

仅限XLSX 2007以后版本&#xff08;2007之前版本不支持&#xff09; 1、判断文件夹是否存在&#xff0c;不存在则创建 //Application.StartupPath当前项目根目录 if (!Directory.Exists(Application.StartupPath "\Excel")) { …

用全志R128复刻自平衡赛车机器人,还实现了三种不同的操控方式

经常翻车的朋友们都知道&#xff0c;能在翻车后快速摆正车身的车才是好车。 就像动画《四驱兄弟》中展现的那样&#xff0c;在比赛中需要跟着赛车一起跑圈&#xff0c;而且赛车如果被撞翻还需要重新用手扶正&#xff0c;所浪费的时间非常影响比赛结果。 如果小豪和小烈可以拥有…

云原生扫盲篇

What 云原生加速了应用系统与基础设施资源之间的解耦,向下封装资源以便将复杂性下沉到基础设施层;向上支撑应用,让开发者更关注业务价值 云原生是一种构建和运行应用程序的方法,也是一套技术体系和方法论. Cloud 表示应用程序位于云中而不是传统的数据中心Native表示应用程序从…

基于STM32的DHT11温湿度传感器与LCD显示器的集成设计

在本文中&#xff0c;我们将详细介绍如何基于STM32微控制器实现DHT11温湿度传感器与LCD显示器的集成设计。我们将包括硬件连接、软件编程以及涉及的STM32库函数和相关知识。这个项目旨在帮助您理解如何使用STM32来读取DHT11温湿度传感器的数据&#xff0c;并将数据显示在LCD显示…

qt-C++笔记之使用QLabel和QPushButton实现一个bool状态的指示灯

qt-C笔记之使用QLabel和QPushButton实现一个bool状态的指示灯 code review! 文章目录 qt-C笔记之使用QLabel和QPushButton实现一个bool状态的指示灯1.QPushButton实现2.QLabel实现2.QLabel实现-对错符号 1.QPushButton实现 运行 代码 #include <QtWidgets>class Ind…

msvcp120.dll丢失的多种详细有效解决方法

在计算机使用过程中&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是“msvcp120.dll丢失”。那么&#xff0c;msvcp120.dll到底是什么&#xff1f;为什么会出现丢失的情况&#xff1f;丢失后会对电脑产生什么影响&#xff1f;本文将为您详细解答这些问题&#…

计算机网络 运输层下 | TCP概述 可靠传输 流量控制 拥塞控制 连接管理

文章目录 3 运输层主要协议 TCP 概述3.1 TCP概述 特点3.2 TCP连接RSVP资源预留协议 4 TCP可靠传输4.1 可靠传输工作原理4.1.1 停止等待协议4.1.2 连续ARQ协议 4.2 TCP可靠通信的具体实现4.2.1 以字节为单位的滑动窗口4.2.2 超时重传时间的选择4.2.3 选择确认SACK 5 TCP的流量控…

MAC苹果笔记本电脑如何彻底清理垃圾文件软件?

苹果电脑以其流畅的操作系统和卓越的性能而备受用户喜爱。然而&#xff0c;随着时间的推移&#xff0c;系统可能会积累大量垃圾文件&#xff0c;影响性能。本文将介绍苹果电脑怎么清理垃圾文件的各种方法&#xff0c;以提升系统运行效率。 CleanMyMac X是一款专业的Mac清理软件…