操作系统之线程

一、线程简介

早期的计算机系统只允许一个任务独占系统资源, 一次只能执行一个程序。由于对程序并发执行的需求,引入了多进程。进程的引入可以解决多任务支持的问题,但是也产生了新的问题:每个进程分别分配资源开销比较大,进程频繁切换导致额外系统开销,进程间的通信实现复杂。考虑现实中的场景:
一个word程序如果采用多进程,一个进程负责界面交互,一个进程负责后台运算,会相当低效 (进程通信不好实现 进程频繁切换导致额外的系统开销)。一个同时要处理大量请求的网络数据库如果采用多进程,对每个请求都创建一个进程去响应那服务器的资源很快就耗尽了,而且进程切换消耗很大。
由此就演化出了在一个进程的内存空间上开辟多个"小进程",利用这些小进程来实现多个任务的方法,这些小进程就是所谓的线程。这些线程在进程的内存空间内共享很多进程的资源,所以每个线程分配资源开销不会很大。线程的规模较小,切换开销也不会很大。线程之间共享进程的一部分地址空间,线程之间的通信也不会很麻烦。从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。

线程是进程里面的一个执行序列,每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。同一个进程中的多个线程可以并发执行,一个线程可以创建和撤销另一个线程,但是线程不能够独立执行,必须依存在进程中。每个进程运行时都会创建一个主线程,也叫主控线程,通过主控线程可以继续创建其他子线程。
进程是资源管理的最小单位,线程是程序执行调度的最小单位。
进程的实现只能由操作系统内核来实现,而不存在用户态实现的情况。线程既可以通过内核来实现 也可通过用户态来实现。因为线程的管理者可以是用户也可以是操作系统本身,线程是进程内部的东西,当然存在由进程直接管理线程的可能性,因此线程的实现就应该分为内核态线程实现和用户态线程实现。
多线程之间切换消耗资源少,但是不稳定 一个线程崩溃了会影响整个进程;多进程之间切换消耗资源多,但是稳定 一个进程崩溃不会影响其他进程。

协程是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程,一个线程也可以拥有多个协程。 协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。可以在线程内并发执行,又不会引起安全问题。

二、线程资源

线程间共享的资源:内核区:文件描述符表,每种信号的处理方式,当前工作目录;用户区:堆区,数据区( bss段,Data段Test 段) 

线程间独占的资源
1.线程ID 
每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。
2.寄存器组的值 
线程间是并发运行的,每个线程有自己不同的运行上下文,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程重新切换时能恢复。
4.错误返回码 
同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以不同的线程应该拥有自己的错误返回码变量。
5.线程的信号屏蔽码 
每个线程感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
6.线程的优先级 
线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
7.线程的栈 
一个栈中只有最下方的帧可被读写,相应的也只有该帧对应的那个函数被激活,处于工作状态。为了实现多线程必须绕开栈的限制。为此在创建新的线程时, 要为这个线程建新的栈,每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务。多线程的进程在内存中有多个栈,多个栈之间以固定的区域隔开,以备栈的增长。每个线程可调用自己栈下方的帧中的参数和变量。

三、线程切换

(1)一般的进程切换分为两步 :1)切换页目录使用新的地址空间;2)切换内核栈和硬件上下文。对于Linux来讲,地址空间是线程和进程的最大区别,如果是线程切换的话,不需要切换页目录使用新的地址空间。但是切换内核栈和硬件上下文则是线程切换和进程切换都需要做的。
(2)切换进程上下文:
进程上下文可以分为三个部分: 
用户级上下文:  正文、数据、用户堆栈以及共享存储区;
寄存器上下文:  通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文:  进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。
系统中的每一个进程都有自己的上下文。一个正在使用处理器运行的进程称为当前进程。当前进程因时间片用完或者因等待某个事件而阻塞时,进程调度需要把处理器的使用权从当前进程交给另一个进程,这个过程叫做进程切换。此时,被调用进程成为当前进程。在进程切换时系统要把当前进程的上下文保存在指定的内存区域(该进程的任务状态段TSS中),然后把下一个使用处理器运行的进程的上下文设置成当前进程的上下文。当一个进程经过调度再次使用CPU运行时,系统要恢复该进程保存的上下文。进程的切换也就是上下文切换。
(3)线程切换:
Linux下的线程实质上是轻量级进程。线程生成时会生成对应的进程控制结构,只是该结构与父线程的进程控制结构共享了同一个进程内存空间。同时新线程的进程控制结构将从父线程(进程)处复制得到同样的进程信息,如打开文件列表和信号阻塞掩码等。创建线程比创建新进程成本低,因为新创建的线程使用的是当前进程的地址空间。相对在进程之间切换,在线程之间切换所需的时间更少,因为后者不包括地址空间的切换。
线程上下文切换的原理与此类似,只是线程在同一地址空间中,不需要MMU等切换,只需要切换必要的CPU寄存器,因此,线程切换比进程切换快的多。

