『 Linux 』线程控制

news2024/11/19 5:45:49

文章目录

    • 线程库
    • 线程的创建
    • 线程库中的线程ID
    • 线程等待及线程退出
    • C++11 中的线程库
    • 线程库的线程与轻量型进程


线程库

请添加图片描述

在Linux内核中没有实际的线程概念,只有轻量级进程的概念,即使用task_struct内核数据结构模仿线程;

所以本质上在Linux内核中无法直接调用系统调用接口创建线程,只能调用系统调用接口创建轻量级进程;

在Linux上需要对线程进行控制需要使用第三方线程库,即<pthread.h>,该库封装了Linux下的轻量级进程接口;

同时该库也成为了Linux平台下的默认库,即Linux系统将会自带该库;

该库为动态库,封装了一系列的线程控制接口,当使用该库创建线程时其对应的线程的结构将被该动态库进行组织与维护;

该库为第三方库,所以在使用该库时需要使用-lpthread动态链接该库;

该库中使用了大量的void*类型为用户提供了一定程度的泛型编程能力,使得其能够传递不同类型的变量,也包括对象;


线程的创建

请添加图片描述

线程的创建通常使用pthread_create()接口;

NAME
       pthread_create - create a new thread

SYNOPSIS
       #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.

RETURN VALUE
       On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.

当函数调用成功时返回0,调用失败时返回!0;

  • pthread_t *thread

    该参数为一个输出型参数,类型为pthread_t*,其中pthread_t类型本质上是一个对unsigned long int类型的typedef;

    typedef unsigned long int pthread_t;
    

    该输出型参数用于返回线程threadid;

  • const pthread_attr_t *attr

    该参数用于指向线程属性结构的指针,如果为nullptr则使用默认属性;

    可以用来设置线程的各种属性,如栈的大小,调度策略等,通常情况下使用nullptr默认情况即可;

  • void *(*start_routine) (void *)

    该参数为一个函数指针,用于传递一个函数;

    创建的新线程将会使用该回调函数,接收的参数为一个void*的类型的参数并返回void*类型的返回值;

    同时该函数为新线程的入口点;

  • void *arg

    传递给start_routine函数的参数,若是不需要传递参数则设置为nullptr;

该函数中众多的void*类型为用户提供了一定程度的泛型编程能力,使得其能够传递不同类型的变量,也包括类;

  • void*类型的大小

    void*void两个类型一个是空指针类型一个是空类型,两种类型都具有大小;

    其中void*为指针,大小与所有指针相同,在不同机器位的机器下的大小都不同;

    void的大小在Linux中一般为1;

    int main()
    {
      printf("sizeof(void) = %d\n", sizeof(void));
      printf("sizeof(void*) = %d\n", sizeof(void*));
      return 0;
    }
    

    结果为:

    $ ./mythread 
    sizeof(void) = 1
    sizeof(void*) = 8
    

    不同的是void*可以用来声明变量,void不能声明变量;

pthread_create()使用如下:

void* Print(void* arg) {
  while (true) {
    cout << "I am "<<(const char*)arg<<" , the PID : " << getpid() << endl;
    sleep(2);
  }
  return nullptr;
}

int main() {
  pthread_t newthread;
  pthread_create(&newthread, nullptr, Print, (void*)"new_thread1");
  sleep(1); // 确保线程已经被运行

  while (true) {
    cout << "I am main thread , the PID : " << getpid() << endl;
    sleep(1);
  }
  return 0;
}

定义一个pthread_t类型的变量用于接收该新线程的返回id;

使用pthread_create()函数创建一个线程并传入一个字符串作为回调函数的参数并强转为(void*)以方便传参;

同时设置了一个名为Print()函数作为该线程的入口点;

新线程所用函数与主线程相同都对自身的PID进行打印;

使用-lpthread编译后可使用ldd进行验证该库是否为第三方库;

$ ldd mythread
	linux-vdso.so.1 =>  (0x00007ffe6f966000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f6598b80000) # 已链接pthread库
	libstdc++.so.6 => /home/__USR/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6 (0x00007f65987ff000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f65984fd000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f65982e7000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f6597f19000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f6598d9c000)

