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

【ONE·Linux || 多线程(一)】

总言

  多线程:进程线程基本概念、线程控制、互斥与同步。

文章目录

  • 总言
  • 1、基本概念
    • 1.1、补充知识
      • 1.1.1、堆区细粒度划分
      • 1.1.2、虚拟地址到物理空间的转化
    • 1.2、如何理解线程、进程
      • 1.2.1、如何理解线程?
      • 1.2.2、如何理解进程?
    • 1.3、实践操作
      • 1.3.1、基本演示(线程创建)
      • 1.3.2、线程如何看待进程内部的资源?
      • 1.3.3、进程VS线程(调度层面上)
  • 2、线程控制
    • 2.1、线程创建
      • 2.1.1、函数介绍
      • 2.1.2、演示
    • 2.2、线程等待
      • 2.2.1、函数介绍
      • 2.2.2、演示一:验证退出有序
      • 2.2.3、演示二:线程返回值
      • 2.2.4、演示三:线程返回值2.0
    • 2.3、线程终止
      • 2.3.1、exit终止进程
      • 2.3.2、pthread_exit
      • 2.3.3、pthread_cancel
    • 2.4、线程ID探索:pthread_self()
    • 2.5、其它验证
      • 2.5.1、验证全局区的数据能被多进程共享(__thread介绍)
      • 2.5.2、如果在线程中使用了execl系列进程替换函数,会发生什么?
    • 2.6、线程分离
  • 3、线程互斥与同步
    • 3.1、线程互斥
      • 3.1.1、问题引入与概念介绍
      • 3.1.2、互斥锁
        • 3.1.2.1、相关涉及函数
        • 3.1.2.2、使用一:静态、全局方式
        • 3.1.2.3、使用二:动态、局部方式
        • 3.1.2.4、问题说明(由实践到理论理解)
      • 3.1.3、锁的原理
      • 3.1.4、死锁
      • 3.1.5、可重入VS线程安全
    • 3.2、线程同步
      • 3.2.1、问题引入与概念介绍
      • 3.2.2、方案一:条件变量
        • 3.2.2.1、方案说明与函数介绍
        • 3.2.2.2、方案演示1.0

  
  
  

1、基本概念

1.1、补充知识

1.1.1、堆区细粒度划分

  问题:堆区里有很多申请到的小空间,那么如何知道哪块区域是一个整体?以及如何找到对应堆区申请的空间?
  
  回答:struct_vm_area_sturct结构体。每次在堆区申请空间,会生成这样一个结构体,vm_startvm_end能记录所申请空间的首尾位置。将这些结构体以双链表的形式链接起来,就可通过vm_nextvm_prev找到每个空间位置。

struct vm_area_struct {unsigned long vm_start;		// Our start address within vm_mm. unsigned long vm_end;		// The first byte after our end address within vm_mm.// linked list of VM areas per task, sorted by address struct vm_area_struct *vm_next, *vm_prev;//…………//其它内容
}

  
   说明:OS是可以做到让进程进行资源的细粒度划分
  
  
  

1.1.2、虚拟地址到物理空间的转化

  ①我们的可执行程序在编译阶段,就已经以4KB为单位按照虚拟地址的区域被划分。(页帧)
  ②物理内存也是以4KB为单位划分为一个个小块,并以struct page{ }结构体来管理。(页框)

Linux内核将整个物理内存按照页对齐方式划分成千上万个页进行管理。由于一个物理页用一个struct page表示,那么系统会有成千上万个struct page结构体,这些结构体也会占用实际的物理内存,因此,内核选择用union联合体来减少内存的使用。

  ③IO的基本单位是4KB。相当于把页帧装进页框里。
在这里插入图片描述

  
  
  
  

1.2、如何理解线程、进程

1.2.1、如何理解线程?

  在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
在这里插入图片描述

  说明:
  1、通过进程虚拟地址空间可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
  2、因此,线程在进程内部运行的(线程在进程地址空间内运行),是OS调度的基本单位(CPU进行调度时,不关心执行流是进程还是线程,只关心PCB)。
  3、一切进程至少都有一个执行线程。
  
  4、不同操作系统下(Linux、windows等),线程的实现方案不同(只要满足设定的条件规则即可)。上述图示的是Linux的线程方案,实际上Linux没有真正意义上的线程结构,是用进程PCB模拟的。也因此,Linux并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口(Linux下,PCB<=其它OS的PCB,故将Linux进程称之为轻量级进程)。
  5、pthread线程库(Linux系统自带的原生线程库):在用户层实现了一套用户层多线程方案,以库的方式提供给用户进行使用。(相当于省去一定的学习线程库实现的成本,只需要会调用该线程库即可)。
  
  
  
  

1.2.2、如何理解进程?

  用户视角:进程 = 内核数据结构(可存在多个PCB)+ 该进程对应的代码和数据。
  内核视角:进程是承担分配系统资源的基本实体。(进程向操作系统申请系统资源,此后线程的资源分配就由进程来执行,即OS角度,这些PCB、虚拟地址、页表等是以进程为单位申请的。)
  
  如何理解曾经我们所写的代码?
  以前我们所写的可执行程序,属于内部只有一个执行流的进程。引入线程后,可以有内部有多个执行流的进程。
  
  
  
  

1.3、实践操作

在这里插入图片描述

1.3.1、基本演示(线程创建)

  1)、相关函数介绍和使用说明
  man pthread_create:创建一个新的线程。

NAMEpthread_create - create a new threadSYNOPSIS#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread.

  参数:

thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数(start_routine)的参数

  返回值:成功返回0;失败返回错误码

RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contentsof *thread are undefined.

  
  其它:由于调用的是操作系统库,因此 使用gcc/g++时需要加上选项表示对应库的名称:-lpthread PS:关于动静态库如何使用的相关细节说明见博文:Linux || 基础IO(二)。

在这里插入图片描述

  
  
  
  2)、演示一:基本使用演示

       int sprintf(char *str, const char *format, ...);int snprintf(char *str, size_t size, const char *format, ...);

  相关代码如下:

#include<iostream>
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<string>
using namespace std;void* threadRun(void * args)
{string name = (char*)args;//字符串首元素地址、赋值运算符while(true){cout << name << " , pid:" << getpid() << "\n" << endl;sleep(1);}
}int main()
{pthread_t tid[5];//创建5个线程char threadname[64];//用于arg参数传递线程名称,以便区分for(int i = 0; i < 5; ++ i){//snprintf每次都会向threadname数组中写入字符串。(thread-1、thread-2、thread-3、……)snprintf(threadname, sizeof(threadname) ,"%s-%d","thread",i);pthread_create(tid+i, nullptr, threadRun, (void*)threadname);sleep(1);//此处是缓解传参BUG}while(true){cout << " main thread , pid:" << getpid() << endl;sleep(3);}return 0;
}

  以下为演示结果:

