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

做网站的可以注册个工作室吗一个网站两个域名吗

做网站的可以注册个工作室吗,一个网站两个域名吗,龙中龙网站开发,二级域名做网站有哪些缺点目录 初识线程 线程的概念 Linux下的线程 线程优缺点 线程控制 线程创建 线程终止 线程等待 线程分离 线程取消 其它 线程互斥 互斥的概念 互斥锁的使用 锁的本质 线程同步 线程同步的概念 条件变量的概念 条件变量的使用 信号量 信号量的概念 信号量接口…目录 初识线程 线程的概念 Linux下的线程 线程优缺点 线程控制 线程创建 线程终止 线程等待 线程分离 线程取消 其它 线程互斥 互斥的概念 互斥锁的使用 锁的本质 线程同步 线程同步的概念 条件变量的概念 条件变量的使用 信号量 信号量的概念 信号量接口的使用 常见锁的概念 互斥锁与自旋锁 乐观锁与悲观锁 其它 经典同步问题 哲学家就餐问题与死锁 生产者-消费者模型 读写者模型与读写锁 内容补充 线程的局部存储  volitale关键字 概念相关 初识线程 线程的概念 线程thread是操作系统能够调度的最小单位它被包含在进程之中。一个线程就是进程中的一个执行流所以一个进程中可以存在多个线程多个线程之间并行或并发地执行不同的任务。也就是说进程是承担系统资源分配(CPU、内存等资源)的基本单位而线程是CPU调度的基本单位。 线程是进程内部的执行流所以同一进程中的多个线程共享该进程中的大多数资源如虚拟地址空间、页表、文件描述符表和信号处理表等等。但每一个线程又都有各自的栈空间、寄存器信息、线程上下文等。 在单CPU单核心的情况下同一进程的多个线程之间是并发地执行的而在多核心的情况下多个线程之间是并行地执行的(真正意义上的同时运行)。其中的渊源可以参考线程级并行。特别的单CPU多核心可以使多个线程并行地执行但同一时间只能运行一个进程。多CPU的情况下既可以使多线程并行执行也可以使多进程并行地执行。 Linux下的线程 不同的操作系统对线程的实现有所不同在Windows下是以内核级线程的形式存在的而在Linux下线程是以轻量级进程的形式体现的。也就是说Linux下的线程是复用了进程的结构的。而我们知道每一个进程都有一个唯一的进程PID用于标识所以Linux中的每一个线程也都有一个唯一的编号LWP用于标识。也就是说在Linux底层任务调度看的是LWP而不是PID。其中我们可以使用 ps -aL 指令查看当前终端下的线程解释如下 -a 选项表示列出所有进程 -L 选项会使得 ps 输出中不仅包含进程的基本信息还包括其下属线程(轻量级进程)的LWP。 其中如果一个进程只有一个执行流那么线程PID就等于这个执行流线程的LWP。 但归根结底Linux下并没有真正的线程有的只是轻量级进程。而在Linux下可以通过诸如clone等函数来模拟出一个线程。但手动封装的使用成本太高而且不具备统一性。所以Linux是自带了一个POSIX标准的pthread线程库的。而pthread库本身就是Linux的一部分所以pthread又叫pthread原生线程库。 这个pthread库本质上就是封装了一些底层的系统调用为我们封装了一个线程出来所以Linux的线程被称之为用户级线程。也就是说这个pthread库对上层用户提供的是线程的接口其实底层依旧是使用的轻量级进程相关的系统调用。但由于pthread库并不是C或者C标准的相当于一个第三方库。所以我们在使用gcc或g编译时需要额外添加 -lpthread 选项指定连接的库文件名称由于pthread库在 /usr/include/ 目录下所以就不需要再指定路径了。 事实上pthread库是一个共享库(动态库)所以可执行程序在运行时pthread是被加载到共享区的。而pthread库在封装管理一个线程时是会设置一些线程的属性的这些线程属性就构成了一个线程属性集 struct pthread_attr_t typedef struct {int etachstate; //线程的分离状态int schedpolicy; //线程调度策略structsched_param schedparam; //线程的调度参数int inheritsched; //线程的继承性int scope; //线程的作用域size_t guardsize; //线程栈末尾的警戒缓冲区大小int stackaddr_set; //线程的栈设置void* stackaddr; //线程栈的位置size_t stacksize; //线程栈的大小 } pthread_attr_t;而为了方便pthread库中的线程id就直接设为了pthread变量中线程属性集在共享区中的地址。所以pthread库的线程id并不等同于内核的LWP。也就是说在Linux底层按照LWP进行调度虽然pthread库封装了LWP但我们无法通过pthread库直接看到LWP。 线程优缺点 线程的特点 线程的创建删除与调度比进程的更快捷。且线程占用的资源也要比进程少很多。 线程的调度切换比进程切换快捷的原因在于首先由于进程的的多个线程共享该进程的大多数资源所以同一进程的线程之间在进行调度切换时只需要将每个线程独有的CPU上下文、栈空间等信息切换即可。而且由于CPU的局部性原理线程切换可能不需要重新加载CPU内的catch缓存而进程切换一定会重新加载catch。同一进程中的多个线程共享该进程中的大多数资源如PID信息、虚拟地址空间、页表、文件描述符表和信号处理表等等。但每一个线程又都有各自的栈空间、寄存器信息、线程上下文等。同一进程下的每个线程信号的未决和递达状态是私有的但信号的理方法却是共有的。线程的CPU上下文是线程的私有数据而内存中的数据是被所有线程所共享的。 线程 VS 进程 线程不能独立执行程序必须依附于某个进程。且一切进程至少都有一个执行流(线程)。多线程的情况下先执行哪个线程是不确定的。多线程能够充分利用多核或者多处理器的并行处理特性。在等待慢速I/O操作结束的同时程序可执行其他的计算任务。对于计算密集型的应用可以将计算分解到多个线程中实现。对于I/O密集型应用可以将I/O操作重叠让多个线程同时等待不同的I/O操作。但多线程不具备独立性。对于同一进程的多个线程一个线程崩了就会导致整个进程出崩溃进而触发信号机制终止进程进程终止该进程内的所有线程也会随即退出。多线程带来便利的同时也带来了一系列的安全问题所以在使用多线程的同时还需要兼顾临界资源的保护、线程的同步调度等条件会对进程性能有一定的影响而且会增加使用成本。多线程会有缺乏访问控制的问题。进程是访问控制的基本粒度(单位)在一个线程中调用某些系统调用时例如信号相关的系统调用会对整个进程造成影响。 注意后续内容出现的线程id指的并不是LWP而是指的pthread库封装的线程id。 线程控制 线程创建 Linux中使用 pthread_create 函数用于创建一个新线程。 #include pthread.hint pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg); 函数说明 thread参数是一个输出型参数表示创建的新线程id。pthread_t 是线程id的类型。attr参数表示指定新创建线程的属性如果为null则表示使用默认属性。start_routine参数是一个函数指针表示新线程要执行的函数。arg参数是作为start_routine函数的参数传入进去的。返回值成功返回0失败返回一个非0的错误码。 特别的start_routine函数的参数和返回值都是void*类型的这就说明其实我们可以将需要传入或返回的多种数据放到一个结构体中这样start_routine函数就可以传入和返回多种类型的数据了。即只要有地址那么各种类型的数据都可以传只需要按照特定的格式使用即可。 pthread_create函数虽然叫做创建线程但其实应该叫创建并运行线程因为pthread_create在创建线程成功后新线程紧接着就会被调度。且新线程创建完成后主线程会照常向后执行新线程也正常运行它们之间并不会相互影响。 线程终止 Linux中使用 pthread_exit 函数用于退出一个线程 #include pthread.hvoid pthread_exit(void *retval); 函数说明 函数只有一个参数retval表示要返回的值可以为null。函数没有返回值。 需要注意的是线程终止其实可以直接return的效果和pthread_exit类似。也可以用。但不能用exit函数终止线程因为exit是直接退出整个进程。 线程等待 Linux中使用 pthread_join 函数用于线程等待 #include pthread.hint pthread_join(pthread_t thread, void **retval); 函数说明 thread参数表示要等待的线程id。retval参数是一个输出型参数表示获取等待进程的返回值。可以设为null。返回值成功返回0失败返回一个非0的错误码。 我们知道进程通过需要wait来回收资源而线程同样也需要等待。如果线程退出没有等待也会导致诸如进程僵尸的问题只不过多线程这里很难看到这个现象。而且一般我们默认main函数所在的线程为主线程当主线程退出时默认会退出整个进程那么自然所有的线程也都会退出。如果main函数的执行流先于其它线程退出会导致未定义问题。 所以介于上述种种原因线程创建之后并不能直接结束还是很有必要进行线程等待的。其中pthread_join为阻塞等待。一般我们不考虑非阻塞式的等待要么阻塞等待要么不等待。 线程分离 Linux中使用 pthread_detach 函数用于线程分离 #include pthread.hint pthread_detach(pthread_t thread); 函数说明 thread参数表示要分离的线程id。返回值成功返回0失败返回一个非0的错误码。 线程分离的作用是将指定线程标记为分离的线程。当分离的线程终止时其资源会自动释放回系统无需通过线程等待回收。而且一旦线程被分离就不能再使用pthread_join()对其进行等待了对一个已分离的线程使用pthread_join()等待会导致程序异常。 线程分离就相当于是脱离了主线程的控制主线程也就不再管他了。但线程分离并不是真正意义上的将线程从当前进程中分离出去其实只是对分离线程的属性做了修改将其标为分离的。所以线程分离除就相当于是一个线程独立了但依旧是属于整个进程执行流中的一员。 线程取消 Linux中使用 pthread_cancel 函数用于取消一个线程 #include pthread.hint pthread_cancel(pthread_t thread); 函数说明 thread参数表示要分离的线程id。返回值成功返回0失败返回一个非0的错误码。 线程取消顾名思义就是取消一个线程。pthread_cancel是一个非阻塞函数所以调用了该函数后哪怕子线程还没终止主线程仍然可以继续往下运行。 但Linux下的线程取消本质上是通过向thread线程发送终止信号来实现的。而我们知道同一进程下的线程共用一套信号处理方法所以一个线程取消(终止)了那么就会导致整个进程退出。所以为了达到只取消一个线程的效果我们还需要借助 pthread_setcancelstate 函数设置当前线程对于线程取消的自定义方法。 int pthread_setcancelstate(int state, int *oldstate) 详情参考线程取消(pthread_cancel) - CynthiaSky - 博客园 (cnblogs.com) 其它 pthread_self 除了上述的几个关键函数外pthread库中常用的还有 pthread_self() 函数用于获取执行当前线程的线程id。函数声明如下 #include pthread.hpthread_t pthread_self(void); 主线程问题 一般情况下我们认为main函数所在的执行流叫做主线程主线程退出会导致其它的副线程也随之退出且不受线程分离的影响。 但其实进程中并没有什么主线程的概念同一个进程中的所有线程之间都是平级关系。即线程都是一样的, 退出了一个不会影响另外一个。之所以main函数执行流退出会导致其它执行流也退出是因为整个进程在启动时其入口代码是以类似 exit( main(...) ) 的方式调用main函数的所以main函数正常执行结束之后会随之调用 exit() 函数所以就会导致整个进程直接退出。 想让main函数所在的线程退出不影响整个进程的做法是在main函数中用 pthread_exit() 函数代替原来的return。按照POSIX标准如果主线程在子线程终止之前调用了pthread_exit()子线程是不会退出的。main()中调用了pthread_exit后会导致主线程提前退出其后的exit()就无法执行了所以要到其他线程全部执行完了整个进程才会退出。 内容参考多线程情况下主线程先退出子线程会被强制退出吗 线程退出没有退出码 之所以线程没有退出码不处理线程出异常的情况是因为如果一个线程出异常了那么整个进程就挂掉了所以就没有这个必要。 每个线程拥有独立栈空间的理解 线程上下文是由pthread库封装了Linux内核的相关系统调用即线程上下文是Linux内核维护的而每一个线程的栈空间则是需要由pthread库提供的。 譬如语言的 stdio 库可以维护用户级缓冲区所以 pthread 库维护一个线程的栈空间是行得通的。 而pthread库运行时是被加载到共享区的所以pthread维护的栈本质上是在堆区申请的空间。而虚拟地址空间中的栈是则由主线程来使用的。 线程互斥 互斥的概念 我们知道同一进程的每个线程都有各自独立的栈空间而虚拟地址空间中还有其它空间诸如全局区、静态区等。所以多线程的情况下全局变量等非栈区的资源其实是被所有线程所共享的。我们把这种多个执行流都可以访问的资源叫做共享资源。特别的我们把任意时刻只能被一个执行流访问的资源(共享资源)叫做临界资源例如显示器的本质是stdout本质上就是一种临界资源。而临界资源本质上是要通过代码访问的我们便把访问临界资源的代码部分叫做临界区临界区就是我们将来要保护的区域。而任何时刻有且只有一个执行流进入临界区访问临界资源就叫做互斥互斥通常对临界资源起保护作用。 那么我们为什么需要互斥呢这里就需要再引入一个叫做原子性的概念了。原子性是指一个操作在执行过程中被视为不可分割的单一单元即该操作要么全部完成要么完全不执行不存在完成一半的这种中间状态。一般情况下我们的大多数操作都不是原子的甚至就连一个简单的 a 操作都会分解为读取、加1、写回三个步骤。我们知道线程调度可能会在任意时刻发送即线程在执行中可能在任意位置发生中断所以就有可能发生对临界资源的错误处理。所以我们需要用一种方式对临界资源保护起来保证临界资源的互斥访问这种方式就是互斥锁。 互斥锁又叫互斥量通过提供一种排他性访问控制手段确保在任何时刻只有一个线程能够对受保护的临界区进行操作而其他试图同时访问该区域的线程则会被阻塞直至持有锁的线程完成其操作并释放锁。 互斥锁的使用 互斥锁的初始化与释放 Linux下互斥锁的初始化方式如下 #include pthread.hint pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; 可以看到有两种初始化方式可以用 PTHREAD_MUTEX_INITIALIZER 宏直接定义并初始化。如果想要实现更为复杂的功能可以用 pthread_mutex_init 函数。其中pthread_mutex_t 是互斥锁的结构体类型。 pthread_mutex_init 的函数说明如下 pthread_mutex_t是互斥锁的id。mutex参数指向要初始化的锁。attr参数表示初始化锁设置的属性为null就表示使用默认属性。返回值成功返回0失败返回一个非0的错误码。 相应的互斥锁的释放函数为 pthread_mutex_destroy其只有一个参数指向要释放的互斥锁。返回值也是成功返回0失败返回一个非0的错误码。而pthread_mutex_destroy函数的本质其实就是对mutex互斥锁进行去初始化操作使传入的mutex成为一个无效值。 #include pthread.hint pthread_mutex_destroy(pthread_mutex_t *mutex); 互斥锁在初始化时如果不需要设置属性可以直接用宏的方式初始化其效果就等同于pthread_mutex_init 函数的attr参数设为null。但一般习惯全局和静态的互斥锁使用宏初始化局部的或类内的用 pthread_mutex_init 函数进行初始化。 申请锁与释放锁 互斥锁初始化完成之后就可以使用了我们通过 pthread_mutex_lock 函数进行加锁操作申请锁 #include pthread.hint pthread_mutex_lock(pthread_mutex_t *mutex); 函数说明 mutex参数表示对 mutex 指向的互斥锁进行加锁或者叫申请锁。返回值成功返回0失败返回一个非0的错误码。 我们用 pthread_mutex_lock 函数对后续内容进行加锁当要出临界区时我们就需要对锁资源进行释放而不是一直持有锁。所以对应的我们用 pthread_mutex_unlock 函数来释放锁。 #include pthread.hint pthread_mutex_unlock(pthread_mutex_t *mutex); 加锁操作的过程大致可以概括为当线程进行加锁操作时pthread_mutex_lock函数首先会查看当前mutex锁的状态。如果有其它线程正在使用该锁那么当前进程就会在pthread_mutex_lock函数处阻塞住。如果该锁没有线程在使用那么就申请锁并返回。也就是说任何一个时刻只允许一个线程申请锁成功而失败的线程就会在mutex_lock处阻塞住。 在申请锁处阻塞本质上是对锁这种资源的等待所以锁其实也可以看作是一种资源。而多个执行流都需要使用锁且同一时刻只允许一个线程获得锁所以更确切的说锁其实是一种临界资源。所以加锁操作其实又叫申请锁互斥锁又叫互斥量。 而在加锁期间的代码处也是有可能会被发生线程切换的。但由于这段代码是在加锁期间的所以只要我们还没解锁那么其他线程就无法进入到加锁期间的代码会在申请锁的代码处阻塞住。也就是说锁保证了加锁期间的整个临界区的原子性。 除了 pthread_mutex_lock 阻塞式申请锁之外其实还有 pthread_mutex_trylock 非阻塞式申请锁只不过我们一般使用的都是阻塞式申请锁。 内容补充 加锁和解锁之间的区域就是真正被锁住的部分。由于加锁和解锁的操作本身就是原子的所以加锁和解锁操作本身其实是安全的。加锁虽然能够保证临界资源的安全性但并不能一味地使用锁。使用锁必然会导致效率降低所以要尽可能给少的代码加锁一般只给临界区加锁。加锁的一般原则哪个线程加锁就由哪个线程解锁。多个执行流访问同一个临界资源需要用同一把锁否则将达不到互斥的效果。即一个临界资源对应一把锁不受线程数量的影响。 锁的本质 所谓的锁在计算机里本质上就是一块内存空间。当这个空间被赋值为1的时候表示加锁了被赋值为0的时候表示解锁了仅此而已。多个线程抢一个锁就是抢着要把这块内存赋值为1。在一个多核环境里内存空间是共享的。每个核上各跑一个线程那如何保证一次只有一个线程成功抢到锁呢这就需要硬件的支持了。 内容参考互斥锁mutex的底层原理是什么- 知乎 (zhihu.com) 线程同步 线程同步的概念 由于不同环境的线程调度方式不同所以在多执行流运行的过程中可能会出现某个或某些线程由于无法获得必要的资源如CPU时间、锁、同步对象等而长期处于等待状态无法执行或者长时间无法完成任务的现象。这种现象叫做线程的饥饿问题。例如对于临界区加锁的情况下可能就会出现长期只有一个线程抢占到锁资源那么其它线程自然就线程饥饿了。而且如果只有互斥锁那么很有就会出现频繁申请释放锁不断抢占锁资源的情况会让效率降低。 而为了避免线程饥饿的问题和频繁申请释放锁的低效情况我们就需要让线程保证线程同步。同步就是协同步调即让多个线程按照规定的先后顺序执行。 也就是说只有互斥没有同步会导致线程饥饿等一些问题。所以多线程访问临界资源不光要让线程之间互斥保证安全问题。还要让线程之间保持同步按照一定的步调执行保证多线程执行的效率问题。 条件变量的概念 线程同步是通过条件变量来做到的条件变量能让线程不做无效的锁申请使得多线程运行的过程具有顺序性。条件变量可以理解为是一个锁资源的控制者可以让多个执行流在申请锁时按照一定的顺序在条件变量处排队这样条件变量就能让多个线程按照一定的先后顺序拿到锁资源也就使得多个执行流能够按照一定的先后顺序访问临界区。 而条件变量其实是提供了一种唤醒机制和等待队列的可以实现线程的阻塞和对等待队列中阻塞线程的唤醒操作。而条件变量相当于使锁执行同步机制的一个控制条件所以条件变量是需要互斥锁配合使用的。 条件变量的使用 条件接口的使用和互斥锁的接口使用方法类似大致如下。 条件变量的初始化与释放 #include pthread.h // 初始化 int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); pthread_cond_t cond PTHREAD_COND_INITIALIZER; // 释放 int pthread_cond_destroy(pthread_cond_t *cond); 接口说明 pthread_cond_t是条件变量的id。pthread_cond_init是初始化条件变量的函数参数cond表示指向要初始化的条件变量参数attr表示设置的条件变量属性可以为null如果为null则表示使用默认属性。如果attr为null也可以直接用PTHREAD_COND_INITIALIZER 宏来初始化。pthread_cond_destroy函数用于销毁条件变量本质上是对cond指向的条件变量去初始化。 条件变量的等待与唤醒 条件变量的等待与唤醒操作有三个常用的函数 // 在指定条件变量下等待 int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);// 唤醒指定条件变量下的一个线程 int pthread_cond_signal(pthread_cond_t *cond);// 唤醒指定条件变量下的全部线程 int pthread_cond_broadcast(pthread_cond_t *cond); pthread_cond_wait函数在指定的条件变量处阻塞住加入到对应条件变量的队列下排队等待cond表示指定的条件变量mutex表示对应的互斥锁。大致过程是线程在pthread_cond_wait处阻塞时会自动释放对应的锁pthread_cond_wait返回之后线程结束阻塞并重新申请锁。所以当线程在pthread_cond_wait处阻塞时锁资源处于空闲状态其它进程就可以申请锁并持有锁了。条件变量等待之后需要唤醒才能再继续否则就会一直阻塞。pthread_cond_signal函数使指定的条件变量成立并唤醒一个在指定条件变量处阻塞的线程。cond参数表示指定的条件变量。先阻塞的先入等待队列所以先阻塞的先唤醒。pthread_cond_broadcast函数也是使指定的条件变量成立并唤醒所有等待这个条件变量的线程。其中批量唤醒是无法保证唤醒线程的执行顺序的。 信号量 信号量的概念 信号量的本质是一个计数器用于解决多个执行流对共享资源的访问控制问题。例如某个资源最多只允许2个进程同时访问那么就将对应的信号量设为2。信号量的核心操作在于其PV操作 P 操作先将信号量的值减 1表示申请占用一个资源。如果减一之后的信号量小于 0则表示已经没有可用资源了那么执行流会在 P 操作处被阻塞住。例如在P操作之后如果信号量的值为2表示还有 2 个资源可以使用如果信号量的值为-2则表示有两个进程正在等待使用这个资源。V 操作将信号量值加 1表示释放一个资源。若减一之后的信号量小于等于 0则表示有执行流正在等待该资源此时需要唤醒一个在对应信号量处阻塞的执行流。 P 操作和 V 操作必须成对出现。缺少 P 操作就不能保证对资源的互斥访问缺少 V 操作就会导致资源一直得不到释放阻塞住的执行流永远得不到唤醒。信号量的本质就是一个计数器申请信号本质上就是预定资源且PV操作是原子的。 图例出处 Linux内核六大进程通信机制原理 互斥锁与信号量的主要区别如下 粒度互斥锁是细粒度的同步工具主要针对单一资源的互斥访问信号量则是粗粒度的可以控制多个同类资源的并发访问或多个线程的复杂同步关系。计数能力互斥锁不具备计数能力只能表示锁定或解锁状态信号量具有计数能力其值表示可用资源的数量。适用场景互斥锁适用于简单的互斥需求确保同一时刻只有一个线程访问特定资源信号量适用于更复杂的同步场景如控制资源的并发访问量、实现多线程的协作流程。 信号量接口的使用 需要注意的是POSIX标准的信号量是的头文件是semaphore.h而不是pthread.h了。 信号量的初始化与释放 #include semaphore.h// 信号量的初始化 int sem_init(sem_t *sem, int pshared, unsigned int value); // 信号量的释放去初始化 int sem_destroy(sem_t *sem); 接口说明 sem_t是信号量的id。sem_init和sem_destroy中的sem参数都表示指向目标信号量的指针只不过一个是用于初始化一个是用于去初始化。pshared参数表示信号量的共享方式如果为0则表示信号量在进程的线程之间共享。如果pShared为非零则信号量在进程之间共享并且应该位于共享内存区中。value参数表示信号量的初始化值后续可以通过PV操作对其修改。 信号量的PV操作 #include semaphore.h// P操作递减信号量减到0就阻塞 int sem_wait(sem_t *sem); // V操作递增信号量大于0就唤醒 int sem_post(sem_t *sem); 接口说明 P操作和V操作都只有一个参数sem表示指向目标信号量的指针。sem_wait函数P操作减少(锁定)sem指向的信号量。如果当前信号量的值大于0则正常递减。如果当前信号量当前的值等于0则会阻塞住知道被唤醒。sem_post函数V操作递增(解锁)sem指向的信号量。如果递增之后的信号量大于0则尝试唤醒目标信号量处阻塞住的执行流。 常见锁的概念 下述内容部分参考什么是悲观锁、乐观锁 | 小林coding (xiaolincoding.com) 互斥锁与自旋锁 概念认识 最底层的两种锁就是「互斥锁和自旋锁」有很多高级的锁都是基于它们实现的你可以认为它们是各种锁的地基所以我们必须清楚它俩之间的区别和应用。 加锁的目的是保证共享资源在任意时间里只有一个线程访问这样就可以避免多线程导致共享数据错乱的问题。而互斥锁和自旋锁对于加锁失败后的处理方式是不一样的 互斥锁加锁失败后线程会释放 CPU 给其他线程自旋锁加锁失败后线程会忙等待直到它拿到锁 互斥锁 互斥锁是一种「独占锁」比如当线程 A 加锁成功后此时互斥锁已经被线程 A 独占了只要线程 A 没有释放手中的锁线程 B 加锁就会失败于是就会释放 CPU 让给其他线程既然线程 B 释放掉了 CPU自然线程 B 加锁的代码就会被阻塞。 对于互斥锁加锁失败而阻塞的现象是由操作系统内核实现的。当加锁失败时内核会将线程置为「睡眠」状态等到锁被释放后内核会在合适的时机唤醒线程当这个线程成功获取到锁后于是就可以继续执行。如下图 所以互斥锁加锁失败时会从用户态陷入到内核态让内核帮我们切换线程虽然简化了使用锁的难度但是存在一定的性能开销成本。 那这个开销成本是什么呢就是会有两次线程上下文切换的成本 当线程加锁失败时内核会把线程的状态从「运行」状态设置为「睡眠」状态然后把 CPU 切换给其他线程运行接着当锁被释放时之前「睡眠」状态的线程会变为「就绪」状态然后内核会在合适的时间把 CPU 切换给该线程运行。 上下切换的耗时有大佬统计过大概在几十纳秒到几微秒之间如果你锁住的代码执行时间比较短那可能上下文切换的时间都比你锁住的代码执行时间还要长。所以如果你能确定被锁住的代码执行时间很短就不应该用互斥锁而应该选用自旋锁否则就使用互斥锁。 自旋锁 自旋锁是通过 CPU 提供的 CAS 函数Compare And Swap在「用户态」完成加锁和解锁操作不会主动产生线程上下文切换所以相比互斥锁来说会快一些开销也小一些。 一般加锁的过程包含两个步骤 第一步查看锁的状态如果锁是空闲的则执行第二步第二步将锁设置为当前线程持有 CAS 函数就把这两个步骤合并成一条硬件级指令形成原子指令这样就保证了这两个步骤是不可分割的要么一次性执行完两个步骤要么两个步骤都不执行。 如果使用的是自旋锁当发生多线程竞争锁的情况时加锁失败的线程会「忙等待」直到它拿到锁。这里的「忙等待」可以想象成是一个while循环实现。 自旋锁是最比较简单的一种锁一直自旋利用 CPU 周期直到锁可用。需要注意的是在单核 CPU 上需要抢占式的调度器即不断通过时钟中断一个线程运行其他线程。否则自旋锁在单 CPU 上无法使用因为一个自旋的线程永远不会放弃 CPU。 自旋锁开销少在多核系统下一般不会主动产生线程切换适合异步、协程等在用户态切换请求的编程方式但如果被锁住的代码执行时间过长自旋的线程会长时间占用 CPU 资源所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系我们需要清楚的知道这一点。 其中互斥锁的接口我们已经在前面认识了下面是Linux下自旋锁的相关接口使用方法和互斥锁的类似只是原理有所不同这里就不再过多赘述了。 #include pthread.h// 自旋锁初始化 int pthread_spin_init(pthread_spinlock_t *lock, int pshared);// 自旋锁的销毁去初始化 int pthread_spin_destroy(pthread_spinlock_t *lock);// 自旋锁上锁 int pthread_spin_lock(pthread_spinlock_t *lock);// 自旋锁解锁 int pthread_spin_unlock(pthread_spinlock_t *lock); 互斥锁与自旋锁对比 自旋锁与互斥锁使用层面比较相似但实现层面上完全不同当加锁失败时互斥锁用「线程切换」来应对自旋锁则用「忙等待」来应对。 它俩是锁的最基本处理方式更高级的锁都会选择其中一个来实现比如读写锁既可以选择互斥锁实现也可以基于自旋锁实现。 乐观锁与悲观锁 前面提到的互斥锁、自旋锁、读写锁都是属于悲观锁。 悲观锁做事比较悲观它认为多线程同时修改共享资源的概率比较高于是很容易出现冲突所以访问共享资源前先要上锁。 乐观锁做事比较乐观它假定冲突的概率很低每次取数据时候总是乐观的认为数据不会被其他线程修改因此不上锁。但是在更新数据前会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式版本号机制和CAS操作。 乐观锁虽然去除了加锁解锁的操作但是一旦发生冲突重试的成本非常高所以只有在冲突概率非常低且加锁成本非常高的场景时才考虑使用乐观锁。 其它 公平锁与非公平锁 公平锁公平锁在并发环境中获取锁时会查看锁维护的等待队列如果队列为空或者当前线程是等待队列的第一个就占有锁否则加入等待队列按照FIFO等待获取锁。非公平锁非公平锁直接尝试占有锁如果尝试失败就在采用类似公平锁的方式获取锁。 独占锁写锁与共享锁读锁 独占锁独占锁锁一次只能被一个线程持有。共享锁共享锁可以被多个线程共同持有。 经典同步问题 哲学家就餐问题与死锁 哲学家就餐问题的概述如下。假设有一张圆桌周围坐着5位哲学家每位哲学家面前摆放着一盘食物和两把叉子每位哲学家只能使用自己左右两侧的叉子来用餐。哲学家只有思考和进餐两种状态他们不断地在这两个状态之间切换。哲学家必须同时拿到左右两只叉子才能进餐。吃完一口他们就会继续放下两只叉子回到思考状态。如图所示 图例出处 多线程冲突了怎么办 | 小林coding (xiaolincoding.com) 问题的漏洞在于当所有哲学家都拿起一边的叉子后就会出现循环等待的情况。例如当所有哲学家都拿起左侧的叉子时他们下一步的动作都是去拿右侧的叉子那么就会陷入无限等待右侧叉子的情况。 将哲学家抽象成线程将叉子抽象为互斥锁当哲学家拿叉子死循环时就是死锁的场景。 理论上讲死锁有四个必要条件 互斥条件一个共享资源每次只能被一个执行流使用。持有并等待条件一个执行流因请求资源而阻塞(等待)时对已获得的资源保持不放。不可剥夺条件一个执行流已获得的资源在末使用完之前不能强行剥夺。循环等待条件若干执行流之间形成一种头尾相接的循环等待资源的关系。 所以要想避开死锁问题就需要从上述四个条件入手想办法破环其中一个条件就不会形成死锁了。 解决哲学家就餐问题的常见方法有 最多只允许4个哲学家同时拿筷子保证至少有一人能够进餐。仅当左、右两根筷子均可用时,才允许他拿起筷子。奇数号哲学家先拿左边的筷子偶数号先拿右边的筷子。 其原理都是破坏了循环等待条件对应的还有死锁检测算法也是破环了循环等待条件通过构建线程与资源之间的等待图 (WFG)通过拓扑排序判断图中是否有环进而判断是否会构成循环等待条件。 还有一个做银行家算法用一句话概括就是当一个线程要申请使用资源的时候银行家算法通过先“尝试”分配给该进程资源然后通过安全性算法判断分配后的系统是否处于安全状态若不安全则试探分配作废让该进程继续等待。大致流程图如下 参考 一句话一张图说清楚——银行家算法-CSDN博客 但其实解决死锁的最好办法就是不使用锁即无锁编程。但多执行流下的无锁编程不是那么简单的感兴趣的可以尝试阅读无锁队列的实现 | 酷 壳 - CoolShell 生产者-消费者模型 生产者-消费者模型由两个角色——生产者和消费者一个交易场所——缓冲区一般是内存空间构成。生产者生产数据后不直接交给消费者而是放到缓冲区中。而消费者则每次从缓冲区中拿数据。任何时刻只能有一个生产者或者消费者可以访问缓冲区。 所以生产者和生产者、消费者和消费者、生产者与消费者之间都是互斥的关系。 而且如果缓冲区为空则消费者必须等待生产者生产数据如果缓冲区满了则生产者必须等待消费者从缓冲区中取出数据。所以生产者和消费者之间不仅是互斥的而且还需要同步。 只需要对缓冲区加锁就可以保证同一时刻只有一个生产者或者消费者访问缓冲区了而至于生产者和消费者则可以用两个条件变量控制一个用于缓冲区为空时让消费者阻塞一个用于缓冲区满时让生产者阻塞。 生产消费模型通过一个缓冲区将生产和消费两个操作进行解耦支持多生产多消费。这种异步处理方式提高了系统的整体吞吐量和响应速度。尤其适用于处理速度不一致、工作负载波动较大的场景。 读写者模型与读写锁 读写者模型是一种读者多写者少且读写频次比较高的场景。与生产消费者模型类似读写者模型由两个角色——生产者和消费者以及一个场所——缓冲区构成。但不同的是生产者是从缓冲区中拿数据拿完之后缓冲区中的数据也就不在了而读者是从缓冲区中读数据拿到的只是数据的拷贝读完之后缓冲区中的数据还在。 读写者模型的关系描述 读者与读者没有互斥与同步的关系只有并行或并发的关系。写者与写者互斥的关系同一时刻只允许一个写者向缓冲区中写入。读者与写者互斥与同步的关系。没有写者时读者才能读没有读者时写者才能写。 所以基于读写者模型便产生了读写锁。读写锁是一种特殊的锁机制它允许多个读线程同时访问资源但同一时刻只允许一个写线程进行写操作。 其中Linux下有具体的读写锁接口 #include pthread.h// 初始化与释放 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr); pthread_rwlock_t rwlock PTHREAD_RWLOCK_INITIALIZER;// 读加锁 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 写加锁 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);// 设置读写属性 int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref); /* pref共有3种选择PTHREAD_RWLOCK_PREFER_READER_NP (默认缺省)读者优先可能会导致写者饥饿情况PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先但写者不能递归加锁 */ 内容补充 线程的局部存储  每个线程都有各自的栈空间但其它的全局变量、静态变量等是被所有进程所共享的。 而如果在变量之前加一个 __thread 就表示线程的局部存储即这个全局变量或其他共享资源就不再同步了。原理大致为将这个变量在自己的线程局部存储区域私有一份。这个工作发生在编译期。 要注意__thread 编译选项并不支持c的各种容器。 volitale关键字 volatile是一个C语言的类型修饰符。它被设计用来修饰被不同线程访问和修改的变量。如果没有volatile基本上会导致这样的结果要么无法编写多线程程序要么编译器失去大量优化的机会。 volatile用于提醒编译器它后面所定义的变量随时都有可能改变因此编译后的程序每次需要存储或读取这个变量的时候告诉编译器对该变量不做优化都会直接从变量内存地址中读取数据而不是去CPU寄存器中读取从而可以提供对特殊地址的稳定访问。 如果没有volatile关键字则编译器可能优化读取和存储可能暂时使用寄存器中的值如果这个变量由别的程序更新了的话将出现不一致的现象。 所以在多线程的情况下如果进行了编译优化就需要对共享变量加volatile修饰。 概念相关 可重入与不可重入 概念同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。常见不可重入的情况 调用了malloc/free的函数因为malloc函数是用全局链表来管理堆的。调用了标准I/O库的函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构。可重入函数体内使用了静态的数据结构 常见可重入的情况 不使用全局变量或静态变量不使用用malloc或者new开辟出的空间不调用不可重入函数不返回静态或全局数据所有数据都有函数的调用者提供使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据 互斥、同步、异步、竞争条件 互斥是指某一资源同时只允许一个访问者对其进行访问具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序即互斥是无序的。同步就是协同步调让多个线程按照规定的先后顺序执行。通常需要建立在互斥的基础上。异步异步与同步相对异步就是多执行流之间彼此独立互不干涉。竞争条件是指多执行流对同一共享资源进行访问或修改时由于它们的执行顺序受到各自调度和执行速度不确定性的影响导致最终结果取决于这些实体执行时序的特定情况而非遵循预期的逻辑。 并发、并行与串行 并发并发是指两个或多个事件在同一时间间隔内发生。虽然看起来是同时进行的但实际上是通过快速的切换来实现的。并行指的是多个任务同时进行可以利用多个处理器核心或者多个CPU来实现。串行指的是多个任务按照一定的顺序依次执行一个任务必须在前一个任务完成之后才能开始执行。
http://www.pierceye.com/news/855689/

相关文章:

  • 郑州视频网站建设大概多少钱赶集网2022年最新招聘
  • 购物网站怎么做优化wordpress 暖岛 主题
  • 帝国cms如何做电影网站广告设计要学哪些软件
  • 企业做网站的意义网站建设的知识
  • 重庆荣昌网站建设价格内网网站建设流程
  • 专业网站建设哪家好网站开发英语英语
  • 亿恩 网站备案做养生网站需要什么资质
  • 镇江网站建设案例洛阳网站建站
  • 网站建设如何把代码沈阳网站制作
  • 微网站自己怎么做的模版网站和语言网站
  • 做平台是做网站和微信小程序的好别京津冀协同发展国家战略
  • 北京怎样做企业网站电脑网页开发
  • 企业网站建设运营方案Wordpress hover插件
  • 做暧暖ox免费网站微信开店小程序怎么弄
  • 网站建站网站网站维护动画设计属于什么大类
  • 深圳宝安上市公司网站建设报价制作网站去哪家好
  • 沈阳做网站客户多吗网站地图抓取
  • 做网站比较专业的公司微信商城在哪里找
  • 网站建设开发的流程网站标题title怎么写
  • 网络营销的优势海宁网站怎么做seo
  • wordpress 英文主题南宁网站排名优化公司
  • 行业网站建设方案有专门做电商网站的CMS吗
  • 网站备案 快递公司变更流程
  • 简单的做图网站wordpress加密授权
  • 哪里做网站域名不用备案新华舆情监测平台
  • 品牌工厂网站建设qt 网站开发
  • xxx网站建设规划家庭服务网站的营销策略
  • 哪里可以做宝盈网站江门百度seo公司
  • 电子商务的网站建设名词解释如何建立官网
  • 网站建设维护外包群排名优化软件