Linux基础 (十二):Linux 线程的创建与同步

news2024/10/6 6:42:32

       本篇博客详细介绍与线程有关的内容,这部分也是笔试面试的重点,需要我们对线程有深刻的理解,尤其是线程的并发运行以及线程同步的控制!接下来,让我们走进线程的世界,去理解线程,使用线程!

目录

一、线程的概念与实现方式

1.1线程的概念

1.2线程的实现方式

1.3进程与线程的区别

1.4 代码中如何实现多线程

二、线程使用

2.1 线程库中的接口介绍

2.1.1  创建线程函数

2.1.2 退出当前线程函数

2.1.3  等待子线程结束函数

2.2 线程并发运行

2.2.1 示例代码1

2.2.2 示例代码2

三、线程同步

3.1 线程同步的概念

3.2 信号量

3.2.1 函数接口介绍

3.2.1.1  信号量初始化函数

3.2.1.2 等待信号量(P操作) 

3.2.1.3 释放信号量(V操作)

3.2.1.4 销毁信号量

3.2.2 使用示例

3.2.3  面试题

3.3 互斥锁

3.3.1 函数接口介绍

3.3.1.1 互斥锁初始化函数

3.3.1.2  加互斥锁函数

3.3.1.3  解互斥锁函数

3.3.1.4  销毁互斥锁函数

3.3.2 使用示例

3.4 条件变量

3.4.1 函数接口介绍

3.4.1.1 初始化条件变量

3.4.1.2 唤醒一个等待线程

3.4.1.3 唤醒所有等待线程

3.4.1.4 等待条件变量

3.4.1.5  销毁条件变量

3.4.2 使用示例

3.5 读写锁

3.5.1 函数接口介绍

3.5.1.1 读写锁初始化

3.5.1.2 获取读锁

3.5.1.3 获取写锁

3.5.1.4 销毁读写锁

3.5.1.5 释放锁(读锁或写锁)

3.5.2 使用示例

四、线程安全

4.1 概念

4.2 多线程环境如何保证线程安全

4.3 举例理解

​五、线程与fork

 5.0 多线程的PID

 5.1 多线程中某个线程调用 fork(),子进程会有和父进程相同数量的线程吗?

5.1.1 主线程调用fork()产生子进程

5.1.2  子线程调用fork()产生子进程

5.2 父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁?

六、生产者消费者模型(面试题)

6.1 生产者消费者问题概述

6.2 生产者消费者模型优点

6.3 生产者消费者模型实现

 七、面试题总结


一、线程的概念与实现方式

1.1线程的概念

        我们前面学过进程,我们知道进程是一个正在运行的程序,那么线程是什么呢?其实,线程就是进程中的一条执行路径,我们之前写的程序(进程),只有一个主函数,程序(进程)是从这里开始运行的,其实这也是线程,它可以被叫做主线程,因此,可以说:进程至少有一个线程,而这个线程就是就是主线程,我们也把这种进程叫单线程程序;一个进程是可以有多个线程的,也叫做多线程程序,并且这些线程(子线程)和主线程是同时并发运行的;进程是从资源分配角度上看,线程是从调度执行角度上看.

 

并发运行

      指的是在一个时间段内,多个任务(线程)交替进行。并发并不意味着这些任务(线程)是同时进行的,而是多个任务(线程)在同一时间段内被调度器交替执行,从而使用户感觉这些任务(线程)在同时进行。并发运行的关键在于任务(线程)之间的切换和协调。

  • 例子: 在单核处理器上运行多个应用程序。操作系统通过快速切换任务(线程),使用户感觉多个程序在同时运行。
  • 特点: 并发运行强调任务(线程)间的交替执行和资源共享,适用于I/O操作频繁的场景。

并行运行(Parallelism)

        指的是在同一时刻,多个任务真正地同时进行。并行运行需要多核处理器或多个处理器来实现,每个任务在独立的核心上运行,从而实现真正的同时执行。

  • 例子: 在多核处理器上运行一个可以被分解为多个独立部分的应用程序,如科学计算中的矩阵乘法。
  • 特点: 并行运行强调任务的同时执行,适用于计算密集型任务。

1.2线程的实现方式

     在操作系统中,线程的实现有以下三种方式:

◼ 内核级线程

◼ 用户级线程

◼ 组合级线程

  1. 用户级用户空间有很多线程,但在内核里只有一个线程,所以即使有多个处理器空闲 ,用户空间的多个线程也只能交替使用这一个处理器。
  2. 内核级:内核空间能够感知到用户空间有多少条线程,假如有俩个处理器空闲,这三条线程中的某俩条就能在同一时刻并行。内核级创建的开销很大。
  3. 组合(混合):根据处理器的数目,允许在内核中创建多条执行路径,可以和处理器的数目有关联,比如有四个处理器,我们最多让内核中有8/4个线程,让每个线程都能用到处理器。超出这个数目的线程,从用户空间来体现。解决了可以创建多个线程,也能使用多个处理器的问题。

Linux 中线程的实现(面试)

       Linux 实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux 把 所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来 表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯 一隶属于自己的 task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和 其他一些进程共享某些资源,如地址空间)。

1.3进程与线程的区别

       进程 (Process)

  1. 定义:

           进程是一个独立的执行单元,具有自己的内存空间和资源。它是操作系统进行资源分配和调度的基本单位。
  2. 内存空间:

          每个进程有自己独立的内存地址空间。一个进程无法直接访问另一个进程的内存。
  3. 资源开销:

          创建和销毁进程的开销较大,因为需要分配独立的内存空间和系统资源。
  4. 通信:

          进程间通信 (Inter-Process Communication, IPC) 比较复杂且耗时,可以通过管道、消息队列、共享内存等方式实现。
  5. 独立性:

          进程之间是相对独立的,一个进程的崩溃通常不会影响到其他进程。

        线程 (Thread)

  1. 定义:

         线程是进程中的一个执行单元。一个进程可以包含多个线程,线程是操作系统调度的最小单位。
  2. 内存空间:

         同一进程内的线程共享进程的内存地址空间和资源,因此它们可以直接访问彼此的内存。
  3. 资源开销:

         创建和销毁线程的开销较小,因为线程共享进程的资源,不需要分配独立的内存空间。
  4. 同步:

         线程间同步比较简单,因为它们共享同一进程的内存空间,可以通过信号量、互斥锁、条件变量、读写锁等方式实现。
  5. 独立性:

           线程之间的独立性较低,一个线程的崩溃可能会导致整个进程的崩溃,因为它们共享同一个内存空间。

       并发运行:只有一个处理器(CPU)两个线程交替执行,并行运行:有两个处理器(CPU)两个线程同时运行,并行也是特殊的并发。

