当前位置: 首页 > article >正文

线程基础编程

早期的计算机只能执行一个任务,一旦任务完成,计算机就会等待下一个任务。这种模型效率低下,无 法充分利用计算机的性能。

随着计算机技术的发展,操作系统开始支持多进程模型,即同时执行多个任务。每个任务被称为一个进 程,每个进程都有自己的内存空间和资源。这种模型提高了计算机的并发执行能力,但仍然存在资源共 享的问题(进程间通信开销很大(学习过程中会刻意植入概念)。

为了进一步提高计算机的并发执行能力以及资源更高效利用,多线程模型被引入。每个进程可以拥有多个线程,这些线程共 享进程的内存空间和资源。多线程模型可以更好地利用计算机的资源,提高程序的执行效率

8核心:指的一个完整的cpu芯片有8个cpu核心,每个核心可以执行一个程序的代码。

16线程:一个完整cpu最多有16个线程同时在执行。每个cpu核心可以同时处理两个线程。以c语言程序为例。一个线程可以执行一个c语言程序。

总结:一个cpu同一时间可以执行很多个线程,每个线程都可以执行代码

一、线程概念
1、什么是线程

(1)官方定义

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中实际运行工作的单位。每个线程都是进程的一部分,包含在进程的地址空间内,可以利用进程所拥有的资源

理解线程:为计算机种实际执行工作的最小单位。

(2)线程和进程之间的关系

  • 包含进程是系统中资源分配的最小单元,具有独立的地址空间。而线程是执行(cpu调度)的最小单元,同一进程中的多个线程共享进程的资源,如内存和文件。
  • 通信:进程间通信需要特定的IPC机制,并且进程间相互独立。而线程之间可以直接通信,因为它们共享同一个进程内的内存地址空间(需要满足这几个线程是同一进程的)。
  • 开销:创建新进程的资源和时间开销相对较大。线程相对轻量,创建和切换线程的开销小于进程。
  • 调度:操作系统独立调度进程。操作系统在进程内部调度线程,使得在执行任务时能并行处理,提高执行效率

一个进程里可以包含多个线程,同一个进程里的不同线程之间可以共享资源

(3)如何理解线程

工厂例子:一个工厂里有很多工人,工人们共享同一个工厂里的资源来进行工作,而每个工人可以做不同的工作,比如一个工人负责零件制造生产线,另一个工人则负责零件焊接,并且多个工人可以同时进行工作。那么最终多个工人的协同工作可以生产一个产品。

这里面工厂就好比进程,一个工人就好比线程。进程里可以包含多个线程,而每个线程都共享同一个进程里的资源,每个线程都可以负责一个任务,并且可以同时进行。比如qq音乐用一个进程来执行,那么一个线程可以用来加载桌面应用界面,一个线程用于进行数据获取,一个线程用于进行音频的播放和暂停等。

2、线程的总结
  • 线程:线程是计算机中执行指令(运算调度)的最小单位,线程是存在于正在运行的程序(进程)之中。每个程序至少有一个线程(默认线程(主线程),操作系统自动创建),一个进程可以有多个线程。比如执行a.out就相当于操作系统新建一个进程并自动创建一个线程(主线程)来执行main函数代码。
  • 特点
    • 独立性:同一个程序里的不同线程之间是相互独立的,即一个线程的结束或崩溃不会影响到其他线程的执行
    • 数据共享:同一个程序里的所有线程都可以直接使用该程序里的资源。比如全局变量等。
    • 并行处理:同一个程序,如果有多个线程,那么这几个线程是同时执行的(具体谁先执行,是由cpu和操作系统调度算法决定)
3、线程的好处

(1)资源消耗更少

  • 内存占用:线程之间共享进程的内存空间,包括代码段、数据段和打开的文件等。这意味着创建一个新线程所需的内存远少于创建一个新进程,因为后者需要为其分配独立的内存空间。
  • 创建和销毁开销:线程的创建和销毁比进程更快、更节省资源,因为进程需要进行更多的内存和资源分配。

(2)数据共享、通信更方便

  • 数据共享:由于线程共享同一进程的内存空间,它们之间的数据共享非常方便,不需要特殊的IPC(进程间通信)机制。这使得线程间的数据交换和状态同步更加高效。
  • 通信成本:线程间的通信成本远低于进程间通信。进程间通信需要操作系统介入,进行上下文切换,而线程间可以直接通过读写共享内存来通信。

(3)提高程序响应性

  • 用户界面响应:在用户界面(UI)密集的应用程序中,多线程可以在一个线程中处理用户交互,而在另一个线程中执行后台任务,从而保持应用程序的响应性。
  • 并发操作:多线程允许程序在等待某些操作(如I/O操作)完成时继续执行其他任务,从而提高程序的整体效率和用户体验。

(4)更高的效率

  • 利用多核处理器:在多核处理器上,多线程能够更有效地利用计算资源。通过将不同线程分配到不同的处理器核心上,可以实现真正的并行计算,从而提高程序的执行效率。

(5)程序设计模型简化

  • 简化复杂操作:对于某些问题,使用多线程可以简化程序设计。例如,在网络服务器或并发处理应用中,为每个客户端连接或任务分配一个线程可以简化编程模型,使得代码更容易理解和维护。
4、多线程和多进程

(1)多进程编程

多进程编程是指一个应用程序使用两个或多个并行运行的进程来执行任务。每个进程在操作系统中作为一个独立的实体存在,拥有自己的地址空间、系统资源和数据。进程间通常通过进程间通信(IPC)机制(如管道、消息队列、信号量等)来交换信息。

特点分析

  • 隔离性:每个进程有自己的内存和资源,一个进程的崩溃不会直接影响到其他进程。
  • 资源需求:创建进程比线程更消耗资源,因为每个进程需要独立的内存和系统资源。
  • 通信成本:进程间的通信成本较高,因为它们在不同的内存空间中,需要操作系统介入来实现数据交换。

(2)多线程编程

多线程编程是指在单个应用程序内部创建和管理两个或多个并行运行的线程。线程是进程的执行单元,它们共享父进程的地址空间和资源,如内存和文件句柄等。虽然线程共享资源,但每个线程有自己的执行堆栈和程序计数器。

特点分析

  • 资源共享:线程间自然共享进程资源,使得数据共享和通信更方便。
  • 开销较小:相比进程,创建和管理线程的开销小,因为它们共享许多资源。
  • 同步问题:由于资源共享,线程间的同步和并发控制变得复杂,需要使用互斥锁、读写锁、条件变量等机制来避免冲突和竞态条件。

(3)两个的应用场景和区别

对比

  • 隔离性 vs. 效率:多进程提供更好的隔离性,但以较高的资源需求和通信成本为代价;而多线程提供了更高的资源效率,但需要更小心地管理资源共享和同步问题。
  • 安全性 vs. 速度:多进程由于隔离性较高,通常被认为更安全,适合需要隔离不同任务的场景;多线程则因其轻量和快速响应的特性,适合对性能要求高的场景。

应用场景

  • 多进程:

    • 浏览器每个标签页都是一个进程
    • 操作系统中的服务和守护进程通常是独立的进程,以确保稳定性。
    • 手机系统的每个app应用都是一个进程
  • 多线程

    • 各种gui应用(界面应用,比如qq音乐)

    • 各种游戏等(比如英雄联盟等)

      • 使用多线程可以减少游戏的延迟,提高响应速度,这对于需要实时交互的游戏尤为重要。例如,在英雄联盟这种需要快速响应玩家操作的游戏中,一个线程可能专门用于处理用户的输入,确保游戏控制的反应速度,而不会因为渲染或网络延迟而受到影响。

        • 渲染线程:负责绘制游戏画面到屏幕上。
        • 逻辑线程:处理游戏逻辑,如玩家输入、AI 决策等。
        • 声音线程:用于处理音频的播放。
        • 网络线程:管理玩家之间的网络通信或与服务器的数据同步。
      • 然而,这并不排除游戏在某些情况下可能使用多进程,例如游戏客户端和配套的聊天或更新服务可能是运行在单独进程中的。但核心游戏循环和功能性的部分,如图形渲染和游戏逻辑处理,通常是在同一个进程中的多个线程来完成的

    • 数据库(mysql,sql server)

二、线程的使用和管理(c99)

如果要使用线程需要引入对应的头文件。同时后续在用gcc命令时需要加上-lpthread参数来表示链接线程函数库,不然会报一下错误,而无法使用

#include <pthread.h>
gcc 文件名 -lpthread
1、创建线程

(1)基本语法

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

    1. pthread_t *thread
      • 这是一个指向 pthread_t 类型变量的指针,用于存储新创建的线程的标识符。
      • pthread_create() 成功返回时,这个变量将包含新线程的唯一标识符。
      • 调用者需要分配这个变量(通常是一个简单的局部变量),并将其地址传递给 pthread_create()
    2. const pthread_attr_t *attr
      • 这是一个指向 pthread_attr_t 类型变量的指针,用于指定新线程的属性。
      • pthread_attr_t 是一个结构体类型,用于存储线程的调度策略、堆栈大小、是否绑定到特定的处理器核等属性。
      • 如果调用者不需要设置特殊的线程属性,可以传递 NULL,此时将使用默认的线程属性。
    3. void *(*start_routine) (void *)
      • 这是一个函数指针,指向新线程将要执行的函数。
      • 这个函数必须接受一个 void* 类型的参数,并返回一个 void* 类型的结果。
      • 函数的参数通常用于传递数据给新线程,而返回值则可以用于线程间的通信或作为线程的退出状态。
    4. void *arg
      • 这是一个 void* 类型的参数,用于传递给新线程执行函数的参数。
      • 调用者可以将任何类型的数据转换为 void* 类型,并在新线程的函数中将其转换回原始类型。
      • 如果不需要传递任何参数给新线程的函数,可以传递 NULL
  • 返回值

    • 如果成功返回 0

    • 如果失败,将返回一个非零的错误代码,以指示失败的原因 strerror()

2、结束一个线程:pthread_exit

用于退出一个线程。当一个线程调用此函数时,它会结束自己的执行,但不会影响其他线程。

(1)基本语法

void pthread_exit(void *retval);
  • 参数
    • retval:线程结束时的状态,如果不传递填写NULL。其他线程可以通过pthread_join时会返回此状态

(2)和exit的区别

  • pthread_exit是线程级的退出,而exit是进程级的退出。使用pthread_exit可以让线程清理后结束运行,而不会影响其他线程或进程;而使用exit则会结束所有线程并关闭整个程序。通常在多线程程序中,当你只需要结束一个线程的时候使用pthread_exit,而当你需要结束整个程序时使用exit

(3)注意

  • pthread_exit仅仅是终止线程的执行;如果其他线程不调用pthread_join来等待该线程的结束,并且该线程也没有被设置为分离状态(pthread_detach),那么这个线程在退出后其资源仍然会保留(成为僵尸线程),直到这两个条件中的一个被满足,其资源才会被释放
3、等待线程结束:pthread_join

pthread_create调用成功以后,新线程和老线程谁先执行用户是不知道的,这一块取决与操作系统对线 程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似于多进 程编程中的waitpid

(1)基本语法

int pthread_join(pthread_t thread, void **retval);
  • 参数
    • thread: 目标回收的线程号
    • retval: 线程结束时pthread-exit或线程函数通过return返回的状态 如果不考虑,则填写NULL
  • 返回值
    • 成功:返回0
    • 失败:返回错误码

(3)示例代码 ( 通过join得到线程的执行结果:返回值是 int )

  • 示例代码:

    #include <stdio.h>
    #include <stdbool.h>
    #include <string.h>
    #include <stdlib.h>
    #include <pthread.h>// 注意要引入一些相关的头文件:
    #include <stdlib.h>
    #include <unistd.h>   // sleep
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>// 写一个线程函数
    void* one(void* args){for(int i = 1; i <= 50; i++){//sleep(1);usleep(100000);if(i == 20){//pthread_exit( NULL );  // 线程级别的结束//exit(0);				 // 进程级别的结束}printf("one-->%d\n", i);}return NULL;
    }
    int main(int argc, char *argv[]) {pthread_t pid;int result =  pthread_create( &pid , NULL, one, NULL);if(result != 0){perror("线程创建失败!");exit(EXIT_FAILURE);}printf("创建的线程id是:%ld\n", pid );for(int i = 1; i <= 50; i++){//sleep(1);usleep(100000);printf("main==========>%d\n", i);}// 等待线程结束pthread_join( pid, NULL );	puts("----------- end -------------");	return 0;
    }
    
  1. 主线程的关键作用
    • 主线程结束时,整个进程会被销毁,所有子线程都会被强制终止。
    • 使用 pthread_join 可以确保主线程在子线程完成后再退出。
  2. pthread_join 的双重意义
    • 同步执行顺序:确保子线程的代码完整执行。
    • 资源管理:避免内存泄漏,回收线程资源。
4、得到线程返回值 :pthread_join
  • pthread_join 的另一个核心功能是获取线程函数的返回值(或退出状态)。这个机制允许线程之间通过返回值传递数据,是线程间通信的一种简单方式。

  • 示例代码:

    通过pthread_join() 得到线程函数的返回值 :

    // 写一个线程函数
    void* one(void* args){int *p = (int*)args;for(int i = 1; i <= *p; i++){//sleep(1);usleep(100000);printf("one-->%d\n", i);}// 比如:函数执行完后,有一个结果:889900int result = 889900;int* num = malloc(sizeof(int));//返回动态内存分配的指针*num = result;return num ;
    }/*局部变量 result 存储在线程的栈帧中。
    当线程函数返回时,栈帧被销毁,result 的内存空间被回收。
    主线程通过 pthread_join 拿到的指针指向已释放的内存,访问它会导致未定义行为(如段错误、数据错乱)。*/int main(int argc, char *argv[]) {pthread_t pid;int param = 18;// 传递参数给线程函数。int result =  pthread_create( &pid , NULL, one,  (void*)&param);if(result != 0){perror("线程创建失败!");exit(EXIT_FAILURE);}printf("创建的线程id是:%ld\n", pid );for(int i = 1; i <= 50; i++){//sleep(1);usleep(100000);printf("main==========>%d\n", i);}// 功能1:等待线程结束// 功能2:接受线程函数结束之后的返回值int* r = NULL;pthread_join( pid, (void**)&r );// 得到结果:printf("函数运行结果:%d\n", *r );// 要释放free(r);puts("----------- end -------------");	return 0;
    }
    
  1. 内存管理
    • 如果返回的是动态分配的内存(如 malloc),主线程必须负责释放,否则会导致内存泄漏。
    • 避免返回线程栈上的局部变量指针(线程结束后内存会被回收)。
  2. 类型转换
    • void* 本质是无类型指针,需正确转换为原始类型(如 (char*)(int))。
  3. 错误处理
    • 如果线程因异常终止(如调用 abort()),pthread_join 可能无法获取预期的返回值。
  4. 由主线程预先分配内存
void* one(void* args) {int* result_ptr = (int*)args; // 由主线程传入的指针*result_ptr = 889900;return NULL;
}int main() {int result;pthread_create(&tid, NULL, one, &result); // 传入主线程的变量地址pthread_join(tid, NULL);printf("结果: %d\n", result); // 直接读取,无需freereturn 0;
}

pthread_join 使用二级指针void**)来接收线程函数的返回值。这是因为它需要修改调用者提供的指针变量,使其指向线程返回的内存地址。

  • 线程函数返回的是 void* 类型的指针(如 malloc 分配的内存地址)。

  • pthread_join 需要将这个指针值写入调用者提供的变量中,因此需要传入该变量的地址(即二级指针)。

  • int main() {pthread_t tid;void* status; // 调用者提供的变量(一级指针)// 创建线程...pthread_create(&tid, NULL, thread_function, NULL);// &status 是二级指针(void**),指向调用者的 status 变量pthread_join(tid, &status); // 将线程返回的 void* 写入 status// 使用 status...int* result = (int*)status;printf("结果: %d\n", *result);
    }
    
  • 内存交互过程

  1. 线程函数返回

    void* thread_function(void* arg) {int* result = malloc(sizeof(int));*result = 42;return result; // 返回堆内存地址(void* 类型)
    }
    
  2. pthread_join 执行

    void* status; // 调用者的变量(未初始化)
    pthread_join(tid, &status); // 将线程返回的 void* 写入 &status 指向的内存(即 status 变量)
    
  3. 内存状态变化

    调用前:               调用后:
    status ──> NULL      status ──> [0x12345678](线程返回的 malloc 地址)
    

就是需要往status内传入值,就是改变status的值,而改变它的值需要用它的指针,但是它本来就是一个指针,所以用到二级指针
pthread_join 接收 &status(二级指针),并将线程返回的指针值写入 status 变量。

  • 关键细节
  1. 为什么需要 &status
    • pthread_join 的第二个参数类型是 void**(二级指针),需要传入 status 的地址(&status)。
    • 通过 &statuspthread_join 可以修改 status 变量本身的值(即让它指向线程返回的内存)。
  2. 变量作用域
    • status 是主线程的局部变量,但 pthread_join 可以通过其地址(&status)修改它。
    • 线程返回的指针值(如 malloc 分配的地址)被存储在 status 中,主线程可以安全访问。
5、设置线程分离属性:pthread_detach

(1)基本语法

int pthread_detach(pthread_t thread);`
  • 功能描述:当一个线程被设置为分离状态后,系统会自动管理该线程的资源。具体来说,当这个线程终止时,系统会自动回收其占用的资源(如线程的堆栈和线程描述符),而无需其他线程调用 pthread_join() 函数来显式回收。这样做的好处是可以简化多线程编程中的资源管理,减少程序出错的可能性

    注意:设置成隔离线程(和join是互斥的操作)

  • 参数:thread:目标线程号

  • 返回值

    • 成功:返回 0
    • 失败:返回错误码
  • 示例:

1. 自动资源回收

  • 默认行为:线程结束后,资源(如栈空间)不会自动释放,除非其他线程调用 pthread_join

  • 分离后

    :线程结束时,系统自动回收其资源,无需pthread_join

    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_detach(tid); // 设置线程为分离状态
    // 线程结束后自动释放资源,无需 pthread_join
    

2. 避免资源泄漏

  • 场景:当你不关心线程的返回值,且不想等待它结束时,可使用 pthread_detach 避免内存泄漏。

  • 对比

    // 不使用 detach(可能导致资源泄漏)
    pthread_create(&tid, NULL, thread_func, NULL);
    // 主线程未调用 pthread_join,线程结束后资源未释放// 使用 detach(安全释放资源)
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_detach(tid); // 线程结束后自动释放资源
    

3. 后台线程(Daemon 线程)

  • 适用场景

    :某些线程需要长期运行(如监控、日志),且不需要与主线程同步。

    void* daemon_thread(void* arg) {while (1) {// 持续执行监控任务...}return NULL;
    }// 创建并分离后台线程
    pthread_t daemon;
    pthread_create(&daemon, NULL, daemon_thread, NULL);
    pthread_detach(daemon); // 让线程在后台独立运行
    
6、获取自己的线程号:pthread_self

返回调用线程的ID。这个值与创建该线程的pthread_create调 用中的thread(线程号)中返回值相同

(1)基本语法

pthread_t pthread_self();
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);  // 创建线程并存储ID到tid// 在线程函数内部:
void* thread_func(void* arg) {pthread_t self = pthread_self();// self 的值与外部的 tid 完全相同return NULL;
}
7、申请结束一个线程: pthread_cancel

(1)基础语法

申请结束一个线程 (向线程发送一个取消请求。目标线程是否以及何时对取消请 求做出反应取决于该线程控制的两个属性:状态和类型。)

int pthread_cancel(pthread_t pid)
  • 参数
    • 需要结束的线程号
  • 返回值
    • 成功:0,只能说成功提交了取消请求,不能说取消成功。具体是否取消成功是受线程自身的取消状态取消类型属性,以及线程是否达到了取消点这些来综合决定的。要确定目标线程是否真正被取消,其他线程需要调用pthread_join并检查传出的状态是否为PTHREAD_CANCELED
    • 失败:返回非0 的错误代码。情况有可能是一个无效id或线程已终止,或该线程已被设置为不响应取消请求。

(2)注意

  • 一般线程什么时候是系统管控的,系统会针对线程的取消机制提供了取消点机制。取消点是线程可能检查取消请求并决定是否退出的预定义的程序执行点,例如系统调用或者库函数(比如read、write函数等)

  • 如果要不响应取消可以使用pthread_setcancelstate来设置状态。具体见下

(3)线程取消机制设置

  • 设置是否取消:pthread_setcancelstate

    • 语法
    int pthread_setcancelstate(int state, int *oldstate);
    

    参数

    1. state:指定新的取消状态,接受两个值:
      • PTHREAD_CANCEL_ENABLE:设置线程为可取消状态,这是默认设置。线程可以在任何取消点(以及如果设置为异步取消类型,那么几乎在任何执行点)响应取消请求。
      • PTHREAD_CANCEL_DISABLE:设置线程为不可取消状态,即忽略取消请求。线程不会响应取消请求,直到取消状态被更改为 PTHREAD_CANCEL_ENABLE
    2. oldstate
      • 一个指向 int 的指针,用于存储调用此函数之前的取消状态。如果对之前的状态不感兴趣,可以传递 NULL
  • 设置线程取消类型

    • 基础语法
pthread_setcanceltype(int type, int *oldtype);
  • ​ 功能:

    pthread_setcanceltype() 函数用于设置线程的取消类型(cancel type),即线程在接收到取消请求时,是立即响应(异步取消)还是等待执行到下一个取消点再响应(延迟取消)。

    • 参数
      • type参数可以是:
        • PTHREAD_CANCEL_DEFERRED(延迟取消,只在取消点检查取消请求)
        • PTHREAD_CANCEL_ASYNCHRONOUS(允许线程在任何非取消点立即被取消)
      • oldtype:和pthread_setcancelstate一样,如果oldtype不是NULL,之前的取消类型会被存储在oldtype指向的位置
三:线程练习:
  • 卖票问题 有100张票,两个或叁个线程同时卖票。请写出代码实现。
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>// 注意要引入一些相关的头文件:
#include <stdlib.h>
#include <unistd.h>   // sleep
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int num = 100;// 共享资源:总票数void* sale(void* args)
{char* name = (char*)args;//传入的参数,窗口名称int count = 0;// 本窗口售出票数while(true){if(num > 0) // 检查是否有票{count++;usleep(100000);// 模拟处理时间(引入线程安全问题)num--;// 修改共享资源(非原子操作)printf("%s窗口卖第:%d 张票,还余%d张票\n", name,  count,  num);}else {break;}}printf("%s窗口一共卖了%d张票\n", name, count);	
}//线程安全问题:usleep 导致线程可能在检查后被调度走,其他线程修改 num 为 0,原线程继续执行 num-- 导致负数。int main(int argc, char *argv[]) {pthread_t one, two, three ;// 创建三个抢票线程,决定了线程 ID(one, two, three)的初始化顺序,但线程 ID 本身与执行顺序无关。if(pthread_create( &one , NULL, sale , "12306") != 0){perror("线程创建失败!");exit(EXIT_FAILURE);}if(pthread_create( &two , NULL, sale , "美团抢票") != 0){perror("线程创建失败!");exit(EXIT_FAILURE);}if(pthread_create( &three , NULL, sale , "多多抢票") != 0){perror("线程创建失败!");exit(EXIT_FAILURE);}// 等待所有线程结束pthread_join(one, NULL);pthread_join(two, NULL);pthread_join(three, NULL);printf("最后的结果是:%d\n", num );puts("----------- end -------------");	return 0;
}
  • pthread_create 创建三个线程,传入不同的窗口名称作为参数。
  • pthread_join 确保主线程等待所有子线程结束后再继续执行。

线程安全问题分析

典型竞态条件场景

  1. 时间点 T1:线程 A 检查 num > 0(此时 num=1),进入条件分支。
  2. 时间点 T2:线程 A 调用 usleep 被挂起,线程 B 执行。
  3. 时间点 T3:线程 B 检查 num > 0(仍为 1),进入条件分支,执行 num--num=0),打印 “售出第 1 张,余 0 张”。
  4. 时间点 T4:线程 A 恢复执行,继续 num--num=-1),打印 “售出第 1 张,余 - 1 张”。

原子性(Atomicity) 是指一个操作或一组操作在执行过程中不可被中断,要么全部执行完成,要么完全不执行,不会出现中间状态。这个概念类似于数据库事务中的 “原子性”。

相关文章:

线程基础编程

早期的计算机只能执行一个任务&#xff0c;一旦任务完成&#xff0c;计算机就会等待下一个任务。这种模型效率低下&#xff0c;无 法充分利用计算机的性能。 随着计算机技术的发展&#xff0c;操作系统开始支持多进程模型&#xff0c;即同时执行多个任务。每个任务被称为一个进…...

DJango项目

一.项目创建 在想要将项目创键的目录下,输入cmd (进入命令提示符)在cmd中输入:Django-admin startproject 项目名称 (创建项目)cd 项目名称 (进入项目)Django-admin startapp 程序名称 (创建程序)python manage.py runserver 8080 (运行程序)将弹出的网址复制到浏览器中…...

深入了解JavaScript当中如何确定值的类型

JavaScript是一种弱类型语言&#xff0c;当你给一个变量赋了一个值&#xff0c;该值是什么类型的&#xff0c;那么该变量就是什么类型的&#xff0c;并且你还可以给一个变量赋多种类型的值&#xff0c;也不会报错&#xff0c;这就是JavaScript的内部机制所决定的&#xff0c;那…...

excel数据对比找不同:6种方法核对两列数据差异

工作中&#xff0c;有时需要核对两列数据的差异&#xff0c;用于对比、复核等。数据较少的情况下差异肉眼可见&#xff0c;数据量较大时用什么方法比较好呢&#xff1f;从个人习惯出发&#xff0c;我整理了6种方法供参考。 6种方法核对两列数据差异&#xff1a; 1、Ctrl G定位…...

基于智能代理人工智能(Agentic AI)对冲基金模拟系统:模范巴菲特、凯西·伍德的投资策略

股票市场涉及众多统计数据和模式。股票交易基于研究和数据驱动的决策。人工智能的使用可以实现流程自动化&#xff0c;让投资者在研究上花费更少的时间&#xff0c;同时提高准确性。这使他们能够更加专注于监督实际交易和服务客户。 顶尖对冲基金经理发挥着至关重要的作用&…...

MySQL数据库基础(二)———数据表管理

前言 上篇文章介绍了MySQL数据库以即数据库的管理 这篇文章将给大家讲解数据表的管理 一、数据表常见操作 数据表常见操作的指令 进入数据库use数据库&#xff1b; 查看当前所有表&#xff1a;show tables; 创建表结构 1.创建表操作 1.1创建表 create table 表名(列名 …...

如何在Lyra中创建一个新的Game Feature Plugin和Experience游戏体验

目录 -1.前言0.预备知识1.创建一个新的Game Feature Plugin插件2.创建Lyra Pawn Data Asset3. 创建Lyra Experience Definition4. 创建自定义关卡5. 设置资产管理器Asset Manager引用6. 创建Lyra User Facing Experience Definition7. 在编辑器中运行测试后记-1.前言 由于转职…...

RDMA简介5之RoCE v2队列

在RoCE v2协议中&#xff0c;RoCE v2队列是数据传输的最底层控制机制&#xff0c;其由工作队列&#xff08;WQ&#xff09;和完成队列&#xff08;CQ&#xff09;共同组成。其中工作队列采用双向通道设计&#xff0c;包含用于存储即将发送数据的发送队列&#xff08;SQ&#xf…...

SAFe/LeSS/DAD等框架的核心适用场景如何选择?

在敏捷开发的规模化实践中&#xff0c;SAFe&#xff08;Scaled Agile Framework&#xff09;、LeSS&#xff08;Large Scale Scrum&#xff09;和DAD&#xff08;Disciplined Agile Delivery&#xff09;是三大主流框架。它们分别以不同的哲学和方法论应对复杂性、协作与交付的…...

鸿蒙应用开发之uni-app x实践

鸿蒙应用开发之uni-app x实践 前言 最近在开发鸿蒙应用时&#xff0c;发现uni-app x从4.61版本开始支持纯血鸿蒙&#xff08;Harmony next&#xff09;&#xff0c;可以直接编译成ArkTS原生应用。这里记录一下开发过程中的一些经验和踩过的坑。 一、环境搭建 1.1 开发工具 …...

window查看SVN账号密码

背景 公司的SVN地址发生迁移&#xff0c;想迁移一下本地SVN地址&#xff0c;后来发现SVN账号密码忘记了。写此文章纯记录。 迁移SVN地址&#xff1a; 找到svn目录点击relocate&#xff0c;输入新的svn地址&#xff0c;如需输入账号密码&#xff0c;输入账号密码即完成svn地址…...

Python训练营---Day44

DAY 44 预训练模型 知识点回顾&#xff1a; 预训练的概念常见的分类预训练模型图像预训练模型的发展史预训练的策略预训练代码实战&#xff1a;resnet18 作业&#xff1a; 尝试在cifar10对比如下其他的预训练模型&#xff0c;观察差异&#xff0c;尽可能和他人选择的不同尝试通…...

前端项目初始化

​​​​​​ 目录 1. 安装 nvm 2. 配置 nvm 并切换到 Node.js 16.15.0 3. 安装 LightProxy 代理 4. GIT安装 1. 配置用户名和邮箱&#xff08;这些信息将用于您在提交代码时的标识&#xff09;&#xff1a; 2. 生成SSH密钥&#xff08;用于将本地代码仓库与远程存储库连…...

USB扩展器与USB服务器的2个主要区别

在现代办公和IT环境中&#xff0c;连接和管理USB设备是常见需求。USB扩展器&#xff08;常称USB集线器&#xff09;与USB服务器&#xff08;如朝天椒USB服务器&#xff09;是两类功能定位截然不同的解决方案。前者主要解决物理接口数量不足的“近身”连接扩展问题&#xff0c;而…...

第46节:多模态分类(图像+文本)

一、多模态分类概述 多模态分类是指利用来自不同模态(如图像、文本、音频等)的数据进行联合分析和分类的任务。 在当今大数据时代,信息往往以多种形式存在,例如社交媒体上的图片配文字、视频附带字幕、医疗检查中的影像与报告等。单一模态的数据往往只能提供有限的信息,…...

spring获取注册的bean并注册到自定义工厂中管理

背景 在开发的时候&#xff0c;对于同一个对象的按照某个字段的不同有很多的处理方式。想着开发一个类似于工厂模式&#xff0c;由上层工厂统一分配。 由于是基于springboot开发&#xff0c;所以有很多自动注入的对象&#xff0c;如果由工厂统一创建new对象的方式&#xff0c;那…...

IDEA 中 Maven Dependencies 出现红色波浪线的原因及解决方法

在使用 IntelliJ IDEA 开发 Java 项目时&#xff0c;尤其是基于 Maven 的项目&#xff0c;开发者可能会遇到 Maven Dependencies 中出现红色波浪线的问题。这种现象通常表示项目依赖未能正确解析或下载&#xff0c;导致代码提示错误、编译失败等问题。本文将详细分析该问题的常…...

springMVC-10验证及国际化

验证 概述 ● 概述 1. 对输入的数据(比如表单数据)&#xff0c;进行必要的验证&#xff0c;并给出相应的提示信息。 2. 对于验证表单数据&#xff0c;springMVC提供了很多实用的注解, 这些注解由JSR303 验证框架提供. ●JSR 303 验证框架 1. JSR 303 的含义 JSR&#xff0…...

使用Python和TensorFlow实现图像分类

最近研学过程中发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击链接跳转到网站人工智能及编程语言学习教程。读者们可以通过里面的文章详细了解一下人工智能及其编程等教程和学习方法。下面开始对正文内容的…...

LRU 和 DiskLRU实现相册缓存器

我是写Linux后端的&#xff08;golang、c、py&#xff09;&#xff0c;后端缓存算法通常是指的是内存里面的lru、或diskqueue&#xff0c;都是独立使用。 很少有用内存lru与disklru结合的场景需求。近段时间研究android开发&#xff0c;里面有一些设计思想值得后端学习。 写这…...

figma MCP + cursor如何将设计稿生成前端页面

一、准备工作 figma MCP需要通过figma key来获取设计稿权限&#xff0c;key的生成步骤如下 1. 打开figma网页版/APP&#xff0c;进入账户设定 2. 点击生成token 3. 填写内容生成token(一定要确认复制了&#xff0c;不然关闭弹窗后就不会显示了) 二、配置MCP 4. 进入到cursor…...

如何理解OSI七层模型和TCP/IP四层模型?HTTP作为如何保存用户状态?多服务器节点下 Session方案怎么做

本篇概览&#xff1a; OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f;TCP/IP四层模型和OSI七层模型的区别是什么&#xff1f; HTTP 本身是无状态协议&#xff0c;HTTP如何保存用户状态? 能不能具体说一下Cookie的工作原理、生命周期、作用域&#xff1f;使用…...

Flask 核心概念速览:路由、请求、响应与蓝图

一、路由参数与请求方法 Flask 路由允许定义多种参数类型,并通过 methods 属性限制请求方法。 1. 路由参数类型: 除了默认的 string,Flask 还支持: int: 匹配整数,自动转换为 Python int 类型。非数字输入会返回 404。 float: 匹配浮点数,自动转换为 Python float 类型…...

Spring Boot消息系统开发指南

消息系统基础概念 消息系统作为分布式架构的核心组件&#xff0c;实现了不同系统模块间的高效通信机制。其应用场景从即时通讯软件延伸至企业级应用集成&#xff0c;形成了现代软件架构中不可或缺的基础设施。 通信模式本质特征 同步通信要求收发双方必须同时在线交互&#…...

【Elasticsearch】映射:Nested 类型

映射&#xff1a;Nested 类型 1.为什么需要 Nested 类型2.如何定义 Nested 类型3.相关操作3.1 索引包含 Nested 数据的文档3.2 查询 Nested 数据3.3 聚合 Nested 数据3.4 排序 Nested 数据3.5 更新 Nested 文档中的特定元素 4.Nested 类型的高级操作4.1 内嵌 inner hits4.2 多级…...

Vue3 + UniApp 蓝牙连接与数据发送(稳定版)

本教程适用于使用 uni-app Vue3 (script setup) 开发的跨平台 App&#xff08;支持微信小程序、H5、Android/iOS 等&#xff09; &#x1f3af; 功能目标 ✅ 获取蓝牙权限✅ 扫描周围蓝牙设备✅ 连接指定蓝牙设备✅ 获取服务和特征值✅ 向设备发送数据包&#xff08;ArrayBu…...

三种读写传统xls格式文件开源库libxls、xlslib、BasicExcel的比较

最近准备读写传统xls格式文件&#xff0c;而不是较新的xlsx&#xff0c;询问DeepSeek有哪些开源库&#xff0c;他给出了如下的简介和建议&#xff0c;还给出了相应链接&#xff0c;不过有的链接已失效。最后还不忘提醒&#xff0c;现在该用xlsx格式了。 以下是几个可以处理传统…...

Nature子刊同款的宏基因组免疫球蛋白测序怎么做?

免疫球蛋白A&#xff08;IgA&#xff09;是人体肠道黏膜分泌的主要抗体&#xff0c;它在塑造肠道微生物群落和维持肠道稳态中起着关键作用&#xff0c;有研究发现缺乏IgA的患者更容易患自身免疫性疾病和感染性疾病。 目前用于研究IgA结合的主要技术是IgA-SEQ&#xff0c;结合了…...

2025年牛客网秋招/社招高质量 Java 面试八股文整理

Java 面试 不论是校招还是社招都避免不了各种面试。笔试&#xff0c;如何去准备这些东西就显得格外重要。不论是笔试还是面试都是有章可循的。关键在于理解企业的需求&#xff0c;明确自己的定位&#xff0c;以及掌握一定的应试技巧。 笔试部分&#xff0c;通常是对基础知识、…...

ADI的BF609双核DSP怎么做开发,我来说一说(五)LAN口测试

作者的话 ADI的双核DSP&#xff0c;第二颗是Blackfin系列的BF609&#xff0c;这颗DSP我用了很久&#xff0c;比较熟悉&#xff0c;且写过一些给新手的教程。 硬件准备 ADSP-BF609-CORE&#xff1a;ADI BF609开发板 产品链接&#xff1a;https://item.taobao.com/item.htm?…...