在新窗口中使用shell脚本对进程进行监控;

$ while :; 
do ps axj | head -1 && ps axj | grep mythread | grep -v grep ; 
echo "----------------------------" ; 
sleep 1 ; 
done

执行结果为如下:

# 程序所在窗口
$ ./mythread 
I am new_thread1 , the PID : 6974
I am main thread , the PID : 6974
...

# shell脚本所在窗口
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 6416  6974  6974  6416 pts/0     6974 Sl+   1001   0:00 ./mythread
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 6416  6974  6974  6416 pts/0     6974 Sl+   1001   0:00 ./mythread
----------------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 6416  6974  6974  6416 pts/0     6974 Sl+   1001   0:00 ./mythread

其中脚本可以看出只有一个进程在运行,且进程中的两个线程作为不同的执行流都打印同一个PID;

可使用ps -aL | grep <Processname>查看当前对应运行进程中的轻量级进程的基本属性(该命令为内置命令,且Linux中不存在线程概念,所以查看的属性为轻量级进程的基本属性);

$ ps -aL | head -1 && ps -aL |  grep mythread
  PID   LWP TTY          TIME CMD
 6974  6974 pts/0    00:00:00 mythread
 6974  6975 pts/0    00:00:00 mythread

其中LWPLight-weight process 轻量级进程 的缩写,对应的PIDLWP相同的轻量级进程即可看出是 “主线程” ;


线程库中的线程ID

请添加图片描述

pthread_create()函数中的*thread参数用于返回所创建线程的tid;

而实际上该参数所返回的是一个地址;

  • pthread_self()

    在线程中可使用pthread_myself()接口获取自身线程的tid;

    NAME
           pthread_self - obtain ID of the calling thread
    
    SYNOPSIS
           #include <pthread.h>
    
           pthread_t pthread_self(void);
    
           Compile and link with -pthread.
    
    DESCRIPTION
           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.
    
    RETURN VALUE
           This function always succeeds, returning the calling thread's ID.
    

    返回值为一个pthread_t类型的参数,即线程本身的tid;

void* Print(void* n) {
  printf("I am newthread , the tid:%p\n",pthread_self());
  sleep(1);
  return nullptr;
}

int main() {
  pthread_t newthread;
  pthread_create(&newthread, nullptr, Print, nullptr);
  sleep(1);  // 确保线程已经被运行
  printf("the new thread tid : %p\n", newthread);
  sleep(3);
  return 0
}

该程序中创建了一个新线程,并且在新线程中使用pthread_self()获取自身线程tid并打印;

运行结果如下:

$ ./mythread 
I am newthread , the tid:0x7f18642ef700
the new thread tid : 0x7f18642ef700

主线程与新线程打印tid结果相同;

本质上是因为在Linux中线程是<pthread.h>线程库所提供的概念,当需要用到线程时需要将<pthread.h>库链接至内存共享区中;

线程的概念为<pthread.h>提供,故该库也应需要对线程进行组织与管理,使用<pthread.h>库所创建的线程(除主线程外)都将被该库在共享区中进行组织与管理;

故在Linux(CentOS7)中,本身pthread_t *thread参数为一个可以索引至共享区中对该应线程属性结构的线性地址(虚拟地址);

由于地址具有唯一性,该设计可以使得其TID既保证了能够进行唯一标识,也可以快速(直接或间接)访问到对应的TCB结构体当中;

一般来说该TCB结构体为了防止线程的安全用户无法直接访问,只能通过该库内部的机制进行间接访问;


线程等待及线程退出

请添加图片描述

线程与进程相同都需要被等待,否则将出现类似于僵尸进程一样的问题,即线程已经执行完毕但未被回收导致资源被占用;

通常主线程需要使用pthread_join()对新线程进行等待回收;

NAME
       pthread_join - join with a terminated thread

SYNOPSIS
       #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);

       Compile and link with -pthread.

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