四、线程的用户级和内核级

进程的实现只能由操作系统内核来实现,而不存在用户态实现的情况。但是线程的管理者可以是用户也可以是操作系统本身,因此线程的实现分为内核态线程实现和用户态线程实现。

线程是进程的不同执行序列,也就是说线程是独立运行的基本单位,也是CPU调度的基本单位。那么操作系统是如何实现管理线程的?

首先操作系统像管理进程一样维护线程的所有资源,将线程控制块存放在操作系统的内核空间中,此时操作系统就同时掌管进程控制块和线程控制块。操作系统管理线程的好处是:

1.用户编程简单;
2.如果一个线程执行阻塞操作,操作系统可以从容的调度另外一个线程的执行。

内核线程的实现缺点是:

1.效率低,因为线程在内核态实现,每次线程切换都需要陷入到内核由操作系统来调度,而由用户态切换到内核态要花很多时间。另外内核态实现会占用内核稀有的资源,因为操作系统要维护线程列表,操作系统所占内核空间一旦装载后就无法动态改变,并且线程的数量远远大于进程的数量,随着线程数的增加内核将耗尽;

2.内核态的实现需要修改操作系统。

用户态是如何实现管理线程的?用户态管理线程就是用户自己做线程的切换,自己管理线程的信息,操作系统无需知道线程的存在。在用户态下进行线程的管理需要用户创建一个调度线程。一个线程在执行完一段时间后主动把资源释放给其他线程使用,而在内核态下则无需如此,因为操作系统可通过周期性的时钟中断把控制权夺过来,在用户态实现情况下,执行系统的调度器也是线程,没有能力夺取控制权。对操作系统来说,用户级线程具有不可见性。用户态实现的优点:

首先是灵活,不用修改操作系统,操作系统不用知道线程的存在,所以任何操作系统上都能应用;其次,线程切换快,因为切换在用户态进行,无需陷入内核态。

用户态实现的缺点:

首先编程复杂,由于在用户态下各个进程间需要相互合作才能正常运转。那么在编程时必须考虑什么情况下让出CPU,让其他的线程运行,而让出时机的选择对线程的效率和可靠性有很大影响,这个并不容易做到;

其次,用户态线程实现无法完全达到线程提出所要达到的目的:进程级多道编程,如果在执行过程中一个线程受阻,它无法将控制权交出来,这样整个进程都无法推进。操作系统随即把CPU控制权交给另外一个进程。这样,一个线程受阻造成整个进程受阻,通过线程对进程实施分身的计划就失败了。这是用户态线程致命的缺点。

调度器激活:线程阻塞后,CPU控制权交给了操作系统,要激活受阻进程的线程,唯一的办法就是让操作系统在进程切换时先不切换,而是通知受阻的进程执行系统(即调用执行系统),并问其是否还有别的线程可以执行。如果有,将CPU控制权交给该受阻进程的执行系统线程,从而调度另一个可以执行的线程到CPU上。一个进程挂起后,操作系统并不立即切换到别的进程上,而是给该进程二次机会,让其继续执行。如果该进程只有一个线程,或者其所有线程都已经阻塞,则控制权将再次返回给操作系统。这时操作系统就会切换到其他线程了。

鉴于用户态与内核态都存在缺陷,现代操作将两者结合起来。用户态的执行负责进程内部线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换,即同时实现内核态和用户态线程管理。每个内核态线程可以服务一个或多个用户态线程。
什么情况下会造成线程从用户态到内核态的切换?首先,如果在程序运行过程中发生中断或者异常,系统将自动切换到内核态来运行中断或异常处理机制;此外,程序进行系统调用也会从用户态切换到内核态。

可重入函数指可以多个任务并发使用,而不必担心数据错误的函数。相反,“不可重入函数”则是只能由一个任务所占用,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要在使用本地变量或在使用全局变量时保护自己的数据。

