Linux高并发服务器之Linux多线程开发

news2024/11/15 22:40:52

Linux高并发服务器之Linux多线程开发

  • 一、线程概述
  • 二、线程操作相关函数
    • 1、创建线程
    • 2、线程终止
    • 3、线程连接
    • 4、线程分离
    • 5、线程取消
    • 6、线程属性
  • 三、线程同步
    • 1、多线程卖票案例
    • 2、互斥锁解决卖票问题
    • 3、读写锁优化卖票问题
    • 4、生产者消费者模型
    • 5、条件变量解决生产者消费者问题
    • 6、信号量解决生产者消费者问题
  • 四、死锁问题
    • 1、死锁问题的概述
    • 2、死锁问题的处理
      • 死锁的预防
      • 死锁的避免
      • 死锁的检测与恢复

本文源自C++高薪面试项目的学习笔记,主要记录Liunx多线程的学习,主要知识点是线程概述等基础概念以外,还有线程相关Liunx系统函数以及对应练手代码,除此之外还有线程同步问题的讲解以及实战多线程买票问题及解决,并且使用互斥锁读写锁等方法解决的生产者和消费者模型,最后就是有关死锁问题的概念,以及采用银行家算法等对死锁问题的处理

一、线程概述

与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)

  • 进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。
  • 线程是轻量级的进程(LWP:Light Weight Process),在 Linux 环境下线程的本质仍是进程。
  • 查看指定进程的 LWP 号:ps –Lf pid
  • man找不到有关rwlock等函数可以sudo apt-get install manpages-posix-dev

进程和线程的区别:
◼ 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用
一些进程间通信方式,在进程间进行信息交换。
◼ 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如
内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销
依然不菲。
◼ 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。
◼ 创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采
用写时复制来复制内存,也无需复制页表

进程和线程虚拟地址空间:
在这里插入图片描述
线程之间共享和非共享的资源:
在这里插入图片描述
Liunx线程库–NPTL
◼ 当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone()
系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的
一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用
来完成在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处
理、调度和进程间同步等方面都存在问题。另外,这个线程模型也不符合 POSIX 的要求。
◼ 要改进 LinuxThreads,需要内核的支持,并且重写线程库。有两个相互竞争的项目开始
来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPT(Next-Generation
POSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT
在 2003 年中期被放弃了,把这个领域完全留给了 NPTL。
◼ NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它
克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相
比,它在性能和稳定性方面都提供了重大的改进。
◼ 查看当前 pthread 库版本:getconf GNU_LIBPTHREAD_VERSION

二、线程操作相关函数

查看线程相关所有函数:man -k pthread 或者man pthread加上两次tap键

注意编译要加上 -pthread:gcc test.c -o app -pthread

相关函数如下:
◼ int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg); // 创建子线程
◼ pthread_t pthread_self(void); //获取当前线程id
◼ int pthread_equal(pthread_t t1, pthread_t t2); //比较两线程id是否相等
◼ void pthread_exit(void *retval);//退出当前线程
◼ int pthread_join(pthread_t thread, void **retval);//连接已终止的子线程回收资源
◼ int pthread_detach(pthread_t thread); //分离线程,进程结束时系统回收其资源
◼ int pthread_cancel(pthread_t thread); //当线程执行到取消点时终止线程

以上函数逐个介绍并附实操代码如下(多数为老师课上敲的正确代码,如有错漏欢迎指出):

1、创建线程

man pthread_create 查看得以下信息:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
void *(*start_routine) (void *), void *arg);
    - 功能:创建一个子线程
    - 参数:
        - thread: 传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
        - attr : 设置线程的属性,一般使用默认值,NULL
        - start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
        - arg : 给第三个参数使用,传参
    - 返回值:
        成功:0
        失败:返回错误号。这个错误号和之前errno不太一样。
        获取错误号的信息:  char * strerror(int errnum);

实操调用:

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

void *callback(void * arg){
    int num = *(int *)arg;//需要先将void* 转化为int* 然后再解引用
    while(num--){
        sleep(1);
        printf("child_pthread : %d\n", num);
    }
    return NULL;
}