该函数调用成功时返回0,失败时返回!0;

  • pthread_t thread

    该参数用于需要等待的一个线程ID,即调用该函数的前提是必须存在一个pthread库所创建的线程;

  • void **retval

    该参数用于返回线程所调用回调函数的返回值;

    以二级指针的方式对返回值进行接收;

    参数返回时不能直接返回线程栈上开辟空间的数据,因为线程的栈是其独立的,当程序执行完毕后表示该线程的生命周期结束,对应的其栈的空间将会被回收;

    如果直接返回线程栈中的数据则可能会出现段错误;

    若是不关心该线程的返回值或是线程无有用返回值可设置为nullptr;

void* Print(void* arg) {
  cout << "I am " << (const char*)arg << " , the PID : " << getpid() << endl;

  int* n = new int;
  *n = 42;
  cout << "new thread quit..." << endl;
  sleep(1);
  return n;
}

int main() {
  pthread_t newthread;
  pthread_create(&newthread, nullptr, Print, (void*)"new_thread1");
  sleep(1);  // 确保线程已经被运行

  cout << "I am main thread , the PID : " << getpid() << endl;
  unsigned long int* ret;
  pthread_join(newthread, (void**)&ret);
  sleep(2);

  printf("new thread quit... , the exit_code:%lu\n", *ret);
  delete ret;

  return 0;
}

该程序中创建了一个线程,并在该线程的结束时返回一个通过new创建的一个数据(42)以避免直接返回线程栈中的数据;

主线程中创建了一个ret变量用于接收该线程执行完毕的返回值并对该返回值进行打印,调用pthread_join()等待该线程;

可使用shell脚本对线程进行观察:

$ while :;
do ps -aL | head -1 && ps -aL |  grep mythread ;
echo "------------------------------------" ; 
sleep 1 ;
done

结果如下:

# 程序所在会话
$ ./mythread 
I am new_thread1 , the PID : 8942
new thread quit...
I am main thread , the PID : 8942
	## 中间间隔了两秒 
new thread quit... , the exit_code:42


# shell脚本所在会话
  PID   LWP TTY          TIME CMD
 8942  8942 pts/0    00:00:00 mythread
 8942  8943 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
 8942  8942 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
 8942  8942 pts/0    00:00:00 mythread
------------------------------------
  PID   LWP TTY          TIME CMD
------------------------------------

运行结果为线程所调用的回调函数返回值被主线程接收,同时可以清楚看到新线程执行完毕后退出同时资源被回收;

主线程中间隔了两秒才打印最终结果表示该函数默认行为为阻塞等待;

  • 线程的退出

    线程的退出无法使用exit();

    本质原因是exit()的退出是调用了_exit()系统调用接口的,该系统调用接口直接会将进程进行退出,退出的是进程而不是线程;

    一般情况下线程的退出不仅可以使用return,也可以使用pthread_exit()函数;

    NAME
           pthread_exit - terminate calling thread
    
    SYNOPSIS
           #include <pthread.h>
    
           void pthread_exit(void *retval);
    
           Compile and link with -pthread.
    

    该函数为退出正在执行的线程;

    传递一个void*类型的参数以表示该线程的返回值,通常该值不能是线程栈上开辟的空间中的数据,必须是在非栈上这种共享空间中,因为该线程的生命周期已经结束,若是访问线程栈中的数据将会导致段错误;

    • pthread_cancel()

      该接口函数可以在主线程中取消一个对应tid的线程;

      NAME
             pthread_cancel - send a cancellation request to a thread
      
      SYNOPSIS
             #include <pthread.h>
      
             int pthread_cancel(pthread_t thread);
      
             Compile and link with -pthread.
      
      RETURN VALUE
             On success, pthread_cancel() returns  0;  on  error,  it  returns  a
             nonzero error number.
      

      参数为pthread_t thread类型表示需要传入一个该线程对应的标识符tid;

      当主线程使用该函数取消一个线程时对应的该线程会返回一个PTHREAD_CANCELED的宏,该宏的值默认为-1;

  • 线程的崩溃

    进程在退出时具有三种情况:

    • 程序执行完毕,结果正确
    • 程序执行完毕,结果错误
    • 程序未执行完毕异常退出

    线程不具备第三种情况,原因是异常结束本质上是收到了信号,而信号的概念是与进程相关的;

    这表明线程收到信号后就代表进程收到了信号;

  • 使用线程函数传入对象完成总数相加计算小程序

    该程序为计算startend相加的和;

    class Request {
     public:
      Request(int start, int end, const string &threadname)
          : start_(start), end_(end), threadname_(threadname) {}
    
     public:
      int start_;
      int end_;
      string threadname_;
    };
    
    class Response {
     public:
      Response(int result, int exitcode) : result_(result), exitcode_(exitcode) {}
    
     public:
      int result_;
      int exitcode_;
    };
    
    void *sumCount(void *args) {
      Request *rq = static_cast<Request *>(args); // static_cast<Request *>(args) 类比于 (Request*)args
      Response *ret = new Response(0, 0);
      for (int i = rq->start_; i <= rq->end_; ++i) {
        ret->result_ += i;
      }
      return ret;
    }
    
    int main() {
      pthread_t tid;
      Request *rq = new Request(1, 100, "Thread1");
      pthread_create(&tid, nullptr, sumCount, rq);
    
      Response *rsp;
      pthread_join(tid, (void **)&rsp);
    
      printf("the sumSount sucess , the result is %d , the exitcode is %d\n",
             rsp->result_, rsp->exitcode_);
      delete rsp;
      delete rq;
      return 0;
    }
    

    该程序设计了两个类,分别为计算数值传入的类Request,一个为返回对应计算结果与退出码的类Response;

    并设计了一个sumCount()函数作为线程的入口点也是用于计算的方法;