有几种场合必须使用单线程:1、程序可能会fork(2);2、限制程序的CPU占用率。单线程程序的优势在于简单。如果很少的CPU负载或IO流量就能让对方满载,那么多线程程序都没啥优势。多线程的适用场景是:提高响应速度,让IO和“计算”相互重叠,降低延迟。虽然多线程不能提高绝对性能,但能提高平均响应性能。一个多线程服务程序的线程大致可分为三类:1. IO线程,这类线程的主循环是IO多路复用,阻塞地等在select/poll/epoll_wait系统调用上,也处理定时事件。当然它的功能不止IO,有些简单计算也可放入其中。2.计算线程,这类线程的主循环是阻塞队列,阻塞地等在条件变量上。这类线程一般位于线程池中,通常不涉及IO,一般要避免任何阻塞操作。3.第三方库所用的线程,比如logging,database connection。线程上下文切换是有开销的,如果它的收益不能超过它的开销,那么使用多线程来提高效率将得不偿失

进程与线程的一个简单解释 - 阮一峰的网络日志

一个故事讲完进程、线程和协程

小白科普:线程和线程池

我是一个进程

五、线程的使用

使用线程相关函数需要包括头文件#include<pthread.h> 。两种方式可以打印线程的id :①在线程调用函数中使用pthread_self 函数来获得线程id;②在创建函数时生成的id。

int pthread_create(pthread_t *thread, canst pthread_attr_t *attr, void* (*start_routine) (void*), void*arg);
第一个参数:传入参数.若线程创建成功,则该参数赋值指向线程标识符的指针,指向内存单元将存放线程id.
若创建线程失败 则该参数未定义
第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数
返回值:若线程创建成功,则返回 0;若线程创建失败,则返回出错编号

主线程回收子线程函数

int pthread_join(pthread_t thread, void **retval)
第一个参数为被等待的线程标识符,
第二个参数为一个用户定义的指针,用来获取被等待线程结束时候传进来的返回值 

这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止, 函数返回时,被等待线程的资源被收回。代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束。

#include<stdio.h>
#include<pthread.h>
#include<iostream>
#include<string>
using namespace std;
struct thread_info{
  unsigned long thread_id;
  string thread_name;
};
void* thread_func(void* argv	)
{
	cout<<"now in thread...."<<endl;
	cout<<"main thread name:"<<(*(thread_info *)argv).thread_name<<endl;
  	cout<<"main thread id:"<<(*(thread_info *)argv).thread_id<<endl;

	pthread_exit((void*)pthread_self());//结束时传出线程自己的id
}
int main()
{
  //下面结构体作为子线程参数
  thread_info main_thread_info;
  main_thread_info.thread_id=pthread_self();
  main_thread_info.thread_name="Kyle";
  //提前设置要创建线程的属性
  pthread_attr_t attr;
  int Ret=pthread_attr_init(&attr);
  if(Ret)
  {
    cout<<"thread   attrinit error!"<<endl;
  }
  Ret=pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    if(Ret)
  {
    cout<<"thread set error!"<<endl;
  }
  //创建线程
  pthread_t tid;
  int Ret1= pthread_create(&tid,NULL,thread_func,(void *)&main_thread_info);
  if(Ret1)
  {
    cout<<"thread error:Return value="<<Ret1<<endl;
    return Ret1;
  }
  //回收线程(获取线程结束以后的状态量)
  void* Ret_val;
  int Ret2=pthread_join(tid,&Ret_val);
  if(Ret2)
  {
	  cout<<"thread join error"<<endl;
	  return Ret2;
  }
  cout<<"son thread id:"<<*((unsigned long *)Ret_val)<<endl;
  return 0;
}

六、线程同步

同步与互斥机制是用于控制多个任务对某些特定资源的访问策略。同步是控制多个任务按照一定的规则或顺序访问某些共享资源;互斥是控制某些共享资源在任意时刻只能允许规定数量的任务访问。临界区域critical section是指多使用者可能同时共同操作的那部分代码,比如自加自减操作,多个线程处理时就需要对自加自减进行保护,这段代码就是临界区域。

线程惊群

往一群鸽子中间扔一块食物,所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去等待下一块食物。每扔一块食物都会惊动所有的鸽子即为惊群。对于操作系统来说,多个进程/线程在等待同一资源时也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统性能大打折扣。
2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。