例子

  • 进程的例子:
    • 每个运行中的程序如浏览器、文本编辑器都是一个独立的进程。
  • 线程的例子:
    • 一个浏览器进程可能包含多个线程,如一个线程负责渲染网页,另一个线程负责处理用户输入。

1.4 代码中如何实现多线程

       主线程:就是从main函数的第一行执行到最后一行,想要多个线程,就在写个线程函数thread_fun(),可以跟主线程函数并发执行。想要三个线程,另外两个线程,干同一个事就写一个线程函数,如果让他们干不同的事就要再写两个线程函数。

二、线程使用

2.1 线程库中的接口介绍

         线程库提供了丰富的接口来创建、管理和同步线程。不同的操作系统和编程语言提供的线程库接口可能有所不同,但大多数线程库都提供了一些常见的基础功能。下面以POSIX线程库(Pthreads)为例,介绍一些常用的线程库接口,使用线程库需要提前引入线程库头文件。

   线程库需要引入的头文件 #include <pthread.h>

2.1.1  创建线程函数

pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

 线程创建成功返回 0, 失败返回错误码。

参数说明:

  • thread:指向线程标识符的指针,线程的id地址 。
  • attr:线程属性,通常传递NULL使用默认属性。
  • start_routine:它是一个函数指针,(此函数必须是返回值为void *,参数也是void * ) 它是指向线程函数的。
  • arg:传递给线程函数的参数。

示例1: 主线程创建1个子线程,两个线程并发运行,各自循环5次打印字符串。注意:线程库是共享库,编译的时候需要加上库(-l+库名),即:gcc -o  test  test.c  -lpthread 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* fun(void*arg)    //子线程函数
{
    for(int i=0;i<5;i++)
    {
        printf("fun hello\n");
    }
}
int main()      //主线程
{
    pthread_t id;
    pthread_create(&id,NULL,fun,NULL);//创建线程,creat函数成功后会给线程ID进行初始化。
    for(int i=0;i<5;i++)
    {
        printf("main hello\n");
    }

    exit(0);
    
}

运行结果:

第一次运行结果分析:

          主线程运行非常快,当主线程运行完,调用exit()退出进程,那么子线程是没有机会再运行的,因此,只打印了主线程的,子线程不会打印!

第一次运行结果分析:

         子线程运行的非常快,因此,它是有机会打印出来的,它打印完后主线程也会打印,注意:子线程结束不会影响到整个进程,而主线程结束后,整个进程就会结束,所有的子线程也随之终止!一般都会让主线程活到最后。

修改代码如下:加入睡眠,整个程序运行至少5秒,睡眠1秒的时候,线程会交出CPU的使用权。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* fun(void*arg)              //子线程函数
{
    for(int i=0;i<5;i++)
    {
        printf("fun hello\n");
        sleep(1);               //睡眠1秒,交出CPU使用权
    }
}
int main()                       //主线程
{
    pthread_t id;
    pthread_create(&id,NULL,fun,NULL);//创建线程,creat函数成功后会给线程ID进行初始化。
    for(int i=0;i<5;i++)
    {
        printf("main hello\n");
        sleep(1);             //睡眠1秒,交出CPU使用权
    }

    exit(0);
    
}

运行结果如下:

        可以看到:二者是交替运行的,但是两个的先后顺序是不确定的!也就是每次程序执行的结果是不确定的,这样便失去了控制,这也是线程需要控制的一个问题!

       通过上面的结果,我们知道:主线程结束,整个进程都会结束,子线程也不会运行,如下所示:修改主线程的运行次数为2.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* fun(void*arg)              //子线程函数
{
    for(int i=0;i<5;i++)
    {
        printf("fun hello\n");
        sleep(1);               //睡眠1秒,交出CPU使用权
    }
}
int main()                       //主线程
{
    pthread_t id;
    pthread_create(&id,NULL,fun,NULL);//创建线程,creat函数成功后会给线程ID进行初始化。
    for(int i=0;i<2;i++)
    {
        printf("main hello\n");
        sleep(1);             //睡眠1秒,交出CPU使用权
    }

    exit(0);
    
}

运行结果如下:

可以看到:只要主线程运行两次,子线程就会结束(子线程运行次数少于5次)。

那么如何让主线程结束之后,不要直接结束整个进程呢?那就是让主线程等待子线程结束,就是下面这个函数。pthread_join()  ,修改代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>



void* fun(void*arg) 
{
    for(int i=0;i<5;i++)
    {
        printf("fun hello\n");
        sleep(1);
    }
        pthread_exit("fun over");  //注意:返回给主线程的数据不能传递临时变量,它会随着函数结束而销毁
}
int main()
{
    pthread_t id;
    pthread_create(&id,NULL,fun,NULL);//creat函数成功后会把线程id,传给id的。
    for(int i=0;i<2;i++)
    {
        printf("main hello\n");
        sleep(1);
    }
    char*s=NULL;
    pthread_join(id,(void**)&s);//主线程会阻塞住,等待子线程结束,并接受子线程返回的信息
    printf("main s=%d\n",s);
}
}

pthread_join() 它会等待子线程的结束,第一个参数是线程id,第二个参数是二级指针,一级指针的地址,改变指针的指向,使子线程能给主线程返回信息,让指针指向子线程返回的值。