C++11 中的线程库

请添加图片描述

C++11已经支持了线程库;

在不同的平台下其封装的底层实现不同,在Linux下C++11的线程库主要是对pthread库的封装

这意味在Linux下使用C++11的线程库时仍需在编译时链接对应的pthread库;

void thread_cpp() {
  while (true) {
    cout << " I am a new thread from cpp" << endl;
    sleep(1);
  }
}

int main() {
  thread t(thread_cpp);
  t.join();  // 用于等待线程 对线程进行清理工作
  return 0;
}

运行结果为:

$ ./mythread 
 I am a new thread from cpp
 I am a new thread from cpp
 ...
^C

线程库的线程与轻量型进程

请添加图片描述

pthread线程库本质上就是封装了Linux系统关于轻量型进程的系统调用接口;

在Linux系统中创建轻量级进程的系统调用接口为clone();

NAME
       clone, __clone2 - create a child process

SYNOPSIS
       /* Prototype for the glibc wrapper function */

       #include <sched.h>

       int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

       /* Prototype for the raw system call */

       long clone(unsigned long flags, void *child_stack,
                 void *ptid, void *ctid,
                 struct pt_regs *regs);

   Feature   Test   Macro   Requirements   for   glibc   wrapper   function  (see  fea‐
   ture_test_macros(7)):

       clone():
           Since glibc 2.14:
               _GNU_SOURCE
           Before glibc 2.14:
               _BSD_SOURCE || _SVID_SOURCE
                   /* _GNU_SOURCE also suffices */

RETURN VALUE
       On  success,  the  thread  ID  of  the child process is returned in the caller's
       thread of execution.  On failure, -1 is returned in  the  caller's  context,  no
       child process will be created, and errno will be set appropriately.

该函数用来创建一个新的进程(轻量型进程);

其中本质上fork()系统调用接口就是封装了该接口;

当该函数调用成功时将返回新轻量型进程的ID,失败时返回-1并设置errno;

  • int (*fn)(void *)

    该参数用于传递一个回调函数,该回调函数为该轻量型进程的入口点;

  • void *chile_stack

    这是一个指向子进程的栈底指针,新创建的这个进程(轻量型进程)将使用这个栈;

    通常着需要指向一个预先分配好的栈内存区域,并且栈的顶端应该对齐;

  • int flags

    这是一个标志位参数,用于指定新进程的行为和资源共享方式;

  • void *arg

    该参数用于传给fn的参数,子进程将以此为参数调用fn函数;

  • 可选参数

    pid_t *ptid,struct user_desc *tls,pid_t *ctid;