int main(){
    pthread_t pid;
    int num = 999;
    int ret = pthread_create(&pid, NULL, callback, (void *)&num);//创建一个子线程
    if (ret != 0){      //返回值非0,返回错误信息
        char *errstr = strerror(ret);
        printf("error: %s\n",errstr);
    }

    for(int i = 999; i > 0; i++) {
        sleep(1);       //防止子线程没时间抢占cpu运行
        printf("mian_pthread : %d\n", i);
    }
    return 0;//相当于 exit(0) 进程结束
}

2、线程终止

man pthread_exit 、 man pthread_self 、 man pthread_equal 得出以下信息:

#include <pthread.h>
void pthread_exit(void *retval);
    功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程
    参数:
        retval:需要传递一个指针,作为一个返回值,可以在pthread_join()中获取到。

pthread_t pthread_self(void);
    功能:获取当前的线程的线程ID

int pthread_equal(pthread_t t1, pthread_t t2);
    功能:比较两个线程ID是否相等
    不同的操作系统,pthread_t类型的实现不一样,有的是无符号的长整型,有的
    是使用结构体去实现的。

实操调用:

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

void * callback(void * arg) {
    printf("child thread id : %ld\n", pthread_self());
    return NULL;    // 相当于 pthread_exit(NULL);
} 

int main() {  
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);// 创建一个子线程
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error : %s\n", errstr);
    }

    // 主线程
    for(int i = 0; i < 9; i++) {
        printf("%d\n", i);
    }

    printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());

    // 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。
    pthread_exit(NULL);

    printf("main thread exit\n");
	
	//当所有子线程都退出后,主进程结束,系统回收所有资源
    return 0;   // exit(0);
}

3、线程连接

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
    - 功能:和一个已经终止的线程进行连接
            回收子线程的资源
            这个函数是阻塞函数,调用一次只能回收一个子线程
            一般在主线程中使用
    - 参数:
        - thread:需要回收的子线程的ID
        - retval: 接收子线程退出时的返回值
    - 返回值:
        0 : 成功
        非0 : 失败,返回的错误号

实操演示:

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

int value = 10;

void * callback(void * arg) {
    printf("child thread id : %ld\n", pthread_self());
    // sleep(3);
    // return NULL; 
    // int value = 10; // 局部变量
    pthread_exit((void *)&value);   // return (void *)&value;
} 

int main() {

    // 创建一个子线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, callback, NULL);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error : %s\n", errstr);
    }

    // 主线程
    for(int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }

    printf("tid : %ld, main thread id : %ld\n", tid ,pthread_self());

    // 主线程调用pthread_join()回收子线程的资源
    int * thread_retval;
    ret = pthread_join(tid, (void **)&thread_retval);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error : %s\n", errstr);
    }

    printf("exit data : %d\n", *thread_retval);
    printf("回收子线程资源成功!\n");

    // 让主线程退出,当主线程退出时,不会影响其他正常运行的线程。
    pthread_exit(NULL);
    return 0; 
}

4、线程分离