在这里插入图片描述
  
  相关说明:
  1、ps -aL:可查看线程。
  2、主线程和新线程运行顺序是不确定的,取决于调度器(和父子进程顺序不定一样)
  3、由上图可知,CPU调度时看的是LWP,而非PID。因为当有多个线程时,LWP唯一,但PID可以对应多个线程。(PS:对于单线程的进程,其LWP和PID一样,故CPU看的仍旧是LWP。)
  4、kill -9 PID :用于杀掉一个进程,需要注意,内部所有线程都被杀掉

 main thread , pid:28571
thread-1 , pid:28571thread-0 , pid:28571thread-3 , pid:28571thread-2 , pid:28571thread-4 , pid:28571Killed   #kill -9 28571 杀掉进程,对于进程内所有线程都被杀掉
[wj@VM-4-3-centos T0927]$ 

  
  
  
  
  
  

1.3.2、线程如何看待进程内部的资源?

  进程是资源分配的基本单位,线程是调度的基本单位。

  1、线程共享进程数据,但也拥有自己的一部分数据,如:
    ①线程ID;
    ②一组寄存器
    ③(一般认为独自占用);
    ④errno错误码;
    ⑤信号屏蔽字;
    ⑥调度优先级。
    

  2、除了上述全局变量在各线程中都可以访问到,各线程还共享以下进程资源和环境:
    ①文件描述符表;
    ②每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数);
    ③当前工作目录;
    ④用户id和组id;
    ⑤代码区、全局数据区(已初始化/未初始化)、堆区、共享区

  
  
  
  
  

1.3.3、进程VS线程(调度层面上)

  1)、为什么说线程切换的成本更低?

  1、地址空间、页表不需要被切换。(假如调度的是另外的进程PCB,则上下文、页表、地址空间等都需要切换,故而比线程切换成本更高)。

  2、对于线程,CPU内部有L1~L3 cache(缓存),对内存的代码和数据根据局部性原理预读到CPU内部。(若是进程切换,cache会失效,新进程只能重新缓存)。
  
  
  
  
  

2、线程控制

  1)、总览
在这里插入图片描述
  
  2)、POSIX线程库
  1、与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头。
  2、要使用这些函数库,要通过引入头文<pthread.h>
  3、链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
  
  

2.1、线程创建

2.1.1、函数介绍

  1)、函数介绍
  pthread_create函数在上述小节中已经演示过,此处只做补充说明。

NAMEpthread_create - create a new threadSYNOPSIS#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread.

  参数:

thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数(start_routine)的参数

  返回值:成功返回0;失败返回错误码

RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contentsof *thread are undefined.

  
  

2.1.2、演示

  2)、演示线程异常
  演示代码如下:
  PS:当创建线程成功,新线程执行对应的threadRoutine(参数start_routine)内部内容,主线程继续执行ptread_create后面的代码。

void* threadRoutine(void * args)
{while(true){sleep(2);cout << "新线程: " << (char*)args << " , is runing. " << endl;int a = 10;a /= 0 ;//error}
}int main()
{fflush(stdout);pthread_t tid; // 创建一个线程pthread_create(&tid, nullptr, threadRoutine, (void *)"new thread");while (true){cout << " main thread , pid:" << getpid() << endl;sleep(1);}return 0;
}

  
  演示结果如下:
在这里插入图片描述

  线程异常说明:
  1、单个线程如果出现除零、野指针等问题导致线程崩溃,进程也会随着崩溃
  2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
  
  
  
  
  

2.2、线程等待

2.2.1、函数介绍

  1)、 为什么需要线程等待?
  回答:对于已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。若主线程不等待资源回收,那么新创建的线程不会复用刚才退出线程的地址空间。如此,就会造成类似于僵尸进程的问题。
  
  2)、函数介绍
  man pthread_join:等待线程结束。调用该函数的线程将挂起等待,直到id为thread的线程终止

NAMEpthread_join - join with a terminated threadSYNOPSIS#include <pthread.h>int pthread_join(pthread_t thread, void **retval);Compile and link with -pthread.DESCRIPTIONThe  pthread_join()  function  waits for the thread specified by thread to terminate.  If thatthread has already terminated, then pthread_join() returns immediately.  The thread  specifiedby thread must be joinable.

  参数

thread:线程ID
retval:它指向一个指针,后者通常是线程thread运行结束后的返回值

  返回值:成功返回0;失败返回错误码