线程安全
线程安全就是多线程访问时采用了加锁机制,当一个线程访问该类的某个数据时进行保护,其他线程不能进行访问。直到该线程读取完,其他线程才可使用,不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
线程安全问题由全局变量及静态变量引起。若每个线程中对全局变量、静态变量只有读操作而无写操作,一般来说这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

RAII

为了更有效安全的使用互斥量,一般使用RAII手法来封装互斥量,即用RAII的手法来执行互斥量的创建、销毁、加锁和解锁的步骤。RAII的意思是在对象的构造函数中对资源进行申请,在析构函数中对资源进行释放。设想在临界区代码的收尾分别进行了互斥锁的加锁和解锁,但是运行过程中 一个线程的临界区代码发生了异常退出,然后就没有执行到互斥锁解锁的那一行代码,这个互斥锁就一直锁在那,别的线程一直等待这个锁解开,也就造成了死锁。如果用RAII的手法用对象对互斥量进行封装,在构造函数中执行互斥量的加锁,在析构函数中执行互斥量的解锁,这样出现上面在临界区产生异常而退出的情况的时候对象也会被销毁。对象被销毁的时候就会执行析构函数,从而在析构函数里面执行互斥量释放的代码

1.互斥量

Linux的pthreads mutex采用futex实现,不必每次加锁解锁都陷入系统调用。

互斥量API
锁的动态创建
int pthread_mutex_init(pthread_mutex_t *mutex , const pthread_mutexattr_t *attr); 
锁的静态创建pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;(作为全局变量)
加锁 pthread_mutex_lock()
解锁 pthread_ mutex unlock()
测试加锁pthread_mutex_trylock() 

三分钟基础知识:互斥那点事儿(上)
三分钟基础知识:互斥那点事儿(下)

/********
 //模拟多个线程"抢票"过程
 //模拟用互斥量同步的情况下线程之间的抢占 
 //设定总票数ticket_num为全局变量
 // 在每个线程中减1 表示买一张票
********/
#include<pthread.h>
#include<iostream>
#include<string>
#include<stdio.h>
#include<unistd.h>
using namespace std;
int ticket_nums=10;//票数作为全局变量
 pthread_mutex_t my_mutux;  //表示互斥量
 //也可以用下面这一句静态初始化互斥量的方式 
 //如果用下面这一句来创建互斥量 就不用在主函数里面用pthread_mutex_init
 //pthread_mutex_t mutex_x= PTHREAD_MUTEX_ INITIALIZER ;
void* thread_func(void *ticket)
{
//每个线程内部轮询抢票15次
for(int i=0;i<15;i++)
{
    pthread_mutex_lock(&my_mutux);

  if((*(int *)ticket)>0)
    {
        (*(int *)ticket)--;
        cout<<"In thread"<<pthread_self()<<" Buy one ticket...."<<endl;
    }
    else
    {
         cout<<"In thread"<<pthread_self()<<"No Tickets Left...."<<endl;
    }
     pthread_mutex_unlock(&my_mutux);
     sleep(1);//注意这里的一秒钟休眠 让其他线程有机可乘
}
  //如果不想给主线程传递消息的话 pthread_exit也可以不加 函数结束 线程会自动结束
 pthread_exit(NULL);   
}
 int main()
 {
 //并发多个线程  模拟多个线程抢票的过程
 int Ret;
 pthread_t tid[4];//表示线程id
pthread_mutex_init(&my_mutux,NULL);//表示互斥量初始化
 for(int i=0;i<4;i++)
 {
       //创建线程
        Ret=pthread_create(&tid[i],NULL,thread_func,&ticket_nums);
        if(Ret)
        {   
            cout<<"creat thread error...."<<endl;
            return Ret;
        }
 }
// 主线程休眠5秒 让子线程暂时脱离主线程的控制各自抢票 
//如果此时主线程不休眠 马上往下执行会立刻关闭子线程
 sleep(10);
 for(int i=0;i<4;i++)
 {
       //销毁线程
        Ret=pthread_join(tid[i],NULL);
        if(Ret)
        {   
            cout<<"destory thread error...."<<endl;
            return Ret;
        }
 }
     return  0;
 }

2.条件变量

条件变量可以想象成一种对讲机, 这种对讲机的接收端只能收听,发送端只能说话。每次买票的人发现没票了,售票员就给他一个接收对讲机,同时让他把锁打开出售票间然后先歇着(阻塞) 并且这些拿到接收端对讲机的人会排成一个队 (叫条件等待队列)。 一旦有票会通过对讲机通知他们,这个通知他们的人就是那些退票的人。 每次退完票就用他的对讲机说"现在有票了来买吧" 然后开锁出售票间,这样接收端对讲机的人也就是那些没买到票的人就会收到,然后按照入队顺去去买票。