pthread_exit(); 它的作用是退出子线程,并且会返回给主线程一个信息,主线程通过指针s指向它,然后再打印出来! 千万不能写exit() ,这个函数会直接结束整个进程!!

运行结果如下:

      执行过程如下:主线程打印2次,然后被阻塞住,等待子线程打印5次,退出子线程,将返回信息给主线程,然后主线程打印子线程返回给它的信息。

2.1.2 退出当前线程函数

void pthread_exit(void *retval);
  • 参数

    • retval:一个指向返回值的指针,线程的退出状态可以通过它来传递。
  • 说明

    • 当一个线程调用pthread_exit时,该线程会终止执行,其退出状态会被传递给其他线程(例如通过pthread_join函数)。
    • 调用pthread_exit不会终止整个进程,只会终止调用它的线程。
    • 如果主线程调用了pthread_exit,主线程会终止,但其他子线程会继续运行,直到它们自己终止或者整个进程终止。

2.1.3  等待子线程结束函数

int pthread_join(pthread_t thread, void **retval);
  • 参数

    • thread:要等待终止的线程的线程ID。
    • retval:一个指向指针的指针,用于存储子线程的退出状态。如果不需要获取退出状态,可以传递NULL
  • 返回值

    • 成功时返回0
    • 失败时返回一个错误码。

2.2 线程并发运行

2.2.1 示例代码1

     主线程创建5个子线程,让它们打印它们是第几个被创建的(通过传地址,然后子线程解引用拿到对应的i )。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>



void*fun(void*arg)
{
    int index=*(int*)arg;            //通过解引用拿到传递过来的值
    for(int i=0;i<3;i++)
    {
        printf("index=%d\n",index);
        sleep(1);
    }
}
int main()
{
    pthread id[5];
    int i=0;
    for(;i<5;i++)                     //创建5个子线程
    {
        pthread_creat(&id[i],NULL,(void*)&i);  //把创建的顺序作为参数传递给子线程函数
    }

    for(i=0;i<5;i++)                //等待5个子线程结束
    {
        pthread_join(id[i],NULL);
    }
    exit(0);
}

      按照我们对上述程序的理解:主线程创建5个子线程,把对应的创建顺序(0 、1 、2 、3 、4)传递给子线程,然后5个子线程每一轮打印自己的创建顺序,一轮打印完,休眠1秒,然后继续打印下一轮,一共打印3轮。即结果应该是:0 1 2 3 4  0 1 2 3 4  0 1 2 3 4 

实际运行结果如下:

分析:首先,主线程向内核申请5个子线程(这个时间非常快),然后内核依次一个一个的创建出5个子线程,等5个子线程全部创建完毕后,然后CPU开始进行调度这5个子线程执行,但是5个子线程的执行顺序与我们的创建的顺序未必是相同的,这是由调度算法决定的(处理器的个数是固定的,第一个创建的子线程未必就是第一个运行的),子线程在启动运行后,只有执行到 int index=(int)arg;这一行了,才去获取i的值,如果有多个线程同时启动(并且是多核处理器,多个线程并行运行),它们拿到的都是同一个值 i, 因此,他们几个都会打印出相同的值;如果出现全是0,代表:当主线程创建出5个子线程后(时间非常快),然后置0了,并且在里面阻塞了至少3秒,这时候5个子线程去获取i的值,打印出来的都是0。子线程获取的值会随着主线程的改变而变化!

2.2.2 示例代码2

  定义一个全局变量, 主线程创建5个子线程,这5个子线程每个子线程对这个全局变量进行自增操作1000次。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>


int g_val = 1;
void* thread_fun(void *arg)
{
    int i = 0;
    for( ;i < 1000; i++ )
    {
       printf("g_val=%d\n", g_val++);
    }
}
  
int main()
{
   pthread_t id[5];

   int i = 0;
   for( ; i < 5; i++ )
   {
      pthread_create(&id[i], NULL, thread_fun, NULL);
   }

  for( i = 0;i < 5; i++ )
  {
     pthread_join(id[i], NULL);
  }

    exit(0);
 }

正常理解:打印的最后一个值为:5000.

运行结果如下:

多个线程并行运行,拿到的是同一个i,因此它们都对i 自增一次, 并且是同一个值,导致最后加不到5000.

深入理解:i++

      我们运行的是可执行程序,它是由一条条的二进制指令构成,然后进行编译、汇编、链接操作转换成机器码,当运行i++的操作时,首先计算i的地址,然后将数据从内存加载到CPU,存放到相关的寄存器,经过ALU计算,再写入到相关的寄存器,然后将寄存器中的值写回到内存,中间经历了好几个步骤,因此它不是原子操作!

      因此,当一个线程对i++操作的过程中,还没有进行++操作,此时,又有另外一个进程来读取i,然后进行自增操作,那么这两个线程相当于就只进行了一次++操作,因此,总数达不到我们想要的结果!

        有个全局变量i值为1,如果有两个线程都要i++,如果有一个cpu,那就只能一个线程i++,另外一个线程i++;如果有两个cpu,那就可能两个线程同时对相同的i值进行++,那么加完的值就相同,本来加五次i值为10,有两个cpu呢,结果就可能小于等于10了,有可能俩个线程同时++。

如何解决呢?

方式1:切换为单核处理器,那么同一时刻就只有一个线程进行自增,这样就能保证能够加到5000.

方式2:一个线程在进行自增操作的时候,另一个线程先等着,不要进行任何操作,等这个线程执行完,再进行自增操作,这就是下面要讲的线程同步。

三、线程同步