RETURN VALUEOn success, pthread_join() returns 0; on error, it returns an error number.

  
  说明:thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。(宏:-13. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是pthread_exit传递的参数。
4. 如果对thread线程的终止状态不感兴趣,可以将retval设置为NULL

  
  
  

2.2.2、演示一:验证退出有序

  演示代码如下:

void* pthreadRoutine(void * args)
{cout << (char*)args << ": runing." << endl;int count = 5;do{cout << "new thread: " << count << endl;sleep(1);}while(count--);cout << "new thread: quit." << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");cout << "main thread: create succeed." << endl;pthread_join(tid, nullptr);cout << "main thread: wait succeed, main quit." << endl;return 0;
}

  演示结果如下:
在这里插入图片描述

  
  可用脚本来观察:

while :; do ps -aL | head -1 && ps -aL | grep thread; sleep 1; done

在这里插入图片描述
  
  
  
  

2.2.3、演示二:线程返回值

  问题: 在上述演示代码中,void* pthreadRoutine(void * args), 执行函数会返回一个(void*),该返回值是给谁?
  
  回答: 谁来等待,就给谁。一般是给主线程,主线程可通过线程等待pthread_join来知道结果,即该函数的第二参数: void **value_ptr
  PS:注意其参数类型是void**,这里属于输出型参数,获取pthread_create的返回值(void*),改变了实参value_ptr,那么需要二级指针变量。
  
  演示代码如下:

void* pthreadRoutine(void * args)
{cout << (char*)args << ": runing." << endl;int count = 5;do{cout << "new thread: " << count << endl;sleep(1);}while(count--);cout << "new thread: quit." << endl;return (void*)22;//注意点1:从整形转变为void*类型,相当于将地址数据为22处返回。
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");cout << "main thread: create succeed." << endl;void* ret = nullptr;//注意点2:用于接收pthreadRoutine,新线程返回值pthread_join(tid, &ret);//注意点2:要想实参被修改,则需要传址,void*的地址类型为void**cout << "ret: " << ret << " , ret:" << (long long)ret <<endl;//注意点3:Linux下指针为8字节,故此处强转int类型(4字节)不适用(会出现截断问题)cout << "main thread: wait succeed, main quit." << endl;return 0;
}

  
  演示结果如下:
在这里插入图片描述

  
  
  
  

2.2.4、演示三:线程返回值2.0

  除了上述返回一个值外,线程的返回值具有可玩性,运用恰当可以做一些有意义的操作。以下为一个代码举例,我们可以让新线程做一些运算,并将结果存储在堆中,或以其它方式返回给主线程。

  演示代码如下:虽然是新线程申请的动态空间,但堆区在线程间能够共享,所以主线程也能看到。

void* pthreadRoutine(void * args)
{cout << (char*)args << ": runing." << endl;int* data = new int[5];for(int count = 0 ; count < 5; ++count){cout << "new thread: " << count << endl;data[count] = count;sleep(1);}cout << "new thread: quit." << endl;return (void*)data;//返回了堆上申请的空间(新线程)
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");cout << "main thread: create succeed." << endl;int* ret = nullptr;//用于接收pthreadRoutine,新线程返回值pthread_join(tid, (void**)&ret);cout << "main thread: wait succeed." << endl;for(int i = 0; i < 5; ++i){cout << ret[i] << ' ';}cout << endl;return 0;
}

  演示结果如下:
在这里插入图片描述

  
  
  
  

2.3、线程终止

2.3.1、exit终止进程

  说明:exit是用于终止进程的,调用它不仅仅当前线程会被终止,整个进程都会终止。
  
  演示代码如下:

void* pthreadRoutine(void * args)
{cout << (char*)args << ": runing." << endl;int* data = new int[5];for(int count = 0 ; count < 5; ++count){cout << "new thread: " << count << endl;data[count] = count;sleep(1);}cout<< "now, exit the new thread." << endl;exit(22);//使用exit终止新线程cout << "new thread: quit." << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");cout << "main thread: create succeed." << endl;pthread_join(tid, nullptr);//阻塞式等待cout << "main thread: wait succeed." << endl;while(true){cout << " main still runing." << endl;sleep(1);}return 0;
}

  演示结果如下:
在这里插入图片描述
  
  
  
  

2.3.2、pthread_exit

  1)、函数介绍
  man pthread_exit:线程可以调用该函数终止自己。

NAMEpthread_exit - terminate calling threadSYNOPSIS#include <pthread.h>void pthread_exit(void *retval);Compile and link with -pthread.DESCRIPTIONThe  pthread_exit() function terminates the calling thread and returns a value viaretval that (if the thread is joinable) is available to another thread in the sameprocess that calls pthread_join(3).

  参数:

retval:返回指针,用于存储退出线程的返回数据,注意不要指向一个局部变量。

  返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。

RETURN VALUEThis function does not return to the caller.

  
  
  
  2)、使用演示
  演示代码如下:根据之前所学,如果thread线程是调用pthread_exit终止的,retval参数所指向的单元将会传递给pthread_join

void* pthreadRoutine(void * args)
{cout << (char*)args << ": runing." << endl;int* data = new int[5];for(int count = 0 ; count < 5; ++count){cout << "new thread: " << count << endl;data[count] = count;sleep(1);}cout<< "now, exit the new thread." << endl;pthread_exit((void*)11);//使用exit终止新线程cout << "new thread: quit." << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");cout << "main thread: create succeed." << endl;int* ret = nullptr;pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_exit的参数值cout << "main thread: wait succeed, the return value:" << (long long)ret << endl;while(true){cout << " main still runing." << endl;sleep(1);}return 0;
}

  演示结果如下:
在这里插入图片描述

  
  
  
  
  

2.3.3、pthread_cancel

  1)、函数介绍
  man pthread_cancel:取消一个执行中的线程。

NAMEpthread_cancel - send a cancellation request to a threadSYNOPSIS#include <pthread.h>int pthread_cancel(pthread_t thread);Compile and link with -pthread.DESCRIPTIONThe  pthread_cancel()  function sends a cancellation request to the thread thread.Whether and when the target thread reacts to the cancellation request  depends  ontwo  attributes that are under the control of that thread: its cancelability stateand type.

  参数

thread:线程ID

  返回值:成功返回0,失败返回错误码

RETURN VALUEOn  success, pthread_cancel() returns 0; on error, it returns a nonzero error num‐ber.

  
  
  
  2)、使用演示
  演示代码如下:

void* pthreadRoutine(void * args)
{cout << (char*)args << ": runing." << endl;size_t count = 0;while(true)//让新线程一直循环运行{cout << "new thread: " << count++ << endl;sleep(1);}cout << "new thread: quit." << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");cout << "main thread: create succeed." << endl;sleep(6);//让新线程运行6s后,取消新线程。pthread_cancel(tid);int* ret = nullptr;pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_cancel的返回值cout << "main thread: wait succeed, the return value:" << (long long)ret << endl;int count = 5;//让主线程在新线程退出后再运行一段时间while(count--){cout << " main thread: runing." << endl;sleep(1);}cout << "main quit." << endl;return 0;
}

  演示结果如下:
在这里插入图片描述

  PS:不要在随意位置使用终止函数,按照场景需求正常使用即可。一般是主线程中使用,取消新线程;虽然没说不可以在新线程中取消主线程,但有可能会引起一些奇怪问题。
  
  
  
  

2.4、线程ID探索:pthread_self()

  1)、问题引入
  此处使用2.3.3中演示代码,对main函数稍加修改,打印tid值:

    pthread_cancel(tid);int* ret = nullptr;pthread_join(tid, (void**)&ret);//阻塞式等待,接收来自pthread_cancel的返回值cout << "main thread: wait succeed, the return value:" << (long long)ret <<" ,tid:" << tid << endl;

  如下,tid:140604466824960,是一个很大的数字。这似乎与我们之前学习到的进程ID,文件描述符fd等都不同,且也并非我们用ps -aL 指令查看到的LWP值。

[wj@VM-4-3-centos T0927]$ ./thread.out 
main thread: create succeed.
new thread: runing.
new thread: 0
new thread: 1
new thread: 2
new thread: 3
new thread: 4
new thread: 5
main thread: wait succeed, the return value:-1 ,tid:140604466824960main thread: runing.main thread: runing.main thread: runing.main thread: runing.main thread: runing.
main quit.
[wj@VM-4-3-centos T0927]$ 

  那么,这里的线程ID究竟什么?
  
  
  
  2)、解释说明
  结论:对于Linux目前实现的实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
在这里插入图片描述

  主线程用的是虚拟地址的栈结构,新线程用的是库里提供的私有栈结构。
  
  pthread_self()可以获得线程自身的ID,在哪个线程中使用,获取的就是哪个线程的ID。
  演示代码如下:

void* pthreadRoutine(void * args)
{size_t count = 3;while(count--)    {cout << (char*)args << ": runing." << "  tid:" << pthread_self() << endl;sleep(1);}cout << "new thread: quit." << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, pthreadRoutine, (void*)"new thread");//创建新线程pthread_join(tid, nullptr);//阻塞式等待新线程int count = 3;//让主线程在新线程退出后再运行一段时间while(count--){cout << "main thread: runing."  "  tid:" << pthread_self() << endl;sleep(1);}cout << "main quit." << endl;return 0;
}

  演示结果如下:

[wj@VM-4-3-centos T0927]$ make
g++ -o thread.out mythread.cc -std=c++11 -lpthread
[wj@VM-4-3-centos T0927]$ ls
makefile  mythread.cc  thread.out
[wj@VM-4-3-centos T0927]$ ./thread.out 
new thread: runing.  tid:140510573528832
new thread: runing.  tid:140510573528832
new thread: runing.  tid:140510573528832
new thread: quit.
main thread: runing.  tid:140510591952704
main thread: runing.  tid:140510591952704
main thread: runing.  tid:140510591952704
main quit.
[wj@VM-4-3-centos T0927]$ 

  
  
  
  

2.5、其它验证

2.5.1、验证全局区的数据能被多进程共享(__thread介绍)

  1)、相关演示
  演示代码:

int val = 0;void* Routine(void* args)
{while(true){//cout << "new  thread," << ", val:" << val << ", &val:" << &val << endl;printf("new  thread, val:%d, &val:%p\n",val,&val);val++;sleep(1);}
}int main()
{pthread_t tid = 0;pthread_create(&tid, nullptr, Routine, (void*)"new thread");while(true){//cout << "main thread" << ", val:" << val << ", &val:" << &val << endl;printf("main thread, val:%d, &val:%p\n",val,&val);sleep(1);}pthread_join(tid ,nullptr);return 0;
}

  演示结果:
在这里插入图片描述
  
  
  2)、若想让全局变量不被共享,如何操作?

  __thread:修饰全局变量,可以让该全局变量被每一个线程独自占有(线程的局部存储)。

  注意这里的的__是两个_。使用如下:__thread int val = 0;
在这里插入图片描述

  
  
  
  

2.5.2、如果在线程中使用了execl系列进程替换函数,会发生什么?

  演示代码:

void* Routine(void* args)
{while(true){sleep(3);execl("/bin/ls","ls",nullptr);//进程替换printf("new thread,tid:%u\n",pthread_self());sleep(1);}
}int main()
{pthread_t tid = 0;pthread_create(&tid, nullptr, Routine, (void*)"new thread");while(true){printf("main thread,tid:%u\n",pthread_self());sleep(1);}pthread_join(tid ,nullptr);return 0;
}

  演示结果如下:
在这里插入图片描述
  
  需要注意,语言级别的线程(如C++中也提供了线程),无论再怎么支持,其底层还是使用的是原生系统的线程库。
  
  
  
  

2.6、线程分离

  1)、函数介绍
  man pthread_detach:默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时自动释放线程资源。  

NAMEpthread_detach - detach a threadSYNOPSIS#include <pthread.h>int pthread_detach(pthread_t thread);Compile and link with -pthread.DESCRIPTIONThe  pthread_detach()  function  marks  the thread identified by thread as detached.  When a detachedthread terminates, its resources are automatically released back to the system without the  need  foranother thread to join with the terminated thread.Attempting to detach an already detached thread results in unspecified behavior.RETURN VALUEOn success, pthread_detach() returns 0; on error, it returns an error number.

  1、可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
  2、线程分离和线程等待是冲突的,一个线程不能既是joinable又是分离的。
  
  
  
  
  2)、演示一
  演示代码如下:

void* Routine(void* args)
{//可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离pthread_detach(pthread_self());int count = 5;while(count--){printf("new thread,tid:%u, count:%d\n",pthread_self(),count);sleep(1);}return nullptr;
}int main()
{pthread_t tid = 0;pthread_create(&tid, nullptr, Routine, (void*)"new thread");int count = 5;while(count--){printf("main thread, tid:%u, count:%d\n",pthread_self(),count);sleep(1);}cout <<"thr result:" << strerror(pthread_join(tid ,nullptr)) << endl;return 0;
}

  演示结果如下:
在这里插入图片描述

  
  
  
  3)、演示二:线程分离,若该线程异常,是否会影响主线程?
  回答:会,同一个进程中,资源还是共享的。
在这里插入图片描述

  
  
  
  
  
  

3、线程互斥与同步

3.1、线程互斥

3.1.1、问题引入与概念介绍

  1)、问题引入:不加保护的情况下,多线程抢票逻辑
  问题说明:如果多线程访问同一个全局变量,并对它进行数据计算,多线程会相互影响吗?
  
  演示代码:

int tickets = 1000;void* getTickets(void* args)
{(void)args;while(true){if(tickets > 0 ){usleep(1000);//休眠printf("%p: %d\n", pthread_self(), tickets--);}else break;}return nullptr;
}int main()
{pthread_t tid1, tid2, tid3;//一次创建多个线程pthread_create(&tid1, nullptr, getTickets, nullptr);pthread_create(&tid2, nullptr, getTickets, nullptr);pthread_create(&tid3, nullptr, getTickets, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}

  演示结果:根据上述代码,printf只会打印出tickets > 0的数,可运行程序我们发现最后总会有ticket== -1。为什么会出现此现象?

在这里插入图片描述

  
  
  2)、原因解释1.0
  分析上述代码,tickets>0是逻辑运算,会在CPU中进行,同理,tickets--也是在CPU中进行的。这就需要将物理地址中存储的tickets变量载入进程(当前执行流)的上下文,在线程被调度时CPU执行计算操作。
  但需要注意的是,tickets--实则为三步操作:①读取数据到CPU寄存器;②CPU内部进行数据计算;③将结果写回内存。若在此期间,线程因为CPU调度被切换,那么对于tickets的相关操作,无论执行到哪一步骤,都会随PCB上下文被切换走,直到下次再被调度。
   由此,在不加保护的情况下,多个线程对同一数据不具有实时同步性,会导致并发访问时数据不一致
在这里插入图片描述

  
  
  3)、一些概念
  临界资源: 多线程执行流共享的资源就叫做临界资源。
  临界区: 每个线程内部,访问临界资源的代码,就叫做临界区(实际还有很大一部分代码段属于普通代码)。
  互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
  
  
  
  
  

3.1.2、互斥锁

  针对上述问题,一个避免方法是加锁保护。
在这里插入图片描述

  
  

3.1.2.1、相关涉及函数

  1、对锁初始化(初始化互斥量):

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);mutex:要初始化的互斥量attr:NULL
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

  
  2、加锁、解锁

       int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);

  
  3、销毁

       int pthread_mutex_destroy(pthread_mutex_t *mutex);

  ①使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  ②不要销毁一个已经加锁的互斥量
  ③已经销毁的互斥量,要确保后面不会有线程再尝试加锁
  
  
  
  