条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并测试条件是否满足。

静态方式使用PTHREAD COND INITIALIZER,函数原型:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态方式则使用pthread_cond_init函数,函数原型:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr) 
注销条件变量函数原型:
int pthread_cond_destroy(pthread_cond_t *cond) 
只有无线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY.
因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程

条件变量等待有两种方式:条件等待 pthread_cond_wait()和计时等待pthread_cond_timedwait() 。其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT结束等待。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait() (或 pthread_cond_timedwai())的竞争条件。在更新条件等待队列以前, mutex 需保持锁定状态,并在线程挂起进入等待队列前解锁,在条件满足从而离开 pthread_cond_wait()之前, mutex 将被重新加锁,以与进入pthread_cond wait()前的加锁动作对应。在pthread_cond_wait()这个函数在检测到不满足条件的下包含了三个步骤
线程解锁->进入等待队列->等待到在满足条件时又加锁进入临界区
在一开始就检测到条件满足的情况下就直接跳过,继续执行下面的代码了。

激发条件也有两种形式
pthread_cond_signal()激活等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;
这个函数是非阻塞的,就算没有正在等待的线程这个函数也会成功返回,不会造成线程惊群现象。
pthread_cond_broadcast()则激活所有等待线程。

/********
 //模拟多个线程"抢票"过程
 //模拟用互斥量和条件变量同步的情况下线程之间的抢占 
 //设定总票数ticket_num为全局变量
 // 在每个线程中减1 表示买一张票
********/
#include<pthread.h>
#include<iostream>
#include<string>
#include<stdio.h>
#include<unistd.h>
using namespace std;
int ticket_num; //定义剩余票数 一开始定义为0 表示没有余票了
pthread_mutex_t mutex;//定义一个互斥锁
pthread_cond_t   con_var;//定义一个条件变量
void*  Buy_ticket(void* tmp)//买票线程
{
    pthread_mutex_lock(&mutex);
    while(!ticket_num)//这里应该用while 不能用if 避免虚假唤醒问题
    {
         cout<<"Thread"<<pthread_self()<<" start waiting......"<<endl;
        pthread_cond_wait(&con_var,&mutex);//如果发现没票了 就用条件变量让线程阻塞
    }
    ticket_num--;
    cout<<"Thread"<<pthread_self()<<" buy a ticket"<<endl;
    pthread_mutex_unlock(&mutex);

}
void*  Return_ticket(void* tmp)//买票线程
{
    pthread_mutex_lock(&mutex);
     ticket_num++;
       cout<<"Thread"<<pthread_self()<<" return  a ticket"<<endl;
    pthread_cond_signal(&con_var);//退一张票 通知等待队列里面的一个人可以买票了
    pthread_mutex_unlock(&mutex);
}

int main()
{
    pthread_t tid[4];//定义4个线程 其中3个线程买票 一个线程退票
    pthread_mutex_init(&mutex,NULL);//动态创建互斥锁
    pthread_cond_init(&con_var,NULL);//动态创建条件变量
    int Ret;
    //下面创建三个买票线程
    for(int i=0;i<3;i++)
    {
        Ret=pthread_create(&tid[i],NULL,Buy_ticket,NULL);
        if(Ret)
        {
            cout<<"thread creat fialed"<<endl;
             return Ret;
        }
    }
    //这里的暂停是为了让条件变量的效果显示的更明显一点
    // 让买票的的三个线程先在那等一会
    sleep(6);
    Ret=pthread_create(&tid[3],NULL,Return_ticket,NULL);
        if(Ret)
        {
            cout<<"thread creat fialed"<<endl;
            return Ret;
        }
    //这里的暂停是为了回收子线程
    //理论上此时只有一个线程买到票了 另外两个线程还在那等待  
    //用暂停6秒的形式回收那两个还在等待的线程
    sleep(6);
    return 0;
}

3.信号量

售票方卖票的时候有人买票的间隙别人不能中途插进来买票 (原子操作)。每个人买完票用好以后必须再退回来,总的票数是保持恒定的,也就是一次只能支持一个人对票的操作。引进一种叫信号量的机制作为票数据的载体,一个线程对信号量的加减(PV操作)也就是买票退票,是不能被其他线程中断的(原子操作)。