3.1 线程同步的概念

       线程同步指的是当一个线程在对某个临界资源进行操作时,其他线程都不可以对这个资源进行操作,直到该线程完成操作,其他线程才能操作,也就是协同步调,让线程按预定的先后次序进行运行。线程同步的方法有四种:互斥锁、信号量、条件变量、读写锁。(面试题)

  • 锁(Locks): 锁是一个机制,用于控制对共享资源的访问。常见的锁包括互斥锁(Mutex)和读写锁(Read-Write Lock)。只有获得锁的线程才能进入临界区,其他线程必须等待锁被释放。

  • 互斥锁(Mutex): 互斥锁是最常见的同步机制,它确保同一时间只有一个线程可以访问共享资源。当一个线程获得互斥锁后,其他线程必须等待直到该锁被释放。

  • 信号量(Semaphore): 信号量是一个计数器,用于控制对共享资源的访问。信号量允许多个线程同时访问一定数量的共享资源。信号量分为计数信号量和二进制信号量(类似于互斥锁)。

  • 条件变量(Condition Variable): 条件变量允许线程在某个条件不满足时释放锁并进入等待状态。当条件满足时,其他线程可以通知等待的线程继续执行。条件变量通常与互斥锁结合使用。

  • 读写锁(Read-Write Lock): 读写锁允许多个线程同时读取共享资源,但在写操作时必须独占锁。读写锁分为读锁和写锁,读锁可以被多个线程同时持有,而写锁只能被一个线程持有。

3.2 信号量

       利用信号量进行线程的同步控制比起利用信号量进行进程间通信简单多了,封装好的函数接口,直接引入头文件,调用函数即可。

  需要引入的头文件  #include <semaphore.h>

3.2.1 函数接口介绍

3.2.1.1  信号量初始化函数
int sem_init(sem_t *sem, int pshared, unsigned int value);
初始化信号量。

参数说明:

  • sem: 指向信号量对象的指针。
  • pshared: 指定信号量是否在进程间共享。若为0,表示线程间共享;若非0,表示进程间共享。
  • value: 信号量的初始值。
3.2.1.2 等待信号量(P操作) 
int sem_wait(sem_t *sem);

      等待信号量,如果信号量的值大于0,则减1并立即返回;如果信号量的值为0,则阻塞直到信号量的值大于0。 sem: 指向信号量对象的指针。

3.2.1.3 释放信号量(V操作)
int sem_post(sem_t *sem);

释放信号量,即增加信号量的值。如果有线程正在等待信号量,它将被唤醒。sem: 指向信号量对象的指针.

3.2.1.4 销毁信号量
int sem_destroy(sem_t *sem);

销毁信号量。sem: 指向信号量对象的指针.

3.2.2 使用示例

       每个线程进行自增之前,先进行P操作,获取信号量,使用该进程资源,信号量减为0,此时其他线程P操作不会成功,就会阻塞住,等使用完,再进行V操作,释放信号量,信号量加1.为其他线程使用做准备。这样保证同一时刻,只有一个线程访问这块进程资源,进行自增操作!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>


sem_t sem;   //定义一个信号量

int g_val = 1;
void* thread_fun(void *arg)
{
    int i = 0;
    for( ;i < 1000; i++ )
    {
       sem_wait(&sem);                  //P操作,获取信号量
       printf("g_val=%d\n", g_val++);   //临界区
       sem_post(&sem);                  //V操作,释放信号量
    }
}
  
int main()
{
   pthread_t id[5];

   sem_init(&sem,0,1);                //对信号量进行初始化
   int i = 0;
   for( ; i < 5; i++ )
   {
      pthread_create(&id[i], NULL, thread_fun, NULL);
   }

  for( i = 0;i < 5; i++ )
  {
     pthread_join(id[i], NULL);
  }

   destroy(&sem);                        //销毁信号量
    exit(0);
 }

3.2.3  面试题

       题目描述:编写一个程序,开启三个线程,这三个线程按照顺序依次打印ABC,每个字母打印5次后结束,最后结果如 ABCABCABC… 依次递推.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>

//定义3个信号量
sem_t sema;
sem_t semb;
sem_t semc;