3.1.2.2、使用一:静态、全局方式

  1)、代码演示1.0
  如下:

int tickets = 1000;//1、定义一个锁并对其初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* getTickets(void* args)
{(void)args;while(true){//2、加锁:pthread_mutex_lock(&mutex);if(tickets > 0 ){usleep(1000);//休眠printf("%p: %d\n", pthread_self(), tickets--);pthread_mutex_unlock(&mutex);//3、解锁}else {pthread_mutex_unlock(&mutex);//3、解锁break;}}return nullptr;
}int main()
{pthread_t tid1, tid2, tid3;//一次创建多个线程pthread_create(&tid1, nullptr, getTickets, nullptr);pthread_create(&tid2, nullptr, getTickets, nullptr);pthread_create(&tid3, nullptr, getTickets, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);return 0;
}

  演示结果:
在这里插入图片描述

  
  解释说明:
  1、if…else语句中,break退出前需要解锁:
在这里插入图片描述

  
  2、能明显看出与不加锁时相比,运行速度变慢。(可以获取一个时间来验证,time、gettimeofday)。
在这里插入图片描述
  
  
  3、可加入随机数,让持锁进程更加随机。(实则我们演示时多个线程都有参与)

//int main()srand((unsigned int)time(nullptr)^getpid());//种子:用于让持锁线程更加随机//void* getTickets(void* args)usleep(rand() % 1500);//休眠:让休眠随机一点

  
  
  4、加锁时容易影响效率,为了保证加锁粒度,加锁区域越小越好
  
  
  
  

3.1.2.3、使用二:动态、局部方式
int tickets = 10000;
#define THREAD_NUM 5 //待创建线程数目class ThreadData // 用于创建线程时,args传参:线程名、锁
{
public:ThreadData(const string &name, pthread_mutex_t *pmutex): _name(name), _pmutex(pmutex){}string _name;pthread_mutex_t* _pmutex;
};void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;while(true){//3、加锁:pthread_mutex_lock(td->_pmutex);if(tickets > 0 ){usleep(rand() % 1500);//休眠:让休眠随机一点printf("%s: %d\n", td->_name.c_str(), tickets--);pthread_mutex_unlock(td->_pmutex);//3、解锁}else {pthread_mutex_unlock(td->_pmutex);//3、解锁break;}}delete td;//销毁new出来的空间return nullptr;
}int main()
{srand((unsigned int)time(nullptr) ^ getpid()); // 种子:用于让持锁线程更加随机clock_t t1 = clock();                          // 测试时间pthread_mutex_t mutex;                         // 1、定义一个锁pthread_mutex_init(&mutex, nullptr);           // 2、对锁初始化// 创建线程pthread_t tid[THREAD_NUM]; // 线程IDfor (int i = 0; i < THREAD_NUM; ++i){string name = "thread";        // 线程名name += std::to_string(i + 1); // 线程名ThreadData *td = new ThreadData(name, &mutex);pthread_create(tid + i, nullptr, getTickets, (void *)td);}// 等待线程for (int i = 0; i < THREAD_NUM; ++i){pthread_join(tid[i], nullptr);}//4、销毁锁pthread_mutex_destroy(&mutex);clock_t t2 = clock();cout << "time: " << (t2 - t1) << endl;return 0;
}

  
  
  
  
  

3.1.2.4、问题说明(由实践到理论理解)

  1)、加锁之后,线程在临界区中是否切换,是否会有问题?
  回答:会切换,但不会有问题。
  第一次理解:当前线程虽然被切换了,但其是持有锁被切换的。而其他抢票线程要执行临界区代码,也必须先申请锁,此时锁无法申请成功的,所以,也不会让其他线程进入临界区,由此保证了临界区中数据一致性!
  
  
  2)、原子性体现?
  回答:设线程1持有锁,在未持有锁的线程看来,对其最有意义的情况只有两种:1. 线程1没有持有锁(什么都没做) 2. 线程1释放锁(做完),此时我可以申请锁。
  
  
  
  3)、加锁就是串行执行了吗?
  回答:是的,执行临界区代码一定是串行的。
  
  
  
  

3.1.3、锁的原理

  1)、问题引入
  要访问临界资源,每一个线程都必须先申请锁,每一个线程都必须先看到同把一锁并能够访问它。这就味着锁本身就是一种共享资源。所以,为了保证锁的安全,申请和释放锁,必须是原子的。
  
  那么,谁来保证?如何保证?锁是如何实现的?
  
  
  
  2)、原理解释
  为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换。在汇编角度,若只有一条汇编语句,则认为该汇编指令是原子性。

  即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
在这里插入图片描述
  PS:实际底层还有过多概念原理,这里只是一层理解。
  
  
  1、谁来保证锁的安全?
  回答:锁自身。加锁、解锁这步动作都只涉及一行汇编。
  
  
  
  

3.1.4、死锁

  1)、概念
  死锁是指在一组进程中的各个进程均占有不会释放的资源,因互相申请被其他进程所站用的不会释放的资源,使得彼此处于一种永久等待的状态。
在这里插入图片描述
  
  以下为一种线程自己把自己弄成死锁的场景举例:

void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;while(true){//此处已经加锁pthread_mutex_lock(td->_pmutex);if(tickets > 0 ){usleep(rand() % 1500);printf("%s: %d\n", td->_name.c_str(), tickets--);pthread_mutex_lock(td->_pmutex);//在加锁后,尚未解锁前,再次加锁,那么即使是线程本身,也是申请失败的。pthread_mutex_unlock(td->_pmutex);//解锁}else {pthread_mutex_unlock(td->_pmutex);//解锁break;}}delete td;//销毁new出来的空间return nullptr;
}

在这里插入图片描述
  
  
  
  2)、死锁的必要条件
  互斥条件:一个资源每次只能被一个执行流使用。(产生死锁,正是因为牵扯到互斥)
  请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(类似于吃着碗里的还看着锅里的)
  不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
  PS:只要产生死锁,必然是这四个条件都被满足。反之,若破坏其中某个条件,就无法达成死锁。
  
  
  
  3)、如何避免死锁
  破坏死锁的四个必要条件
  加锁顺序一致
  避免锁未释放的场景
  资源一次性分配
  
  
  
  