/*
    #include <pthread.h>
    int pthread_detach(pthread_t thread);
        - 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
          1.不能多次分离,会产生不可预料的行为。
          2.不能去连接一个已经分离的线程,会报错。
        - 参数:需要分离的线程的ID
        - 返回值:
            成功:0
            失败:返回错误号
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
    printf("chid thread id : %ld\n", pthread_self());
    return NULL;
}

int main() {

    // 创建一个子线程
    pthread_t tid;

    int ret = pthread_create(&tid, NULL, callback, NULL);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error1 : %s\n", errstr);
    }

    // 输出主线程和子线程的id
    printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());

    // 设置子线程分离,子线程分离后,子线程结束时对应的资源就不需要主线程释放
    ret = pthread_detach(tid);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error2 : %s\n", errstr);
    }

    pthread_exit(NULL);

    return 0;
}

5、线程取消

/*
    #include <pthread.h>
    int pthread_cancel(pthread_t thread);
        - 功能:取消线程(让线程终止)
            取消某个线程,可以终止某个线程的运行,
            但是并不是立马终止,而是当子线程执行到一个取消点,线程才会终止。
            取消点:系统规定好的一些系统调用,我们可以粗略的理解为从用户区到内核区的切换,这个位置称之为取消点。
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
    printf("chid thread id : %ld\n", pthread_self());
    for(int i = 0; i < 5; i++) {
        printf("child : %d\n", i);
    }
    return NULL;
}

int main() {
    // 创建一个子线程
    pthread_t tid;

    int ret = pthread_create(&tid, NULL, callback, NULL);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error1 : %s\n", errstr);
    }

    // 取消线程
    pthread_cancel(tid);
    for(int i = 0; i < 5; i++) {
        printf("%d\n", i);
    }

    // 输出主线程和子线程的id
    printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());
    pthread_exit(NULL);

    return 0;
}

6、线程属性

man -k pthread_attr_ :查看所有线程属性函数,或者man pthread_attr_加上两次tap键

线程属性类型 pthread_attr_t

  • int pthread_attr_init(pthread_attr_t *attr); // 初始化线程属性变量
  • int pthread_attr_destroy(pthread_attr_t *attr); //释放线程属性的资源
  • int pthread_attr_getdetachstate(const pthread_attr_t attr, intdetachstate);//获取线程分离的状态属性
  • int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);//设置线程分离的状态属性
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void * callback(void * arg) {
    printf("chid thread id : %ld\n", pthread_self());
    return NULL;
}

int main() {

    // 创建一个线程属性变量
    pthread_attr_t attr;
    // 初始化属性变量
    pthread_attr_init(&attr);

    // 设置属性
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // 创建一个子线程
    pthread_t tid;

    int ret = pthread_create(&tid, &attr, callback, NULL);
    if(ret != 0) {
        char * errstr = strerror(ret);
        printf("error1 : %s\n", errstr);
    }

    // 获取线程的栈的大小
    size_t size;
    pthread_attr_getstacksize(&attr, &size);
    printf("thread stack size : %ld\n", size);

    // 输出主线程和子线程的id
    printf("tid : %ld, main thread id : %ld\n", tid, pthread_self());

    // 释放线程属性资源
    pthread_attr_destroy(&attr);

    pthread_exit(NULL);

    return 0;
}

Linux默认为每个线程分配了足够的堆栈空间(一般是8MB = 8388608 B),可以用ulimit -s 命令查看或修改这个默认值;

三、线程同步

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进
行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处
于等待状态。

线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。
临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应中断该片段的执行。

1、多线程卖票案例

使用多线程实现买票的案例。有3个窗口,一共是100张票。

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

// 全局变量,所有的线程都共享这一份资源。
int tickets = 100;

void * sellticket(void * arg) {
    // 卖票
    while(tickets > 0) {
        usleep(6000);
        printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
        tickets--;
    }
    return NULL;
}

int main() {

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    pthread_exit(NULL); // 退出主线程

    return 0;
}

在这里插入图片描述
执行以上代码会发现一个问题,一张门票似乎被买了三次?这是因为在当全局变量为100的时候同时被多个子线程读取后造成的问题,解决这个问题有很多方法,比如互斥量

2、互斥锁解决卖票问题

◼ 为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。
◼ 互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。
◼ 一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议:

  • 针对共享资源锁定互斥量
  • 访问共享资源
  • 对互斥量解锁

在这里插入图片描述

如果在man操作手册中找不到有关mutex,可以尝试命令
sudo apt-get install manpages-posix-dev安装额外帮助包即可
然后 man pthread_mutex_再连续按tap两下即可
互斥量相关函数:
互斥量的类型 pthread_mutex_t

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    - 初始化互斥量
    - 参数 :
        - mutex : 需要初始化的互斥量变量
        - attr : 互斥量相关的属性,NULL
    - restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
        pthread_mutex_t *restrict mutex = xxx;
        pthread_mutex_t * mutex1 = mutex;

int pthread_mutex_destroy(pthread_mutex_t *mutex);
    - 释放互斥量的资源

int pthread_mutex_lock(pthread_mutex_t *mutex);
    - 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待

int pthread_mutex_trylock(pthread_mutex_t *mutex);
    - 尝试加锁,如果加锁失败,不会阻塞,会直接返回。

int pthread_mutex_unlock(pthread_mutex_t *mutex);
    - 解锁

使用互斥锁实操解决卖票问题:

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

// 全局变量,所有的线程都共享这一份资源。
int tickets = 1000;

// 创建一个互斥量
pthread_mutex_t mutex;

void * sellticket(void * arg) {
    // 卖票
    while(1) {
        // 加锁
        pthread_mutex_lock(&mutex);

        if(tickets > 0) {
            usleep(6000);
            printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets);
            tickets--;
        }else {
            // 解锁
            pthread_mutex_unlock(&mutex);
            break;
        }
        
        // 解锁
        pthread_mutex_unlock(&mutex);
    }

    

    return NULL;
}

int main() {

    // 初始化互斥量
    pthread_mutex_init(&mutex, NULL);

    // 创建3个子线程
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, NULL, sellticket, NULL);
    pthread_create(&tid2, NULL, sellticket, NULL);
    pthread_create(&tid3, NULL, sellticket, NULL);

    // 回收子线程的资源,阻塞
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    pthread_exit(NULL); // 退出主线程

    // 释放互斥量资源
    pthread_mutex_destroy(&mutex);

    return 0;
}

运行以上代码,卖票正常效果如下:
在这里插入图片描述

3、读写锁优化卖票问题

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

比如在上述卖票问题中,假设多个窗口的客人都只是查询余票的,也就是只读,如果这个时候如果是使用的互斥锁,那么这些线程都得等着锁的释放,但是这可以优化一下采用读写锁,当一个线程来访问时,如果是读锁且自己也是读操作就可以访问数据,但是不能修改数据,而如果是写锁,那么就只能等待写锁释放;

在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:

  • 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
  • 如果有其它线程写数据,则其它线程都不允许读、写操作。
  • 写是独占的,写的优先级高。

如果在man操作手册中找不到有关rwlock,可以尝试命令
sudo apt-get install manpages-posix-dev安装额外帮助包即可
然后 man pthread_rwlock_再连续按tap两下即可

读写锁的类型 pthread_rwlock_t
◼ int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
◼ int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

/*
    案例:6个线程操作同一个全局变量。
    3个线程不定时写这个全局变量,3个线程不定时的读这个全局变量
*/
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 创建一个共享数据
int tickets = 100;
pthread_rwlock_t rwlock;