void*funa(void*arg)
{
    for(int i=0;i<5;i++)
    {
        sem_wait(&sema);   //ps1
        printf("A");
        fflush(stdout);
        sem_post(&semb);   //vs2
        sleep(1);
    }
}
void*funb(void*arg)
{
    for(int i=0;i<5;i++)
    {
        sem_wait(&semb);   //ps2
        printf("B");
        fflush(stdout);
        sem_post("c");     //vs3
        sleep(1);
}
 void*func(void*arg)
 {
      for(int i=0;i<5;i++)
    {
        sem_wait(&semc);   //ps3
        printf("A");
        fflush(stdout);
        sem_post(&sema);   //vs1
        sleep(1);  
 }
int main
{
    pthread id1,id2,id3;
    sem_init(&sema,0,1);
    sem_init(&semb,0,0);
    sem_init(&semc,0,0);
    
    pthread_create(&id1,NULL,funa,NULL);
    pthread_create(&id2,NULL,funb,NULL);
    pthread_create(&id3,NULL,func,NULL);
    pthread_join(id1,NULL);
    pthread_join(id2,NULL);
    pthread_join(id3,NULL);
    
    sem_destroy(&sema);
    sem_destroy(&semb);
    sem_destroy(&semb);
    exit(0);
}

3.3 互斥锁

       利用互斥锁进行线程的同步控制其实和利用信号量进行线程同步思想一样,加锁就相当于P操作,解锁就相当于V操作,试想生活中的试衣间,我们在使用试衣间的时候,进去之后会上锁,这样外面的人就无法使用,使用完后,出来,然后打开锁,这样外面的人就可以使用了,这样就可以保证同一时刻,只有一个线程在使用这个资源,如果资源被加锁,其他的线程再访问这块资源,就会被阻塞住!!其实,互斥锁就是信号量值为1的信号量。同样,它也有直接封装好的函数接口,直接引入头文件,调用函数即可。

需要引入的头文件   #include <pthread.h>

3.3.1 函数接口介绍

3.3.1.1 互斥锁初始化函数
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 功能:初始化互斥锁。
  • 参数:
    • mutex:指向要初始化的互斥锁的指针。
    • attr:指向互斥锁属性的指针,通常设置为NULL以使用默认属性。
  • 返回值:成功返回0,失败返回错误码。
3.3.1.2  加互斥锁函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 功能:锁定互斥锁,如果互斥锁已经被其他线程锁定,则阻塞当前线程。
  • 参数:
    • mutex:指向要锁定的互斥锁的指针。
  • 返回值:成功返回0,失败返回错误码。
3.3.1.3  解互斥锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 功能:解锁互斥锁。
  • 参数:
    • mutex:指向要解锁的互斥锁的指针。
  • 返回值:成功返回0,失败返回错误码。
3.3.1.4  销毁互斥锁函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 功能:销毁互斥锁。
  • 参数:
    • mutex:指向要销毁的互斥锁的指针。
  • 返回值:成功返回0,失败返回错误码。

3.3.2 使用示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>


pthread_mutex_t mutex;                    //定义一个锁变量

int g_val = 1;

void* thread_fun(void *arg)
{
    int i = 0;
    for( ;i < 1000; i++ )
    {
       pthread_mutex_lock(&mutex);            // 加互斥锁                                        
       printf("g_val=%d\n", g_val++);         //临界区
       pthread_mutex_unlock(&mutex);         // 解互斥锁                                  
    }
}
  
int main()
{
   pthread_t id[5];
                  
                                                 
   pthread_mutex_init(&mutex, NULL);       // 初始化互斥锁
                  
   int i = 0;
   for( ; i < 5; i++ )
   {
      pthread_create(&id[i], NULL, thread_fun, NULL);
   }

  for( i = 0;i < 5; i++ )
  {
     pthread_join(id[i], NULL);
  }

   
    pthread_mutex_destroy(&mutex);          // 销毁互斥锁                                 
    exit(0);
 }

3.4 条件变量

       在Linux系统中,利用条件变量(condition variables)可以实现线程间的同步。条件变量通常与互斥锁(mutex)一起使用,以确保线程安全。条件变量允许一个或多个线程在特定条件不满足时阻塞(等待),直到条件满足时被唤醒。同样,它也有直接封装好的函数接口,直接引入头文件,调用函数即可。

       线程在条件变量队列上等待被唤醒,如何唤醒是由我们来决定的,在条件变量队列上我们可以唤醒一个线程,也可以唤醒全部线程。

需要引入的头文件   #include <pthread.h>

3.4.1 函数接口介绍

3.4.1.1 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • cond: 指向条件变量的指针。
  • attr: 指向条件变量属性对象的指针,通常传递NULL使用默认属性。
3.4.1.2 唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);

 cond: 指向条件变量的指针。该函数会唤醒一个等待在此条件变量(消息队列)上的线程。如果有多个线程等待,则唤醒其中一个线程。

3.4.1.3 唤醒所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);

 cond: 指向条件变量的指针。该函数会唤醒所有等待在此条件变量(消息队列)上的线程。

3.4.1.4 等待条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • cond: 指向条件变量的指针。
  • mutex: 指向已锁定的互斥锁的指针。在调用此函数时,互斥锁会被解锁,然后线程进入等待状态。一旦线程被唤醒,互斥锁会被重新锁定。
3.4.1.5  销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

 cond: 指向条件变量的指针。

3.4.2 使用示例

        创建两个子线程,将其放在条件变量队列中,当我们从键盘输入数据,唤醒一个线程来打印数据,也可以唤醒这两个线程打印。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>



pthread_mutex_t mutex;   //定义互斥锁
pthread_cond_t cond;     //定义条件变量队列

void*funa(void*arg)
{
    char*s=(char*)arg;
    while(1)
    {
        //等待条件变量可用 
    	//条件变量
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//添加到条件变量的等待队列--阻塞--唤醒->解除阻塞
        pthread_mutex_unlock(&mutex);
        if(strncmp(s,"end",3)==0)
        {
            break;
        }
        printf("funa s=%s\n",s);
    }  
}

void*funb(void*)
{
     char*s=(char*)arg;
    while(1)
    {
        //等待条件变量可用 
    	//条件变量
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//添加到条件变量的等待队列--阻塞--唤醒->解除阻塞
        pthread_mutex_unlock(&mutex);
        if(strncmp(s,"end",3)==0)
        {
            break;
        }
        printf("funa s=%s\n",s);
    }  
}

