多线程及同步的四种方法

 

 

一、在介绍信号量&同步与互斥之前,我们先来了解一下生产者与消费者模型。

 


生产者将数据放入缓冲区,消费者将数据从缓冲区取走消费。

生产者消费者的模型提出了三种关系,两种角色,一个场所

三种关系:
- 生产者之间的互斥关系
- 消费者之间的竞互斥关系
- 生产者和消费者之间互斥和同步关系(同一时刻只能有一个,要么在生产,要么在消费,这就是互斥关系,只能在生产者生产完了之后才能消费,这就是同步关系)

两个角色:一般是用进程或线程来承担生产者或消费者

一个场所:有效的内存区域。(如单链表,数组)

我们就可以把这个想象成生活中的超市供货商,超市,顾客的关系,超市供货商供货,超市是摆放货物的场所,然后用户就是消费的。

 

信号量主要用于同步和互斥,那么什么是同步与互斥呢?

进程互斥:

由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系称为互斥。
系统中的某些资源一次只允许一个进程使用,这样的资源称为临界资源或者互斥资源。
进程中涉及到互斥资源的程序段叫临界区。

进程同步
进程同步指的是多个进程需要相互配合共同完成一项任务。

司机 p1        售票员 p2
while(1)        while(1)
{          {
  启动车辆;        关门;
  正常运行;        售票;
  到站停车;         开门;
}           }

信号量和P、V原语
信号量和p、v原语由大佬Dijkstra(迪杰斯特拉)提出

 

信号量

互斥:P、V在同一进程中
同步:P、V在不同进程中

 

信号量值的含义

s > 0: s表示可用资源的个数
s = 0: 表示无可用资源,无等待进程
s < 0: |s|表示等待队列中进程个数

 

信号量结构体的伪代码

  信号量本质上是一个计数器

  struct semaphore{

    int value;
    pointer_PCB queue;

  }

P原语

  p(s){

    s.value = s.value--;

    if(s.value < 0){

      该进程状态设置为等待状态;

      将该进程的PCB插入相应等待队列s.queue末尾;

    }

  }

 

V原语

  V(s){
    s.value = s.value++;
    if(s.value <= 0){
      唤醒相应等待队列s.queue中等待的一个进程
      改变其状态为就绪态
      将其插入就绪队列
    }
  }

 二、线程关闭

C++11有两种方式来等待线程结束:

  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。
    • 调用detach表示thread对象和其表示的线程完全分离;
    • 分离之后的线程是不在受约束和管制,会单独执行,直到执行完毕释放资源,可以看做是一个daemon线程;
    • 分离之后thread对象不再表示任何线程;
    • 分离之后joinable() == false,即使还在执行;
  • join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
    • 只有处于活动状态线程才能调用join,可以通过joinable()函数检查;
    • joinable() == true表示当前线程是活动线程,才可以调用join函数;
    • 默认构造函数创建的对象是joinable() == false;
    • join只能被调用一次,之后joinable就会变为false,表示线程执行完毕;
    • 调用 ternimate()的线程必须是 joinable() == false;
    • 如果线程不调用join()函数,即使执行完毕也是一个活动线程,即joinable() == true,依然可以调用join()函数;

无论在何种情形,一定要在thread销毁前,调用t.join或者t.detach,来决定线程以何种方式运行。

当使用join方式时,会阻塞当前代码,等待线程完成退出后,才会继续向下执行;

三、线程同步的四种方法

原文链接:线程同步的四种方式

1、临界区(Critical Section):通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。

优点:保证在某一时刻只有一个线程能访问数据的简便办法

缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

2、互斥量(Mutex):为协调共同对一个共享资源的单独访问而设计的。

互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。

优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

缺点:①互斥量是可以命名的,也就是说它可以跨越进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。

②通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。

3、信号量(Semaphore):为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。

优点:适用于对Socket(套接字)程序中线程的同步。(例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。)

缺点:①信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;

②信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;

③核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。

4、事件(Event): 用来通知线程有一些事件已发生,从而启动后继任务的开始。

优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。

缺点:

总结:

①临界区不是内核对象,只能用于进程内部的线程同步,是用户方式的同步。互斥、信号量是内核对象可以用于不同进程之间的线程同步(跨进程同步)。
②互斥其实是信号量的一种特殊形式。互斥可以保证在某一时刻只有一个线程可以拥有临界资源。信号量可以保证在某一时刻有指定数目的线程可以拥有临界资源。

posted @ 2023-07-29 17:52  IceArrow  阅读(598)  评论(0编辑  收藏  举报