void * writeTickets(void * arg) {
    while(1) {
        pthread_rwlock_wrlock(&rwlock);
        if (tickets > 0) 
            printf("%ld 售出 : %d 号票\n", pthread_self(), tickets--);
        else {
            pthread_rwlock_unlock(&rwlock);
            break;
        }
        pthread_rwlock_unlock(&rwlock);
        usleep(1000);
    }
    return NULL;
}

void * readTickets(void * arg) {
    while(1) {
        pthread_rwlock_rdlock(&rwlock);
        if (tickets > 0) 
            printf("%ld 查询余票为 : %d 张\n", pthread_self(), tickets);
        else {
            pthread_rwlock_unlock(&rwlock);
            break;
        }
        pthread_rwlock_unlock(&rwlock);
        usleep(1000);
    }

    return NULL;
}

int main() {
    pthread_rwlock_init(&rwlock, NULL);

    // 创建3个写线程,3个读线程
    pthread_t wtids[3], rtids[3];
    for(int i = 0; i < 3; i++) {
        pthread_create(&wtids[i], NULL, writeTickets, NULL);
    }

    for(int i = 0; i < 3; i++) {
        pthread_create(&rtids[i], NULL, readTickets, NULL);
    }

    // 连接已终止线程回收
    for(int i = 0; i < 3; i++) {
        pthread_join(wtids[i], NULL);
    }

    for(int i = 0; i < 3; i++) {
        pthread_join(rtids[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);
    pthread_exit(NULL);

    return 0;
}

效果如下,直到售出最后一张票程序终止:
在这里插入图片描述

4、生产者消费者模型

/*
    生产者消费者模型(粗略的版本)
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 创建一个互斥量
pthread_mutex_t mutex;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        usleep(100);
    }

    return NULL;
}

void * customer(void * arg) {

    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;

        // 判断是否有数据
        if(head != NULL) {
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {

    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);

    pthread_exit(NULL);

    return 0;
}

5、条件变量解决生产者消费者问题

条件变量的类型 pthread_cond_t

/*
    int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    int pthread_cond_destroy(pthread_cond_t *cond);
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
        - 等待,调用了该函数,线程会阻塞。
    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
        - 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
    int pthread_cond_signal(pthread_cond_t *cond);
        - 唤醒一个或者多个等待的线程
    int pthread_cond_broadcast(pthread_cond_t *cond);
        - 唤醒所有的等待的线程
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        
        // 只要生产了一个,就通知消费者消费
        pthread_cond_signal(&cond);

        pthread_mutex_unlock(&mutex);
        usleep(100);
    }

    return NULL;
}

void * customer(void * arg) {

    while(1) {
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        // 判断是否有数据
        if(head != NULL) {
            // 有数据
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        } else {
            // 没有数据,需要等待
            // 当这个函数调用阻塞的时候,会对互斥锁进行解锁,当不阻塞的,继续向下执行,会重新加锁。
            pthread_cond_wait(&cond, &mutex);
            pthread_mutex_unlock(&mutex);
        }
    }
    return  NULL;
}

int main() {

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    pthread_exit(NULL);

    return 0;
}

6、信号量解决生产者消费者问题

信号量的类型 sem_t

/*
    信号量的类型 sem_t
    int sem_init(sem_t *sem, int pshared, unsigned int value);
        - 初始化信号量
        - 参数:
            - sem : 信号量变量的地址
            - pshared : 0 用在线程间 ,非0 用在进程间
            - value : 信号量中的值

    int sem_destroy(sem_t *sem);
        - 释放资源

    int sem_wait(sem_t *sem);
        - 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞

    int sem_trywait(sem_t *sem);

    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    int sem_post(sem_t *sem);
        - 对信号量解锁,调用一次对信号量的值+1

    int sem_getvalue(sem_t *sem, int *sval);

    sem_t psem;
    sem_t csem;
    init(psem, 0, 8);
    init(csem, 0, 0);

    producer() {
        sem_wait(&psem);
        sem_post(&csem)
    }

    customer() {
        sem_wait(&csem);
        sem_post(&psem)
    }

*/

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

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        sem_wait(&psem);
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        sem_post(&csem);
    }

    return NULL;
}