int main()
{
    pthread_mutex_init(&mutex,NULL);    //初始化互斥锁
    pthread_cond_init(&cond,NULL);     //初始化条件变量队列
    
    pthread_t id1,id2;
    pthread_create(&id1,NULL,funa,buff);   //创建两个线程
    pthread_create(&id2,NULL,funb,buff);
    
    while(1)
    {
        fgets(buff,128,stdin);
        if(strncmp(buff,"end",3)==0)
        {
            pthread_cond_broadcast(&cond);  //唤醒所有线程,运行完退出
            break;
        }
        else
        {
            pthread_cond_signal(&cond);  //唤醒单个线程
        }
    }
    
    pthread_join(id1,NULL);
    pthread_join(id2,NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    exit(0);
}

为什么要加锁?

     因为要唤醒时,有的线程要加入队列,有的要出队列,这些线程不确定,我们到底算不算他们。这一步进行了两次对锁的操作,先解锁;如果被唤醒时,要先加上锁,出队列,保证同一时刻只有我一个人在出队列,一个线程操作队列。所以出队列,之后还要再解一次锁。锁变量传到方法内部,内部会进行解锁,再加锁操作。只有在队列中有线程等待唤醒才能把线程唤醒,要是对列中没有线程等待,白唤醒,没人理。

3.5 读写锁

        读写锁(也称为共享-独占锁)是一种同步机制,它允许多个线程同时读取共享资源,但在写入共享资源时,只有一个线程能够进行操作,且在写入期间不允许其他线程读取。读写锁针对于,俩个人一起读文件可以,但加了读锁了,就不能进行写入;加了写锁了,就不能读了;加了写锁,另一个人在想写,也是不能通过 这种机制适用于读多写少的场景,可以提高并发性能。封装好的函数接口,直接引入头文件,调用函数即可。

需要引入的头文件   #include <pthread.h>

3.5.1 函数接口介绍

3.5.1.1 读写锁初始化
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
  • rwlock:指向读写锁变量的指针。
  • attr:指向读写锁属性对象的指针,可以为NULL,表示使用默认属性。
  • 返回值:成功返回0,失败返回错误码。
3.5.1.2 获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • rwlock:指向读写锁变量的指针。
  • 返回值:成功返回0,失败返回错误码。
3.5.1.3 获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • rwlock:指向读写锁变量的指针。
  • 返回值:成功返回0,失败返回错误码。
3.5.1.4 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • rwlock:指向读写锁变量的指针。
  • 返回值:成功返回0,失败返回错误码。
3.5.1.5 释放锁(读锁或写锁)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • rwlock:指向读写锁变量的指针。
  • 返回值:成功返回0,失败返回错误码。

3.5.2 使用示例

       主线程创建三个线程,两个线程用来读,另外一个线程用来写,我们可以知道:两个读线程可以同时运行,但是读和写不能交错运行。


四、线程安全

 4.1 概念

          线程安全即就是在多线程运行的时候,不论线程的调度顺序怎样,最终的结果都是 一样的、正确的。那么就说这些线程是安全的。

4.2 多线程环境如何保证线程安全

    要保证线程安全需要做到:

  1. 线程同步,保证同一时刻只有一个线程访问临界资源
  2. 在多线程中使用线程安全的函数(可重入函数),所谓线程安全的函数指的是:如果一个 函数能被多个线程同时调用且不发生竟态条件,则我们程它是线程安全的。

       函数以前使用不会出现问题,但在多线程环境下,出现问题,有可能是函数本身导致的,由此我们要使用此类函数的线程安全版本。多线程环境中,库函数使用线程安全版本。

4.3 举例理解

          两个进程分别调用strtok()对字符串进行分割,并发运行。

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* PthreadFun(void *arg)
{
    char buff[] = "a b c d e f g h i";

    char *p = strtok(buff, " ");
    while(p != NULL)
    {
       printf("fun:: %c\n", *p);
       p = strtok(NULL, " ");  //这里为NULL,就会去寻找ptr指针指向的位置
       sleep(1);
    }
}

int main()
{
     pthread_t id;
     int res = pthread_create(&id, NULL, PthreadFun, NULL);
     assert(res == 0);

     char buff[] = "1 2 3 4 5 6 7 8 9";

     char *p = strtok(buff, " ");

     while(p != NULL)
     {
         printf("main:: %c\n", *p);
         p = strtok(NULL, " ");
         sleep(1);
     }
 }

不使用线程安全版本的strtok函数,打出结果是主线程和子线程分割同一个字符串。因为strtok里有静态指针,它记录着分割字符串的位置,再次使用strtok时传入新的字符串地址,就会更改原来静态指针记录的位置为新字符串,会导致俩个线程分割同一个字符串。修改代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void* PthreadFun(void *arg)
{
    char buff[] = "a b c d e f g h i";

    char *q = NULL;
    char *p = strtok_r(buff, " ", &q);
    while(p != NULL)
    {
       printf("fun:: %c\n", *p);
       p = strtok_r(NULL, " ", &q);
       sleep(1);
    }
}

int main()
{
    pthread_t id;
    int res = pthread_create(&id, NULL, PthreadFun, NULL);
    assert(res == 0);

    char buff[] = "1 2 3 4 5 6 7 8 9";

    char *q = NULL;
    char *p = strtok_r(buff, " ", &q);

    while(p != NULL)
    {
        printf("main:: %c\n", *p);
        p = strtok_r(NULL, " ", &q);
        sleep(1);
    }
 }

五、线程与fork

5.0 多线程的PID

上述代码子线程和父线程打印进程的PID,我们知道线程只是进程中的一条执行路径,不管是主线程还是子线程它们都属于同一个进程,因此,它们两个打印的结果应该相同。

 5.1 多线程中某个线程调用 fork(),子进程会有和父进程相同数量的线程吗?

5.1.1 主线程调用fork()产生子进程

运行结果如下:

5.1.2  子线程调用fork()产生子进程

运行结果如下: 

总结:多线程中某个线程调用 fork(),子进程会有和父进程线程数量不相同,并且在哪个线程调用fork(),则子进程就只会执行fork()所在的那个线程!!

5.2 父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁?

结果如下:

 程序中父进程经过延时确保其子线程处于加锁状态,然后才进行进程的复制,子进程未加锁成功!

总结:

  1. 在父进程中定义的锁,产生子进程后,子进程也会有拷贝的锁,但是父子进程各自使用各自的锁;
  2. 子进程锁的状态取决于fork()复制的那一刻锁的状态,因为锁在父进程中的状态是变化的。  ———————如何解决呢?

解决办法:在复制子进程之前先进行一次加锁操作,如果能成功,说明锁的状态是解锁状态,直接复制,然后在子进程解锁;如果失败,说明锁的状态是加锁状态,阻塞延时等待子线程释放锁,然后在复制子进程。

结果如下:

六、生产者消费者模型(面试题)

6.1 生产者消费者问题概述

        生产者/消费者问题,也被称作有限缓冲问题。可以描述为:两个或者更多的线程共享同一个缓冲 区,其中一个或多个线程作为“生产者”会不断地向缓冲区中添加数据,另一个或者多个线程作为“消费者” 从缓冲区中取走数据。

生产者/消费者模型关注的是以下几点:

  1. 生产者和消费者必须互斥的使用缓冲区
  2. 缓冲区空时,消费者不能读取数据
  3. 缓冲区满时,生产者不能添加数据

6.2 生产者消费者模型优点

  1. 解耦:因为多了一个缓冲区,所以生产者和消费者并不直接相互调用,这样生产者和消费者的代码 发生变化,都不会对对方产生影响。这样其实就是把生产者和消费者之间的强耦合解开,变成了生 产者和缓冲区,消费者和缓冲区之间的弱耦合
  2. 支持并发:如果消费者直接从生产者拿数据,则消费者需要等待生产者生产数据,同样生产者需要 等待消费者消费数据。而有了生产者/消费者模型,生产者和消费者可以是两个独立的并发主体。 生产者把制造出来的数据添加到缓冲区,就可以再去生产下一个数据了。而消费者也是一样的,从 缓冲区中读取数据,不需要等待生产者。这样,生产者和消费者就可以并发的执行。
  3. 支持忙闲不均:如果消费者直接从生产者这里拿数据,而生产者生产数据很慢,消费者消费数据很 快,或者生产者生产数据很多,消费者消费数据很慢。都会造成占用CPU的时间片白白浪费。生产 者/消费者模型中,生产者只需要将生产的数据添加到缓冲区,缓冲区满了就不生产了。消费者从 缓冲区中读取数据,缓冲区空了就不消费了,使得生产者/消费者的处理能力达到一个动态的平 衡。

6.3 生产者消费者模型实现

       假定缓冲池中有N个缓冲区,一个缓冲区只能存储一个int类型的数据。定义互斥锁mutex实现对缓冲区的互斥访问;计数信号量dempty用来表示空闲缓冲区的数量,其初值为N;计数信号量dfull用来表示有数据的缓冲区的数量,其初值为0 。

  1. 生产者与消费者互斥,生产者与生产者之间互斥, 消费者与消费者之间互斥。
  2. 定义两个信号量,生产者的信号量大小跟缓冲区大小一样,消费者的信号量初始值为0
  3. 互斥锁应该加在p操作的后面,因为如果缓冲区满了,先拿到锁,不能写入数据,别人也拿不到锁,此时就成为死锁了,生产者与消费者数量可以不对等,可以为多个,他们俩的数量没有关系,消费者谁抢到锁谁就消费;

代码实现: 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
#include <pthread.h>
#include <semaphore.h>
#include <time.h>


#define BUFF_MAX 30
#define SC_NUM 2
#define XF_NUM 3


int in = 0;
int out = 0;
sem_t sem_empty;
sem_t sem_full;

pthread_mutex_t mutex;
int buff[BUFF_MAX] = {0};
void * sc_thread(void* arg)
{
     int index = (int)arg;
     while( 1 )
     {
         sem_wait(&sem_empty);         //ps1
         pthread_mutex_lock(&mutex);  //加锁
         buff[in] = rand()%100;
         printf("生产者%d 产生数据%d,in=%d\n",index,buff[in],in);
         in = (in + 1) % BUFF_MAX;     //更新下标
         pthread_mutex_unlock(&mutex);  //unlock
         sem_post(&sem_full);        //vs2
         int n = rand() % 10;
         sleep(n);
     }
}
void * xf_thread(void* arg)
{
     int index = (int)arg;
     while(1)
     {
       sem_wait(&sem_full);            //ps2
       pthread_mutex_lock(&mutex);     //加锁
       printf("消费者%d 消费数据%d, out=%d\n",index,buff[out],out);
       out = (out+1) % BUFF_MAX;       //更新下标
       pthread_mutex_unlock(&mutex);   //解锁
       sem_post(&sem_empty);           //vs1
       int n = rand() % 10;
       sleep(n);
     }
}
int main()
{
       pthread_mutex_init(&mutex,NULL);
       sem_init(&sem_empty,0,BUFF_MAX);
       sem_init(&sem_full,0,0);
       srand((int)time(NULL));
       pthread_t sc_id[SC_NUM];
       pthread_t xf_id[XF_NUM];
       int i = 0;
       for( ; i < SC_NUM; i++ )
       {
           pthread_create(&sc_id[i],NULL,sc_thread,(void*)i);
       }
       for( i = 0; i < XF_NUM; i++ )
       {
           pthread_create(&xf_id[i],NULL,xf_thread,(void*)i);
       }
       for( i = 0; i < SC_NUM; i++ )
       {
           pthread_join(sc_id[i],NULL);
       }
       for( i = 0; i < XF_NUM; i++ )
       {
           pthread_join(xf_id[i],NULL);
       }
       sem_destroy(&sem_empty);
       sem_destroy(&sem_full);
       pthread_mutex_destroy(&mutex);
       exit(0);
}

 七、面试题总结

  1. 进程与线程的区别
  2. 线程同步的方法有哪些:信号量 互斥锁 读写锁 条件变量
  3. 线程安全:同步,使用线程安全的函数(可重入函数)
  4. 生产者消费者模型 ,给场景问,如何实现同步

        至此,线程已经讲解完毕!篇幅较长,慢慢消化,以上就是全部内容!请务必掌握,创作不易,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!

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

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

相关文章

修改west扩展命令的路径

west命令是zephyr工程中非常重要的工具。使用west命令&#xff0c;可以高效的创建工程&#xff0c;管理代码&#xff0c;此外&#xff0c;通过扩展命令&#xff0c;还可以支持编译&#xff0c;烧录等功能。 从下图中可以看出&#xff0c;extension commands from project mani…

线性代数|机器学习-P8矩阵低秩近似eckart-young

文章目录 1. SVD奇异值分解2. Eckart-Young2.1 范数 3. Q A Q U Σ V T QAQU\Sigma V^T QAQUΣVT4. 主成分分析图像表示 1. SVD奇异值分解 我们知道&#xff0c;对于任意矩阵A来说&#xff0c;我们可以将其通过SVD奇异值分解得到 A U Σ V T AU\Sigma V^T AUΣVT&#xff0…

[ue5]建模场景学习笔记(4)——必修内容可交互的地形,交互沙(1)

1.需求分析&#xff1a; 现在的沙漠场景仅仅只是一张贴图&#xff0c;人物走过不会留下任何痕迹&#xff0c;很不真实&#xff0c;尝试优化一下&#xff0c;做出可交互的沙漠效果。 2.操作实现&#xff1a; 1.思路&#xff1a;这是一个相对复杂的工程&#xff0c;要考虑玩家踩…

深入理解C++三五零法则

三五零法则就是三法则&#xff08;The Rule of Three&#xff09;、五法则&#xff08;The Rule of Five&#xff09;、零法则&#xff08;The Rule of Zero&#xff09;。三五零法则是和C的特殊成员函数有关&#xff0c;特别是那些涉及对象如何被创建、复制、移动和销毁的函数…

ESD防护SP3232E真+3.0V至+5.5V RS-232收发器

特征 采用3.0V至5.5V电源&#xff0c;符合真正的EIA/TIA-232-F标准 满载时最低 120Kbps 数据速率 1μA 低功耗关断&#xff0c;接收器处于活动状态 &#xff08;SP3222E&#xff09; 可与低至 2.7V 电源的 RS-232 互操作 增强的ESD规格&#xff1a; 15kV人体模型 15kV IEC1000…

Java Web学习笔记17——Vue快速入门

什么是Vue&#xff1f; Vue是一套前端框架&#xff0c;免除原生JavaScript中的DOM操作&#xff0c;简化书写。 基于MVVM&#xff08;Model-View-ViewModel&#xff09;思想&#xff0c;实现数据的双向绑定&#xff0c;将编程的关注点放在数据上。 官网&#xff1a;https://v…

概率分析和随机算法

目录 雇佣问题 概率分析 随机算法 生日悖论 随机算法 概率分析 球与箱子 总结 雇佣问题 有n个候选人面试&#xff0c;如果面试者比目前雇佣者的分数高&#xff0c;评价更好&#xff0c;那么就辞掉当前雇佣者&#xff0c;而去聘用面试者&#xff0c;否则继续面试新的候…

区块链简要介绍及运用的技术

一、区块链的由来 区块链概念最早是从比特币衍生出来的。 比特币&#xff08;Bitcoin&#xff09;诞生于2008年&#xff0c;是由一个名叫中本聪&#xff08;Satoshi Nakamoto&#xff09;的人首次提出&#xff0c;这个人非常神秘&#xff0c;至今没有他的任何准确信息。在提出…

三、【源码】Mapper XML的解析和注册使用

源码地址&#xff1a;https://github.com/mybatis/mybatis-3/ 仓库地址&#xff1a;https://gitcode.net/qq_42665745/mybatis/-/tree/03-parse-mapperXML Mapper XML的解析和注册使用 流程&#xff1a; 1.Resources加载MyBatis配置文件生成Reader字符流 2.SqlSessionFact…

Activity->Activity中动态添加Fragment->add和replace方式添加的区别

XML文件 Activity布局文件R.layout.activity_main <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:id"id/root_ll"android:orientation"v…

一个简单好用的 C# Animation Easing 缓动动画类库

文章目录 1. 类库说明2.使用步骤2.1 创建一个Windows Form 项目2.2 安装类库2.3 编码效果3.代码下载1. 类库说明 App.Animations 类库是一个很精炼、好用的 csharp easing 动画库 基于 net-standard 2.0提供 Fluent API,写代码非常舒服。支持多个参数同时参与动画。自带了十几…

Flutter基础 -- Flutter常用组件

目录 1. 文本组件 Text 1.1 基础用法 1.2 Text 定义 1.3 Text 示例 1.4 Text.rich、RichText 、TextSpan 1.5 RichText 示例 2. 导入资源 2.1 加入资源 2.2 加入图片 3. 图片组件 image 3.1 colorBlendMode 混合参数 3.2 fit 图片大小适配 3.3 ImageProvider 图片…

【Python报错】已解决NameError: name ‘xxx‘ is not defined

解决Python报错&#xff1a;NameError: name ‘xxx’ is not defined 在Python编程中&#xff0c;NameError是一个非常常见的错误类型&#xff0c;它发生在你尝试访问一个未被定义的变量时。本文将介绍这种错误的原因&#xff0c;以及如何通过具体的代码示例来解决这个问题。 …

深度学习笔记: 最详尽LinkedIn Feed 排名系统设计

欢迎收藏Star我的Machine Learning Blog:https://github.com/purepisces/Wenqing-Machine_Learning_Blog。如果收藏star, 有问题可以随时与我交流, 谢谢大家&#xff01; LinkedIn Feed 排名 1. 问题陈述 设计一个个性化的LinkedIn Feed&#xff0c;以最大化用户的长期参与度…

【MMU】——ARM 一级页表

文章目录 一级页表项即 entry 的格式如下 从上图可以看出 L1 页表项有四种可能类型 产生中止异常的故障条目。这可能是预取或数据中止、取决于访问类型。这实际上表示虚拟地址未映射 bit[1:0] = 00指向 L2 转换表的条目。这样就能将 1MB 的内存分页 bit[1:0] = 01。1MB 段转换…

问题:8255A的端口A工作在方式2时,使用端口C的______作为与CPU和外部设备的联络信号。 #媒体#经验分享#其他

问题&#xff1a;8255A的端口A工作在方式2时&#xff0c;使用端口C的______作为与CPU和外部设备的联络信号。 参考答案如图所示

《互联网政务应用安全管理规定》电子邮件安全如何整改?

继上篇文章&#xff08;解读《互联网政务应用安全管理规定》网络和数据安全中的身份认证和审计合规&#xff09;之后&#xff0c;本篇文章继续解读第五章“电子邮件安全”&#xff0c;为党政机关事业单位提供电子邮件系统整改思路。 “电子邮件安全”内容从第三十一条到第三十…

vscode 离线下载指定版本插件和安装方法

1、背景 由于不同的vscode版本需要安装对应的插件版本&#xff0c;一般情况下&#xff0c;vscode版本会落后于vscode插件库提供的可以下载的插件版本&#xff0c;网页一般只会提供最新的插件下载版本&#xff0c;因此我们需要下载指定的版本需要采取一些措施。 2、获取需要安…

基于Python的实验室管理系统的设计与实现(论文+源码)_kaic

摘 要 随着实验室设备越来越多&#xff0c;实验室及其设备管理工作变得越来越繁重&#xff0c;还存在些管理模式仍旧处于手工管理模式和一些抢占实验室的不文明现象&#xff0c;传统的手工模式已经满足不了日益增长的管理需求&#xff0c;而本系统摒弃传统模式&#xff0c;开启…

移动端 UI 风格,魅力无限

移动端 UI 风格&#xff0c;打造极致体验