3.1.5、可重入VS线程安全

  1)、概念
  线程安全:多个线程并发同一段代码时,不会出现不同的结果,则说明线程是安全的。常见对全局变量或者静态变量进行操作时,在没有锁保护的情况下会出现并发问题。

  重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,称为不可重入函数。
  
  
  
  2)、常见的线程不安全和线程安全的情况
  
  线程不安全:

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

  
  
  线程安全:

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性

  
  
  
  3)、常见的可重入和不可重入的情况
  
  不可重入:

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

  
  
  可重入:

不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

  
  
  
  4)、联系与区别
  联系:

函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

  
  
  区别:

可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

  
  
  
  
  
  

3.2、线程同步

3.2.1、问题引入与概念介绍

  1)、引入:上述互斥锁是否存在什么问题?
  回答:存在以下两种不合理的行为(虽然没有错误,但不合适)
  1、在拥有资源时,某个线程频繁的申请到资源,导致其它线程处于"饥饿"状态(长时间得不到资源)。
  2、在资源短缺时,某个线程频繁申请失败,浪费彼此时间。
  
  为了解决上述访问临界资源合理性的问题,我们引入同步的概念。
  
  
  
  12)、什么叫做同步?
  说明:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步(线程同步)。
  
  那么,如何实现同步?
  
  

3.2.2、方案一:条件变量

3.2.2.1、方案说明与函数介绍

  1)、在使用条件变量前的一些理解说明
  申请临界资源前,要先对临界资源是否存在做出检测,而检测本身也是在访问临界资源,也需要对其进行加锁解锁。常规方式下, 线程检测条件是否就绪,就需要频繁申请和释放锁。(此时若临界资源不就绪,线程申请锁失败,相当于其在频繁地加锁解锁做无意义的耗时行为)

void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;while(true){pthread_mutex_lock(td->_pmutex);//加锁if(tickets > 0 )//如此处,在申请临界资源前,先对临界资源是否存在做了检测。而该检测是在加锁之后进行的。{usleep(rand() % 1500);printf("%s: %d\n", td->_name.c_str(), tickets--);pthread_mutex_lock(td->_pmutex);pthread_mutex_unlock(td->_pmutex);//解锁}else {pthread_mutex_unlock(td->_pmutex);//解锁break;}}delete td;return nullptr;
}

  
  考虑到此,我们设置出方案,让线程在(首次)检测到资源不就绪时,①不再频繁地重复进行资源检测,而是处于等待状态;②当资源就绪时,能接收到相应通知,随后再进行资源申请和访问。
  
  
  

  2)、条件变量涉及函数 在这里插入图片描述
  
  
  
  

3.2.2.2、方案演示1.0

  
   演示代码:

#include<iostream>
#include<pthread.h>
#include<string>
#include<unistd.h>
using namespace std;#define TNUM 4
volatile bool quit = false;//用于让线程结束循环,退出typedef void(*func_t)(const string& name, pthread_mutex_t* pmx, pthread_cond_t* pcd);//函数指针class ThreadData//用于create线程是,args传参
{
public:ThreadData(const string& name, func_t func, pthread_mutex_t* pmx, pthread_cond_t* pcd):_name(name),_func(func),_pmx(pmx),_pcd(pcd){}string _name;//线程名func_t _func;//函数指针pthread_mutex_t* _pmx;//锁pthread_cond_t* _pcd;//条件变量
};void func1(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件,等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout << name.c_str() << " is running, action : F1--帮助键" << endl;cout << endl;pthread_mutex_unlock(pmx); // 解锁}
}void func2(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件,等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout << name.c_str() << " is running, action : F2--重命名" << endl;cout << endl;pthread_mutex_unlock(pmx); // 解锁}
}void func3(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件,等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout << name.c_str() << " is running, action : F3--搜索按钮" << endl;cout << endl;pthread_mutex_unlock(pmx); // 解锁}
}void func4(const string &name, pthread_mutex_t *pmx, pthread_cond_t *pcd)
{while (!quit){pthread_mutex_lock(pmx); // 加锁// if(满足条件)(访问临界资源)// else(不满足条件,等待临界资源就绪)pthread_cond_wait(pcd, pmx);cout << name.c_str() << " is running, action : F4--浏览器网址列表" << endl;cout << endl;pthread_mutex_unlock(pmx); // 解锁}
}void* Entry(void* args)
{//所有新线程都会执行Entry函数,在Entry函数中每个线程又会执行其对应的funcThreadData* td = (ThreadData*) args;td->_func(td->_name, td->_pmx, td->_pcd);sleep(1);cout << td->_name.c_str() <<": " <<pthread_self() << endl;delete td;//新线程有自己独立的栈结构,每一个td变量都在各自私有栈中保存,最后新线程运行结束时,记得释放掉申请出来的堆return nullptr;
}int main()
{//创建并初始化锁、条件变量pthread_cond_t cond;pthread_mutex_t mutex;pthread_cond_init(&cond, nullptr);pthread_mutex_init(&mutex, nullptr);//创建新线程pthread_t tid[TNUM];func_t funcs[TNUM] = {func1, func2, func3, func4};for(int i = 0; i < TNUM; ++i){string name = "thread";name += to_string(i);ThreadData* td = new ThreadData(name, funcs[i], &mutex, &cond);pthread_create(tid+i, nullptr, Entry, (void*)td);}//主线程:唤醒新线程int count = 10;//执行10s退出while(count--){   cout << "awake thread: " << endl;pthread_cond_signal(&cond);//任意唤醒一个线程:并不关心具体是哪一个sleep(1);}quit = true;//此时func函数不满足条件,线程回到Entrycout << endl << "quit -> true." << endl;pthread_cond_broadcast(&cond);//虽然quit退出函数,但有线程处于等待条件状态,此处再统一唤醒。//只是为了演示两种唤醒函数。sleep(3);cout << endl;//等待新线程for(int i = 0; i < TNUM; ++i){pthread_join(tid[i],nullptr);cout << "join thread: " << tid[i] << endl;}//销毁锁、条件变量pthread_cond_destroy(&cond);pthread_mutex_destroy(&mutex);return 0;
}

  
  演示结果:
在这里插入图片描述

  
  
  
  
  
  
  
  

相关文章:

【ONE·Linux || 多线程(一)】

总言 多线程&#xff1a;进程线程基本概念、线程控制、互斥与同步。 文章目录 总言1、基本概念1.1、补充知识1.1.1、堆区细粒度划分1.1.2、虚拟地址到物理空间的转化 1.2、如何理解线程、进程1.2.1、如何理解线程&#xff1f;1.2.2、如何理解进程&#xff1f; 1.3、实践操作1.…...

华为智能企业上网行为管理安全解决方案(1)

华为智能企业上网行为管理安全解决方案&#xff08;1&#xff09; 课程地址方案背景需求分析企业上网行为概述企业上网行为安全风险分析企业上网行为管理需求分析 方案设计组网架构设备选型设备简介行为管理要点分析方案功能概述 课程地址 本方案相关课程资源已在华为O3社区发…...

Acwing 240. 食物链

Acwing 240. 食物链 题目描述思路讲解代码展示 题目描述 思路讲解 代码展示 #include <iostream>using namespace std;const int N 50010;int n, m; int p[N], d[N]; //p[]是并查集的father,d[]是距离int find(int x) {if (p[x] ! x) { //如果说x不是树根的话int t f…...

c++ 容器适配器

Container //创建一个命名空间&#xff0c;避免和库里的冲突 namespace chen {//写一个模版template<class T, class Container deque<T>>//开始写我们的类class stack{public:void push(const T& x){_con.push_back(x);}void pop(){_con.pop_back();}const …...

正则表达式的应用领域及基本语法解析

目录 一、正则表达式的应用领域 1. 文本搜索和替换 2. 表单验证 3. 数据提取和分析 4. 数据清洗和处理 5. URL路由和路由匹配 二、正则表达式的基本语法 1. 字符匹配 2. 元字符和字符类 3. 量词和边界 4. 分组和捕获 5. 转义字符 三、常见正则表达式示例 1. 邮箱…...

CIP或者EtherNET/IP中的PATH是什么含义?

目录 SegmentPATH举例 最近在学习EtherNET/IP&#xff0c;PATH不太明白&#xff0c;翻了翻规范&#xff0c;在这里记个笔记。下面的叙述可能是中英混合&#xff0c;有一些是规范中的原文我直接搬过来的。我翻译的不准确。 Segment PATH是CIP Segment中的一个分类。要了解PATH…...

使用lombok进行bulider之后调取HashMap的自定义方法进行对象操作报空指针异常(pojo也适用)

概论 这主要的问题就是bulider的特性的问题&#xff0c;就是他只能给你搭建了一个脚手架&#xff0c;里面的东西其实他没动你的&#xff0c;你得自己去给他实体化&#xff0c;如果你使用了类似HashMap等集合的话&#xff0c;你得自己去bulid一个在那个里面作为初始化对象你才可…...

矩阵-day14

...

上古神器:十六位应用程序 Debug 的基本使用

文章目录 参考环境上古神器 DebugBug 与 DebuggingDebugDebug 应用程序淘汰原因使用限制 DOSBox学习 Debug 的必要性DOSBox-X Debug 的基本使用命令 R查看寄存器的状态修改寄存器的内容 命令 D显示内存中的数据指定起始内存空间地址指定内存空间的范围 命令 A使用命令语法错误查…...

[学习笔记]ARXML - Data Format

参考AUTOSAR文档&#xff1a; https://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_TPS_ARXMLSerializationRules.pdfhttps://www.autosar.org/fileadmin/standards/R22-11/FO/AUTOSAR_TPS_ARXMLSerializationRules.pdf 编码 arxml只允许使用UTF-8编码&#xff…...

Go_原子操作和锁

原子操作和锁 本文先探究并发问题&#xff0c;再探究锁和原子操作解决问题的方式&#xff0c;最后进行对比。 并发问题 首先&#xff0c;我们看一下程序 num该程序表面看上去一步就可以运行完成&#xff0c;但是实际上&#xff0c;在计算机中是分三步运行的&#xff0c;如下…...

初识Java 12-1 流

目录 Java 8对流的支持 流的创建 随机数流 int类型的区间范围 generate() iterate() 流生成器 Arrays 正则表达式 本笔记参考自&#xff1a; 《On Java 中文版》 ||| 流的概念&#xff1a;流是一个与任何特定的存储机制都没有关系的元素序列。 流与对象的成批处理有关…...

【软件工程_UML—StartUML作图工具】startUML怎么画interface接口

StartUML作图工具怎么画interface接口 初试为圆形 &#xff0c;点击该接口在右下角的设置中->Format->Stereotype Display->Label&#xff0c;即可切换到想要的样式 其他方式 在class diagram下&#xff0c;左侧有interface图标&#xff0c;先鼠标左键选择&#xff0…...

单片机之瑞萨RL78定时计数器

单片机之瑞萨RL78定时计数器 使用瑞萨RL78定时计数器的简单例程。这个例程使用定时器0来产生一个以秒为单位的定时器中断&#xff0c;并在中断服务程序中增加一个全局变量以跟踪中断的发生。 首先&#xff0c;我们需要了解RL78的定时器0是一个16位的定时器&#xff0c;它的时钟…...

手机号码格式校验:@Phone(自定义参数校验注解)

需求 新增接口 和 修改接口 中&#xff0c;手机号码的格式校验是普遍需要的。 在每个手机号码字段上添加正则表达式校验注解来实现校验&#xff0c;重复书写&#xff0c;容易出错&#xff1b;在不同的手机号码字段上&#xff0c;可能使用了不同的校验规则&#xff0c;无法有效…...

ORACLE Redo Log Buffer 重做日志缓冲区机制的设计

最近和朋友包括一些国产数据库的研发人员交流&#xff0c;很多程序员认为 Oracle 已经过时&#xff0c;开源数据库或者他们研发的国产数据库才代表数据库发展的未来。甚至在很多交流会议上拿出自家产品的某一个功能点和 Oracle 对比就觉得已经遥遥领先。 实际上数据库系统的发展…...

PWN Test_your_nc Write UP

目录 PWN 00 解题过程 总结归纳 PWN 01 解题过程 总结归纳 PWN 02 解题过程 总结归纳 PWN 03 解题过程 总结归纳 PWN 04 解题过程 总结归纳 CTF PWN 开始&#xff01; 冲就完了 PWN 00 解题过程 ssh远程链连接 ssh ctfshowpwn.challenge.ctf.show -p28151 输…...

Centos7配置firewalld防火墙规则

这里写自定义目录标题 欢迎使用Markdown编辑器一、简单介绍二、特点和功能2.1、区域&#xff08;Zone&#xff09;2.2、运行时和永久配置2.3、服务和端口2.4、动态更新2.5、连接跟踪2.6、D-Bus接口 三、设置规则3.1、启动防火墙服务3.2、新建防火墙规则的服务&#xff0c;添加端…...

【新版】系统架构设计师 - 未来信息综合技术

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录 架构 - 未来信息综合技术考点摘要信息物理系统CPS的体系架构CPS 的技术体系CPS应用场景 人工智能分类关键技术机器学习 机器人发展分类机器人4.0 边缘计算概念与特点边云协同安全应用场景 数字孪生关键技…...

CAD二次开发LineSegment2d

在C#的CAD二次开发中&#xff0c;LineSegment2d 是AutoCAD的.NET API中的一个类&#xff0c;用于表示二维空间中的线段。它包含了起点和终点的坐标信息&#xff0c;并提供了一些方法用于进行线段之间的计算和判断。 LineSegment2d 类具有以下常用属性和方法&#xff1a; Star…...

Linux shell编程学习笔记5:变量命名规则、变量类型、使用变量时要注意的事项

跟其他的高级开发语言一样&#xff0c;Linux Shell编程中使用的数据也需要保存在变量中。 Shell使用变量来控制其行为&#xff0c;并且可以通过更改变量值来更改Shell和其他程序的行为。 我们先来了解一下变量命令的规则、变量类型和使用变量时要注意的事项。 一、变量命名规…...

如何把word的页眉页脚改为图片

前言 亲戚A&#xff1a; 听说你是计算机专业&#xff1f; 沐风晓月&#xff1a; 是啊 亲戚A&#xff1a; 那正好&#xff0c;来看看我这个页眉怎么改成图片 沐风晓月&#xff1a; 一万匹马奔腾而过 亲戚B&#xff1a; 听说你是英语专业&#xff1f; 沐风晓月&#xff1a; 是啊…...

spring6-实现简易版IOC容器

手写简易版IOC容器 1、回顾Java反射2、实现Spring的IoC 我们都知道&#xff0c;Spring框架的IOC是基于Java反射机制实现的&#xff0c;下面我们先回顾一下java反射。 1、回顾Java反射 Java反射机制是在运行状态中&#xff0c;对于任意一个类&#xff0c;都能够知道这个类的所…...

Feign接口调用GET请求@RequestParam传参丢失

文章目录 问题现象排查解决GET加注解解决使用POST方式解决 时间戳传参失败 问题现象 项目使用的是Spring Cloud微服务&#xff0c;服务间调用使用的是Feign在一次服务调用时&#xff0c;发现GET传参丢失&#xff0c;没有传递过去任何参数加了RequestParam注解&#xff0c;发现…...

LeetCode每日一题 | 309.买卖股票的最佳时机含冷冻期

题目链接&#xff1a; 309. 买卖股票的最佳时机含冷冻期 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 算法图解&#xff1a; 解题代码&#xff1a; class Solution { public:int maxProfit(vector<int>& prices) {int n prices.size();vector&…...

HTML的学习 Day02(列表、表格、表单)

文章目录 一、列表列表主要分为以下三种类型&#xff1a;1. 无序列表&#xff08;Unordered List&#xff09;&#xff1a;2. 有序列表&#xff08;Ordered List&#xff09;&#xff1a;将有序列表的数字改为字母或自定义内容li.../li 列表项标签中value属性&#xff0c;制定列…...

Android shape记录

之前一直觉得dataPath很好用&#xff0c;可以画各种矢量图。今天发现用shape画图也不错&#xff0c;记录一下自己用shape画的图。 一般使用shape就是定义形状、stroke边、solid内部、corners圆角等&#xff0c;代码 <?xml version "1.0" encoding "utf-8&q…...

WSL2和ubuntu的安装过程

目录 1.WSL2的安装 2.Ubuntu的安装 3.安装完成后的打开方式 1.WSL2的安装 按下WINX键&#xff0c;选择Windows PowerShell (管理员) 1.1执行以下命令&#xff0c;该命令的作用是&#xff1a;启用适用于 Linux 的 Windows 子系统 dism.exe /online /enable-feature /featur…...

力扣第150题 逆波兰表达式求值 stack c++

题目 150. 逆波兰表达式求值 中等 给你一个字符串数组 tokens &#xff0c;表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式。返回一个表示表达式值的整数。 注意&#xff1a; 有效的算符为 、-、* 和 / 。每个操作数&#xff08;运算对象&#xff09;都…...

三、飞行和射击

目录 1.飞行的实现 2.限制玩家视角 3.射击的实现 4.附录 1.飞行的实现 &#xff08;1&#xff09;在Player预制体上挂载Configuration Joint组件&#xff0c;并修改其Y Drive属性 &#xff08;2&#xff09; 修改PlayerInput.cs和PlayerController.cs以实现飞行 PlayerIn…...

网站开发哈尔滨网站开发公司/汽车软文广告

参考 基于PCNTL的PHP并发编程 PCNTL 是 PHP 中的一组进程控制函数&#xff0c;可以用来 fork&#xff08;创建&#xff09;进程&#xff0c;传输控制信号等。 在PHP中&#xff0c;进程控制支持默认关闭。编译时通过 --enable-pcntl 配置选项可以使 PHP的 CGI 或 CLI 版本打开进…...

推广公众号/seo关键词优化技巧

2018年11月13日至15日&#xff0c;由CNCF主办的KubeCon CloudNativeCon将首次登陆中国上海&#xff0c;这是全球范围内规模最大的Kubernetes和云原生技术盛会。 唯一聚焦客户实践的分论坛 11月13日&#xff0c;KubeCon行业客户云原生最佳实践日&#xff08;暨云原生技术实践峰…...

如何用自己电脑做网站服务器/无代码网站开发平台

哈喽&#xff01;大家好&#xff0c;我是小奇&#xff0c;一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术&#xff0c;如果你觉得通过小奇的文章学到了东西&#xff0c;那就给小奇一个赞吧 文章持续更新&#xff0c;可以微信搜索【小奇JAVA面试】第一时间阅…...

广州高档网站建设/排名优化公司口碑哪家好

之前做Spark大数据分析的时候&#xff0c;考虑要做Python的版本升级&#xff0c;对于Python2和Python3的差异做了一个调研&#xff0c;主要对于语法和第三方工具包支持程度进行了比较。基本语法差异核心类差异Python3对Unicode字符的原生支持Python2中使用 ASCII 码作为默认编码…...

建设厅网站沙场限期通知书/seo品牌优化百度资源网站推广关键词排名

今天探讨了下关于python的一个练习剪刀石头布&#xff0c;可能程序还是多少有些不足&#xff0c;欢迎各位批评指正&#xff1a; 代码如下&#xff1a; import random def main(): #将玩家和机器视为两个参量&#xff0c;机器产生随机数1-3&#xff0c;代表着1:石头,2:剪刀,3:布…...

学做网站视频/seo教学

Vue知识点总结 注&#xff1a;个人笔记不适用于他人学习~~ 1. 事件修饰符 Vue中事件修饰符 事件的执行阶段&#xff1a;捕获阶段&#xff08;父元素&#xff09; --> 事件源阶段&#xff08;被点击的内部子元素&#xff09; --> 事件冒泡阶段 1. stop 阻止冒泡如&…...