void * customer(void * arg) {

    while(1) {
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        head = head->next;
        printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        sem_post(&psem);
       
    }
    return  NULL;
}

int main() {

    pthread_mutex_init(&mutex, NULL);
    sem_init(&psem, 0, 8);
    sem_init(&csem, 0, 0);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

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

    pthread_mutex_destroy(&mutex);
    pthread_exit(NULL);

    return 0;
}

四、死锁问题

1、死锁问题的概述

什么是死锁?

两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

死锁、饥饿、死循环的区别?

  • 死锁是各进程之间互相等待对方的资源,导致各进程都阻塞无法推进的现象;
  • 饥饿是某进程长期得不到需要的资源,进程无法推进
  • 死循环是进程执行过程中跳不出某个循环的现象,有时是程序逻辑bug有时是程序员故意设计;

区别:

  • 死锁、饥饿是操作系分配资源的问题,死循环是代理逻辑错误导致的;
  • 饥饿可能是阻塞态也可能是就绪态并且可能是一个或多个,而死锁至少是两个及以上进程同时死锁的,并且一定是阻塞态;

死锁产生的四个必要条件

  1. 循环等待:存在一种进程资源的循环等待链
  2. 互斥资源:对互斥的资源的争夺
  3. 不剥夺条件:即资源只能由进程主动释放,不能被其他进程夺走;
  4. 请求和保存条件:进程已有一个或多个资源又提出新请求,但是无法获取而阻塞状态

什么时候会发生死锁?

对不可剥夺资源的不合理分配,可能导致死锁。

  1. 对互斥的系统资源的争夺
  2. 进程推进顺序非法,a和b并发,分别占1,2,又互相请求对方的资源
  3. 信号量使用不当,比如生产者消费者问题;或者有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。

死锁的几种场景:

  1. 忘记释放锁
  2. 重复加锁
  3. 多线程多锁,抢占锁资源