对应的pthread_create()则是封装了该函数;

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

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

相关文章

人工智能学习①

LLM背景知识介绍 大语言模型 (LLM) 背景 用于理解和生成人类语言&#xff0c;能够处理诸如文本分类、问答、翻译和对话等多种自然语言任务。 语言模型 (Language Model, LM) &#xff1a;给定一个短语&#xff08;一个词组或者一句话&#xff09;语言模型可以生成&#xff0…

机器学习数学基础(1)--线性回归与逻辑回归

声明&#xff1a;本文章是根据网上资料&#xff0c;加上自己整理和理解而成&#xff0c;仅为记录自己学习的点点滴滴。可能有错误&#xff0c;欢迎大家指正。 1 线性回归和逻辑回归与机器学习的关系 线性回归属于机器学习 – 监督学习 – 回归 – 线性回归&#xff0c; 逻辑…

Apache DolphinScheduler Worker Task执行原理解析

大家好&#xff0c;我是蔡顺峰&#xff0c;是白鲸开源的高级数据工程师&#xff0c;同时也是Apache DolphinScheduler社区的committer和PMC member。今天我要分享的主题是《Worker Task执行原理》。 整个分享会分为三个章节&#xff1a; Apache DolphinScheduler的介绍Apache …

数据结构——二叉树定义

一、二叉树概念 二叉树是一种树形数据结构&#xff0c;其中每个节点最多有两个子节点&#xff0c;通常称为左子节点和右子节点。每个子节点本身又可以是一个二叉树。二叉树在计算机科学中有着广泛的应用&#xff0c;例如在搜索算法、排序算法等领域 二叉树(Binary Tree)是n(n…

告别繁琐,2024年PDF合并神器搜罗

有时候我们下载得到的PDF文件可能是被拆分成多份文档&#xff0c;这样对于我们查看文件就会造成一定的困扰。这时候如果把他们合并为一份文件就能方便很多。这次我就介绍几款pdf合并工具来解决这个问题吧。 第一款EIDTOR 福昕PDF 链接&#xff1a;https://editor.foxitsoftwar…

C++ STL 容器之deque

deque与vector同属C STL容器&#xff0c;二者有些相似。deque 采用动态数组来管理元素&#xff0c;提供随机存取&#xff0c;它与vector 几乎一摸一样的接口。不同的是&#xff1a;deque的动态数组头尾都开放&#xff0c;能在头尾两端进行快速安插和散出。下面是deque与vector的…

android前台服务

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 未经允许不得转载 目录 一、导读二、使用2.1 添加权限2.2 新建…

nginx 版本升级

Nginx 的版本最开始使用的是 Nginx-1.18.0 &#xff0c; 由于服务升级&#xff0c;需要将 Nginx 的版本升级到 Nginx-1.19.7 &#xff0c;要求 Nginx 不能中断提供服务。 为了应对上述的需求&#xff0c;提供两种解决方案&#xff1a; 方案1&#xff1a; make upgrade 完成升…

(二十四)进阶算法

文章目录 &#xff08;一&#xff09;埃氏筛法1. 原理2. 代码3. 特点 &#xff08;二&#xff09;欧拉筛法1. 原理2. 代码3. 特点 &#xff08;三&#xff09;分解质因数1. 原理2. 代码 &#xff08;四&#xff09;斐波那契数列1. 递推式2. 代码(1) 方法1(2) 方法2 经过12天的“…

[240728] Wikidata 介绍 | 微软与 Lumen 合作提升人工智能算力

目录 Wikidata 介绍微软与 Lumen 合作提升人工智能算力 Wikidata 介绍 中文&#xff1a; 文言: 粤语&#xff1a; 来源&#xff1a; https://www.wikidata.org/wiki/Wikidata:Introduction/zh 微软与 Lumen 合作提升人工智能算力 为了满足人工智能工作负载不断增长的需求&am…