使用信号量同步需包含头文件semaphore.h ,信号量函数的名字都以sem_”打头,线程使用的基本信号量函数有以下4个

初始化函数int sem_init(sem_t *sem, int pshared, unsigned int value) ; 
该函数用于初始化sem指向的信号对象,设置它的共享选项并给它一个初始的整数,pshared控制信号量的类型,
如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享。
value为sem的初始值,调用成功时返回0 失败返回-1
信号量p操作函数用于以原子操作的方式将信号量的值减1:int sem_wait(sem_t *sem); 
sem 指向的对象是由sem init调用初始化的信号量。信号量大于0时不会阻塞,信号量减1然后返回0;
信号量等于0时会阻塞。

sem_trywait(sem_t *sem)是函数sem_wait的非阻塞版,如果信号量等于0则不阻塞直接返回错误代码。
信号量v操作sem_post函数以原子操作的方式将信号量的值加1,函数原型如下:
int sem_post(sem_t *sem); 
调用成功时返回0,失败返回-1
信号量销毁sem_destory函数用于对用完的信号量进行清理,函数原型如下:
int sem_destroy ( s em_t *s em); 
成功时返回0,失败时返回-1
/********
 //模拟多个线程"抢票"过程
 //模拟用信号量同步的情况下线程之间的抢占 
 // 在每个线程中减1 表示买一张票
********/
#include<pthread.h>
#include<iostream>
#include<string>
#include<stdio.h>
#include<unistd.h>
#include<semaphore.h>
using namespace std;
sem_t   ticket_num;//用信号量来存放票数

void* thread_func(void *thread_id)
{
    if(sem_wait(&ticket_num)==0)
    {
        cout<<"Thread "<<pthread_self()<<" buy a ticket"<<endl;
        usleep(100);
        sem_post(&ticket_num);
    }
    else
    {
         cout<<"Thread "<<pthread_self()<<" is waiting......"<<endl;
    }
  //如果不想给主线程传递消息的话 pthread_exit也可以不加 函数结束 线程会自动结束
 //pthread_exit(NULL);
}
int main()
{
 //并发多个线程  模拟多个线程抢票的过程
 int Ret;
 pthread_t tid[4];//表示线程id
sem_init(&ticket_num,0,2);//设定为局部信号量 票数设定为2

 for(int i=0;i<4;i++)
 {
       //创建线程
        Ret=pthread_create(&tid[i],NULL,thread_func,(void*)&i);
        if(Ret)
        {   
            cout<<"creat thread error...."<<endl;
            return Ret;
        }
        usleep(10);
 }
sleep(5);
return  0;
}

4.线程屏障

屏障是用户协调多个线程并行工作的同步机制。允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。线程屏障是线程同步的一个方式。线程执行完一个操作后,可能需要等待其他线程也完成某个动作,这时候当前该线程就会被挂起,直到其他线程也完成了某个操作,最后所有线程被唤醒。可以使用pthread_barrier_init函数对屏障进行初始化,用thread_barrier_destroy函数反初始化。屏障的本质就是计数,还没有达到某个数的时候,当前线程就被阻塞,等到最后一个线程执行pthread_barrier_wait函数并且得到了某个数的时候,全部线程被唤醒。

5.读写锁

1 )当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
2)当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞。
假设一个线程读/写操作完了 有很多读/写进程会抢夺去访问。这时根据调度策略不同,如果先调度读线程则成为强读者同步,反之则是强写者同步。

6.自旋锁

互斥锁想加锁但是锁已经被别的线程占有时,线程会阻塞;自旋锁想加锁但是锁已经被别的线程占有时,线程会轮询等待。自旋锁适用于线程占有锁时间短的场景,互斥锁要经历从阻塞到唤醒的过程,相对于自旋锁线程一直处理就绪状态轮询等待耗费的时间更多。但如果线程占有锁的时间很长,则其他持有自旋锁线程就会处于长时间的轮询忙等,所以适用于线程占有锁的时间较短的场景。

线程安全:代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

可重入函数:一个可重入的函数简单来说就是可以被中断的函数,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出错;不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,如果被中断的话可能会出现问题,这类函数是不能运行在多任务环境下的。不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数

也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果需要访问全局变量(包括 static),则应通过关中断、信号量(P、V操作)等手段对其加以保护。若对所使用的全局变量不加以保护,此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值