死锁问题代码举例:

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

// 创建2个互斥量
pthread_mutex_t mutex1, mutex2;

void * workA(void * arg) {

    pthread_mutex_lock(&mutex1);
    sleep(1);
    pthread_mutex_lock(&mutex2);

    printf("workA....\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}


void * workB(void * arg) {
    pthread_mutex_lock(&mutex2);
    sleep(1);
    pthread_mutex_lock(&mutex1);

    printf("workB....\n");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);

    return NULL;
}

int main() {

    // 初始化互斥量
    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);

    // 创建2个子线程
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, workA, NULL);
    pthread_create(&tid2, NULL, workB, NULL);

    // 回收子线程资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    // 释放互斥量资源
    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);

    return 0;
}

上述代码sleep是为了产生死锁问题故意的,防止A或B逻辑操作过快无法产生死锁,代码主要是演示死锁问题的产生;

2、死锁问题的处理

在这里插入图片描述

死锁的预防

死锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。

  1. 破坏互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

  2. 破坏不可剥夺条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。

  3. 破坏请求与保持条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:
    (1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;
    (2)资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;
    (3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。

  4. 破坏循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:
    (1)限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;
    (2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

死锁的避免

上面我们讲到的死锁预防是排除死锁的静态策略,它使产生死锁的四个必要条件不能同时具备,从而对进程申请资源的活动加以限制,以保证死锁不会发生。下面我们介绍排除死锁的动态策略–死锁的避免,它不限制进程有关申请资源的命令,而是对进程所发出的每一个申请资源命令加以动态地检查,并根据检查结果决定是否进行资源分配。就是说,在资源分配过程中若预测有发生死锁的可能性,则加以避免。这种方法的关键是确定资源分配的安全性。

1.安全序列

我们首先引入安全序列的定义:所谓系统是安全的,是指系统中的所有进程能够按照某一种次序分配资源,并且依次地运行完毕,这种进程序列{P1,P2,…,Pn}就是安全序列。如果存在这样一个安全序列,则系统是安全的;如果系统不存在这样一个安全序列,则系统是不安全的。

安全序列{P1,P2,…,Pn}是这样组成的:若对于每一个进程Pi,它需要的附加资源可以被系统中当前可用资源加上所有进程Pj当前占有资源之和所满足,则{P1,P2,…,Pn}为一个安全序列,这时系统处于安全状态,不会进入死锁状态。

虽然存在安全序列时一定不会有死锁发生,但是系统进入不安全状态(四个死锁的必要条件同时发生)也未必会产生死锁。当然,产生死锁后,系统一定处于不安全状态。

2.银行家算法

这是一个著名的避免死锁的算法,是由Dijstra首先提出来并加以解决的。

背景知识

一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似:银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。

问题的描述

一个银行家拥有一定数量的资金,有若干个客户要贷款。每个客户须在一开始就声明他所需贷款的总额。若该客户贷款总额不超过银行家的资金总数,银行家可以接收客户的要求。客户贷款是以每次一个资金单位(如1万RMB等)的方式进行的,客户在借满所需的全部单位款额之前可能会等待,但银行家须保证这种等待是有限的,可完成的。

银行家算法允许死锁必要条件中的互斥条件,占有且申请条件,不可抢占条件的存在,这样,它与预防死锁的几种方法相比较,限制条件少了,资源利用程度提高了。

这是该算法的优点。其缺点是:
〈1〉这个算法要求客户数保持固定不变,这在多道程序系统中是难以做到的。
〈2〉这个算法保证所有客户在有限的时间内得到满足,但实时客户要求快速响应,所以要考虑这个因素。
〈3〉由于要寻找一个安全序列,实际上增加了系统的开销。

死锁的检测与恢复

一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。常利用资源分配图、进程等待图来协助这种检测。

死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。一旦在死锁检测时发现了死锁,就要消除死锁,使系统从死锁状态中恢复过来。

  1. 最简单,最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程。

  2. 撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素。

此外,还有进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。

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

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

相关文章

[ 云计算 | Azure ] Episode 03 | 描述云计算运营中的 CapEx 与 OpEx,如何区分 CapEx 与 OpEx

正常情况如果你不是会计&#xff0c;或者对钱相关的数字比较敏感的财务&#xff0c;本文的一些东西你不会接触的&#xff0c;但是最为云架构或者云运营&#xff0c;你可能会遇到如何采购亦或者估算的我成本和运营成本等等&#xff0c;所以本文的一些知识点就需要进行一定的了解…

哪款蓝牙耳机音质好?内行推荐四款高音质蓝牙耳机

蓝牙耳机经过近几年的快速发展&#xff0c;在音质上的表现也越来越好。哪款蓝牙耳机音质好&#xff1f;最近看到很多人问。接下来&#xff0c;我来给大家推荐四款高音质蓝牙耳机&#xff0c;可以当个参考。 一、南卡小音舱蓝牙耳机 参考价&#xff1a;246 发声单元&#xff…

900万英镑!光量子计算公司PsiQuantum获得英国政府支持

PsiQuantum的模块化量子计算系统使用传统光纤将单个低温单元联网。&#xff08;图片来源&#xff1a;网络&#xff09;3月6日&#xff0c;PsiQuantum宣布&#xff0c;在英格兰西北部STFC的Daresbury实验室&#xff0c;开设先进研发设施。这项工作得到了英国政府科学创新和技术部…

HTTP加密/HTTPS工作过程

31.HTTPS即HTTP加密 前言 工作过程清楚明白&#xff01;&#xff01;&#xff01; 本质上是在讲SSL&#xff0c;适用面很广。不用再深挖了&#xff0c;密码学不用了解太多 SSL和TSL&#xff0c;本质上是一类东西 之后可以看看123 文章目录31.HTTPS即HTTP加密前言一、什么是HTT…

C++基础——C++面向对象之数据封装、数据抽象与接口基础总结

【系列专栏】&#xff1a;博主结合工作实践输出的&#xff0c;解决实际问题的专栏&#xff0c;朋友们看过来&#xff01; 《项目案例分享》 《极客DIY开源分享》 《嵌入式通用开发实战》 《C语言开发基础总结》 《从0到1学习嵌入式Linux开发》 《QT开发实战》 《Android开发实…

打怪升级之字符串的分界符与字符串替换

流的字符串分界符 在C的iostream中&#xff0c;有流的字符串分界符&#xff1a; " “和”"都代表简单的分隔。 因此&#xff0c;使用流来做字符串分隔的话&#xff0c;有一个比较简单的方案就是将原定义的分隔符通过替换的方式变成流的分隔符。然后再录入流中就能…

【论文简述】Learning Optical Flow with Kernel Patch Attention(CVPR 2022)

一、论文简述 1. 第一作者&#xff1a;Ao Luo 2. 发表年份&#xff1a;2022 3. 发表期刊&#xff1a;CVPR 4. 关键词&#xff1a;光流、局部注意力、空间关联、上下文关联 5. 探索动机&#xff1a;现有方法主要将光流估计视为特征匹配任务&#xff0c;即学习在特征空间中将…

软件设计师教程(十)计算机系统知识-结构化开发

软件设计师教程 软件设计师教程&#xff08;一&#xff09;计算机系统知识-计算机系统基础知识 软件设计师教程&#xff08;二&#xff09;计算机系统知识-计算机体系结构 软件设计师教程&#xff08;三&#xff09;计算机系统知识-计算机体系结构 软件设计师教程&#xff08;…

Zookeeper3.5.7版本——客户端命令行操作(节点删除与查看)

目录一、节点删除示例1.1、节点删除1.2、递归节点删除二、查看节点状态示例一、节点删除示例 1.1、节点删除 在客户端上创建 test 节点&#xff0c;并查看该节点 [zk: localhost:2181(CONNECTED) 5] create /test "123456"删除 test 节点&#xff0c;并查看该节点 […

初识rollup 打包、配置vue脚手架

rollup javascript 代码打包器&#xff0c;它使用了 es6 新标准代码模块格式。 特点&#xff1a; 面向未来&#xff0c;拥抱 es 新标准&#xff0c;支持标准化模块导入、导出等新语法。tree shaking 静态分析导入的代码。排除未实际引用的内容兼容现有的 commonJS 模块&#…

Sqoop详解

目录 一、sqoop基本原理 1.1、何为Sqoop&#xff1f; 1.2、为什么需要用Sqoop&#xff1f; 1.3、关系图 1.4、架构图 二、Sqoop可用命令 2.1、公用参数&#xff1a;数据库连接 2.2、公用参数&#xff1a;import 2.3、公用参数&#xff1a;export 2.4、公用参数&#xff…

MySQL数据库和表管理

MySQL数据库和表管理一、常用的数据类型1、int(N)2、float(m,d)3、char与varchar二、查看数据库结构1、查看当前服务中的数据库2、查看数据库中存在的表3、查看表结构三、SQL语句1、SQL语言规范2、SQL语言分类四、创建、删除数据库和表1、创建数据库2、创建表3、删除数据表4、删…

云医疗信息系统源码(云HIS)商业级全套源代码

云his系统源码&#xff0c;有演示 一个好的HIS系统&#xff0c;要具有开放性&#xff0c;便于扩展升级&#xff0c;增加新的功能模块&#xff0c;支撑好医院的业务的拓展&#xff0c;而且可以反过来给医院赋能&#xff0c;最终向更多的患者提供更好地服务。 私信了解更多&…

HTTPS加密流程

什么是HTTPSHTTPS是加密后的HTTP,相当于:HTTPS HTTP SSL/TLSHTTPS是基于"密钥"来实现的,客户端将发送的数据用密钥进行加密,服务器将收到的数据用密钥解密.对称加密和非对称加密对称加密是指:交互双方共同使用同一个密钥进行加密 或 解密.如下(图中的客户端和服务器…

还不会CAD批量打印图纸?学会这招再也不怕

各位工程师&#xff0c;相信大家在工作过程中&#xff0c;经常会遇到需要打印大量图纸文件的时候&#xff0c;那么多的图纸&#xff0c;一张张打印太麻烦&#xff0c;此时便需要用到CAD批量打印功能啦&#xff01;可是&#xff0c;总有些新手设计师不直达奥CAD批量打印&#xf…

Spring事务及传播机制

概念 在MySQL中介绍过&#xff0c;当同一时间出现一起读写数据的情况&#xff0c;可能会导致最终的结果出错&#xff0c;因此可以使用事务来提高隔离级别 而Spring中也可以实现事务 手动添加事务 使用SpringBoot中的DataSourceTransactionManager对象可以获取事务&#xff0…

计算机科学导论笔记(六)

目录 八、算法 8.1 概念 8.1.1 非正式定义 8.1.2 示例 8.1.3 定义动作 8.1.4 细化 8.1.5 泛化 8.2 三种结构 8.2.1 顺序 8.2.2 判断 8.2.3 循环 8.3 算法的表示 8.3.1 UML 8.3.2 伪代码 8.4 更正式的定义 8.5 基本算法 8.5.1 求和 8.5.2 求积 8.5.3 最大和最…

Egg:使用joi进行参数校验以及注册接口小demo

目录 前言&#xff1a; 准备工作&#xff1a; 前端代码&#xff1a; 后端目录截图&#xff1a; 1.获取参数 2.校验参数 3.查询数据库中是否已经存在该用户 4.用户入库 5.测试一哈 添加用户成功 同样的用户名再注册一遍 ​编辑总结&#xff1a; 前言&#xff1a; 在阅…

C#基础之基础语法(一)

总目录 文章目录总目录前言一、C#简述1 C#是什么&#xff1f;2 .Net平台3. C# 和.Net的关系4. 集成开发环境&#xff08;IDE&#xff09;二、控制台应用程序1. 常用代码2.注意事项三、基础语法1.编写C#代码注意事项2.C#注释2. 变量&标识符&关键字4. 变量&#xff0c;字…

unity 面试汇总

1、什么是协同程序&#xff1f;答&#xff1a;在主线程运行时同时开启另一段逻辑处理&#xff0c;来协助当前程序的执行。换句话说&#xff0c;开启协程就是开启一个可以与程序并行的逻辑。可以用来控制运动、序列以及对象的行为。2、Unity3D中的碰撞器和触发器的区别&#xff…