(2024,通用逼近定理(UAT),函数逼近,Kolmogorov–Arnold定理(KAT),任意深度/宽度的网络逼近)综述

A Survey on Universal Approximation Theorems 公和众与号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 1. 简介 2. 神经网络&#xff08;NN&#xff09; 3. 通用逼近定理&#xff0…

openssh服务升级到最新版本OpenSSH-9.8p1完全手册---- (只适用于centos6)

[年] 在centos6下编译openssh-9.8p1的rpm包 1、创建用于rpm编译的目录 mkdir -p /root/rpmbuild/SPEC mkdir -p /root/rpmbuild/SOURCES 2、安装rpmbuild和一些其它的基本依赖 yum install gcc gcc-c rpm-build -y 3、上传openssh-9.8p1.tar.gz 这个源码包到centos6服务器上&am…

一篇文章教你如何读懂 JMeter聚合报告参数!

在进行性能测试时&#xff0c;JMeter是一款备受推崇的开源工具。而其中的聚合报告&#xff08;Aggregate Report&#xff09;是我们分析测试结果、了解系统性能的重要依据。今天&#xff0c;我们就来深入探讨如何读懂JMeter聚合报告中的各项参数。 面对复杂的聚合报告&#xf…

MySQL创建表完全指南-从零开始学习数据库设计

MySQL创建表快速指南 在大数据时代,掌握数据库技能至关重要。无论你是刚入门的开发者,还是经验丰富的数据分析师,了解如何创建MySQL表格都是必备技能。本文将为你详细讲解MySQL创建表格的全过程,帮助你快速上手数据库设计。 1. 连接到MySQL服务器 首先,确保你已经安装了MyS…

Linux 的超级记事本(代码编辑器) —— vim

Linux 的超级记事本&#xff08;代码编辑器&#xff09; —— vim 关于 vimvim 的使用入门级使用——多模式基础使用——多模式插入模式&#xff08;Insert mode&#xff09;理解 命令模式&#xff08;command mode&#xff09;理解命令集 底行模式&#xff08;last line mode&…

Logback 快速入门

一、简介 Java 开源日志框架&#xff0c;以继承改善 log4j 为目的而生&#xff0c;是 log4j 创始人 Ceki Glc 的开源产品。 它声称有极佳的性能&#xff0c;占用空间更小&#xff0c;且提供其他日志系统缺失但很有用的特性。 其一大特色是&#xff0c;在 logback-classic 中本…

5G 基站特有的 5 个关键同步挑战

随着 5G 的推出和 O-RAN 联盟等举措&#xff0c;移动设备领域正在遭遇相当大的颠覆&#xff0c;这当然适用于基站和移动回程。 从手机到物联网设备&#xff0c;设备数量呈爆炸式增长&#xff0c;再加上移动视频流、工业物联网和汽车应用等新应用&#xff0c;给移动网络带来了容…

自学JavaScript(放假在家自学第一天)

目录 JavaScript介绍分为以下几点 1.1 JavaScript 是什么 1.2JavaScript书写位置 1.3 Javascript注释 1.4 Javascript结束符 1.5 Javascript输入输出语法 JavaScript(是什么?) 是一种运行在客户端(浏览器)的编程语言&#xff0c;实现人机交互效果。 2.作用(做什么?)网…

算法-插入排序

插入排序步骤 前面文章分享了两种排序算法&#xff1a;冒泡排序和选择排序。虽然它们的效率都是O(N2)&#xff0c;但其实选择排序比冒泡排序快一倍。现在来学第三种排序算法——插入排序。你会发现&#xff0c;顾及最坏情况以外的场景将是多么有用。 插入排序包括以下步骤。 …

从0开始搭建vue + flask 旅游景点数据分析系统(一):创建前端项目

根据前面的爬虫课程&#xff0c;我们重新开一个坑&#xff0c;就是基于爬取到的数据&#xff0c;搭建一个vueflask的前后端分离的数据分析系统 1 通过这个系列教程可以学习到什么&#xff1f; 从0开始搭建一个 vue flask 的数据分析系统&#xff1b;了解系统的整体架构&…