线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁

线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁_第1张图片
线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。

进程间同步方式:
①事件(Event) 【进程&线程间同步–内核对象】
②互斥量【进程&线程间同步–内核对象】可以命名→可以跨进程
③信号量(Semaphore)【进程&线程间同步–内核对象】–有名信号量
  比如socket套接字,HTTP限制用户访问数量
  信号量机制功能强大,但使用时对信号量的操作分散,而且难以控制,读写和维护都很困难。
  优:PV操作能够实现对临界区的管理要求;实现简单;允许使用它的代码休眠,持有锁的时间可相对较长。
  缺:信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点。信号量机制功能强大,但使用时对信号量的操作分散,而且难以控制,读写和维护都很困难。加重了程序员的编码负担;核心操作P-V分散在各用户程序的代码中,不易控制和管理;一旦错误,后果严重,且不易发现和纠正。
  
④管程:针对信号量,又提出了一种集中式同步进程——管程
  将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成。这样模块之间联系清晰,便于维护和修改,易于保证正确性。
  缺:如果一个分布式系统具有多个CPU,并且每个CPU拥有自己的私有内存,它们通过一个局域网相连,那么这些原语将失效。而管程在少数几种编程语言之外又无法使用,并且,这些原语均未提供机器间的信息交换方法。
  
⑤自旋锁:
  调用者一直循环在那里看是否该自旋锁的保持着已经释放了锁,自旋锁是一种比较低级的保护数据结构和代码片段的原始方式。–【低开销,安全高效】
  导致问题:→(1)死锁 (2)过多地占用CPU资源

线程同步:
1.内核对象:
  ①事件(Event) 【进程&线程间同步–内核对象】  
  ②互斥量【进程&线程间同步–内核对象】
    可以命名→可以跨进程&可以用名字打开
  ③信号量【进程&线程间同步–内核对象】–无名信号量
2.进程对象:
  ④文件锁(文件记录锁和文件锁):进程生存期资源【进程终止→文件锁被释放】
  ⑤条件变量【存放在进程地址空间内—进程对象-不能用于进程】
  ⑥临界区(Critical Section)【仅线程间,不能跨进程–用户方式的同步】
    →速度快,实现简单,相比互斥量而言速度更快且消耗资源少
//⑦信号【应该属于异步通知吧,怎么可能回事同步机制呢?】
其中后两者需要依赖于共享内存才能用于进程间同步,因此只有文件锁是进程生存期的资源,其他的都属于内核生存期资源。

线程同步(本质是有序操作)

当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,只能等,直到该线程完成操作

互斥量(互斥锁mutex)

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

应用范围:进程内 or 跨进程使用

缺点:
1、跨越进程使用可以命名且创建互斥量需要的资源更多(跨进程的互斥量一旦被创建,就可以通过名字打开它)
//若仅仅是用于进程内,则相较于互斥量,临界区具有速度快,占用资源少的优势

2、通过互斥量可以指定资源被独占的方式使用
3、访问上限为1(临界区也是1,而信号量为1~若干)

在Linux操作系统中,用户层面上编程使用的所有锁都是建议锁,不具有强制性,因此访问共享数据的所有线程(进程)都应该先加锁才能访问。

  互斥量提供了对共享资源的保护访问,当一个线程使用互斥量锁定某个资源后,其他线程对该资源的访问都会被阻塞。 用于保护【临界区】(共享资源),以保证在任何时刻只有一个线程能够访问共享的资源。 互斥量类型声明为pthread_mutex_t数据类型。

注意:man 3 pthread_mutex_init时提示找不到函数,说明你没有安装pthread相关的man手册。安装方法:1、虚拟机上网;2、sudoapt-get install manpages-posix-dev

pthread_mutex_t 互斥锁//struct	近似为int

pthread_mutex_t mutex; 变量mutex(可用资源数量)只有两种取值1(空闲,可上锁)、0(已被占用)

1、互斥锁函数

1、静态初始化://定义在全局,或加static关键字修饰
				pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;//宏结构常量赋值给互斥锁(结构体)
2、动态初始化:局部变量必须采用动态初始化:pthread_mutex_init(&mutex, NULL)#include 
int pthread_mutex_init(pthread_mutex_t *mutex,//调用时应传 &mutex
 		 	const pthread_mutexattr_t *attr); //互斥量属性,通常用NULL表默认属性【快速互斥锁】
2、销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

32种上锁方式
3.1(阻塞式上锁):
int pthread_mutex_lock(pthread_mutex_t *mutex);
	 lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。

3.2、try上锁(非阻塞,若已被占据则立即返回BUSY)
int pthread_mutex_trylock(pthread_mutex_t *mutex);

4、解锁:	mutex++(或+1------线程归还资源(使用权)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
	unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。

5、销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。
由于在Linux中,互斥锁并不占用任何资源,
因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

2、互斥锁属性

互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:

PTHREAD_MUTEX_TIMED_NP 缺省(普通)锁,当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样保证当不允许多次加锁时不出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

对于普通锁和适应锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制,这个不同目前还没有得到解释。

条件变量:协调线程的执行顺序

从微观上来讲,线程同步要同时做到:
  ①保证线程互斥【互斥锁、信号量】
  ②保证了线程的执行顺序【条件变量】
  
条件变量协调线程的执行顺序,从而保证线程 安全,有顺序 的访问共享数据。
   通常与互斥锁配合使用,需要注意的是条件变量本身不是锁,而是给多线程提供一个会合的场所。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

条件变量和互斥量
   条件变量并不是锁,但是可以通过调用条件变量相关API函数来造成线程阻塞,唤醒等,通常配合互斥量使用,很多操作并不是直接针对互斥量的,而是通过条件变量来影响互斥量。

条件变量允许线程阻塞等待特定条件发生,当条件不满足时,则调用条件变量的等待相关函数,让线程进入阻塞状态并等待条件发生改变,一旦某个线程满足条件就调用条件变量的通知相关函数,可通知唤醒一个或多个阻塞的线程。

那么条件变量为什么会阻塞线程?其实条件变量本质上是一个等待队列,会把所有阻塞等待的线程放置在等待队列,而互斥量用来保护等待队列,所以条件变量通常和互斥锁一起使用。

线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁_第2张图片

  1. 首先生产者负责生产,消费者负责消费。如果生产者线程没有生产出产品(条件不成立),此时消费者线程应该调用相应的wait函数加入到线程等待队列中(条件变量)阻塞,等待被唤醒。

2. 当生产者线程生产出产品了(条件成立),生产者线程需要调用通知相关函数唤醒正在等待的消费者线程去消费。

也就是说生产者线程调用pthread_cond_signal函数和pthread_cond_broadcast函数唤醒等待队列中一个或多个消费者线程的同时,还会把消费者线程从等待队列中踢出,因为等待队列是用于放置阻塞的线程,而被唤醒的消费者线程由于不再阻塞,也就没必要在等待队列中,此时该线程会从等待队列中删除。

3. 如果等待队列中有多个阻塞的消费者线程,那么可能会同时唤醒多个消费者线程同时去消费产品,为了避免这种情况,需要互斥锁对等待队列上锁进行保护,从而达到保护线程对共享数据的访问。

条件变量的使用及函数

pthread_cond_init       
pthread_cond_destroy
pthread_cond_wait       
pthread_cond_signal(只能唤醒一个)/pthread_cond_broadcast(广播激活多个线程)

信号量sem

使用时设计的数据:
1、同时访问共享资源的线程最大数目(与sam创建时指定)
2、当前可用资源计数S = 可用最大数量 - 实际已用计数
          当S<0时表示正在等待使用共享资源的线程数

核心操作PV

P操作申请资源:
  (1)S减1;
  (2)若S减1后仍大于等于零,则进程继续执行;
  (3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
V操作释放资源:
  (1)S加1;
  (2)若相加结果大于零,则进程继续执行;
  (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。 (参考自:peaceminusone)

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

缺点:
1、信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;
2、信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;
3、核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。

  线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量sem_t sem,它可以被增加或减少,但对其的关键访问被保证是原子操作。并且带有两个原子操作wait和signal。Wait(别名down、P或lock),signal(别名up、V、unlock或post)。在UNIX的API中(POSIX标准)用的是wait和post。

信号量sem_t sem:
  正数:表示的是当前可用的资源个数
  0:没有空余资源,也没有线程在排队等资源
  负数:拿不到资源而处于阻塞等待状态的线程数量

POSIX有两种信号量的实现机制:
  ①无名信号量:只可以在共享内存的情况下,比如实现进程中各个线程之间的互斥和同步,也被称作基于内存的信号量
  ②命名信号量。命名信号量通常用于不共享内存的情况下,比如进程间通信

根据信号量取值的不同,POSIX信号量还可以分为:
  ①二值信号量:信号量的值只有0和1,这和互斥量很类似,若资源被锁住,信号量的值为0,若资源可用,则信号量的值为1;
  ②计数信号量:信号量的值在0到一个大于1的限制值之间,该计数表示可用的资源的个数。

二值信号量与互斥锁区别:
互斥锁和二值信号量在使用上非常相似,但是互斥锁解决了【优先级翻转】的问题
  ①互斥锁Mutex只能由获取它的线程释放【你不放手 别人永远拿不到】
  ②可以从任何其他线程(或进程)发出信号量信号【高优先级线程能够抢走使用权而使用资源】,因此信号量更适合某些同步问题,如producer-consumer。

线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁_第3张图片

无名信号量

#include
sem_t sem(非负整数)可以理解为一种资源的数量,或者房间的钥匙数量
①int sem_init(sem_t *sem, int pshared, unsigned int value);初始化
 pshard设置共享选项,0表示当前进程的局部信号量,否则信号量是在进程间共享
 value为sem的初始值
②int sem_destroy(sem_t *sem);注销
③sem_post() V操作 【我让出手中的资源】【原子操作】用来增加信号量的值,sem+1
   (若有人在等则让其解除阻塞,wait返回)           
④sem_wait() P操作 【我要资源】【原子操作】阻塞当前线程直到sem>0,解除阻塞后sem-1(拿到资源)
   (如果wait时sem>0则不进入阻塞,直接拿到资源并sem-1) 
   int sem_wait(sem_t *sem);//阻塞方式
   int sem_trywait(sem_t *sem);//非阻塞方式
   int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//限时阻塞方式
⑤int sem_getvalue(sem_t *restrict, int *restrict);获得当前信号量restrict
前四个函数成功放回0,失败-1

有名信号量

之所以称为命名信号量,是因为它有一个名字、一个用户ID、一个组ID和权限。这些是提供给不共享内存的那些进程使用命名信号量的接口。命名信号量的名字是一个遵守路径名构造规则的字符串。

①sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
 参数name是一个标识信号量的字符串
 参数oflag用来确定是创建信号量还是连接已有的信号量。
 oflag的参数可以为0,O_CREAT或O_EXCL:如果为0,表示打开一个已存在的信号量;如果为O_CREAT,表示如果信号量不存在就创建一个信号量,如果存在则打开被返回,此时mode和value都需要指定;如果为O_CREAT|O_EXCL,表示如果信号量存在则返回错误。
 mode参数用于创建信号量时指定信号量的权限位,和open函数一样,包括:S_IRUSR、S_IWUSR、S_IRGRP、S_IWGRP、S_IROTH、S_IWOTH。
 value表示创建信号量时,信号量的初始值。

②int sem_close(sem_t *);关闭命名信号量
   单个程序可以用sem_close函数关闭命名信号量,但是这样做并不能将信号量从系统中删除,因为命名信号量在单个程序执行之外是具有持久性的。当进程调用_exit、exit、exec或从main返回时,进程打开的命名信号量同样会被关闭。
③int sem_unlink(const char *name);
   在所有进程关闭了命名信号量之后,将信号量从系统中删除:

与无名信号量一样,操作信号量的函数如下:
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *restrict, int *restrict);

1、有名信号量必须指定一个相关联的文件名称,这个name通常是文件系统中的某个文件;无名信号量不需要指定名称。

命名和无名信号量的作用域
  有名信号量既可用于线程间的同步,又能用于进程间的同步;无名信号量通过shared参数来决定是进程内还是相关进程间共享。

命名和无名信号量的持续性
  命名信号量是随内核持续的。一个进程创建一个信号量,另外的进程可以通过该信号量的外部名(创建信号量使用的文件名)来访问它。进程结束后,信号量还存在,并且信号量的值也不会改动。【直到内核重新自举或调用sem_unlink()删除该信号量。】
  无名信号量的持续性却是不定的:①一个进程内的线程共享的无名信号量就是随进程持续的,当该进程终止时它也会消失。②不同进程间共享的无名信号量是必须存放在共享内存区中,只要该共享内存区存在,该信号量就存在。→所以此时无名信号量是随内核的持续性。

自旋锁

自旋锁的目的是为了保护共享资源,实现线程同步。自旋锁区别于其他锁的地方在于若某线程在未获得锁时将不断的询问(判断)自旋锁保持者是否释放了锁(获取锁操作将自旋在那里,不断地申请获取,直到自旋锁保持者释放了锁),因此比较适用于保持锁时间比较短的情况(CPU一直在空转)。需要注意的是:一个锁只能有一个保持着。

信号量、互斥量、条件变量的差异比较

1.互斥量必须由给它上锁的线程(持有者)【才能解锁】;而信号量:共享线程可以拿到/释放资源、(高优先级能够从低级那儿)抢资源。

2.互斥量要么被锁住,要么被解开,只有这两种状态;而信号量的值可以支持多个进程/线程成功的进行wait操作;
3.信号量的挂出操作总是被记住,因为信号量有一个计数值,挂出操作总会将该计数值加1,然而当条件变量发送一个信号时,如果没有线程等待在条件变量,那么该信号就会丢失。

代码示例:
#include 
#include 
#include 
#include 
#include 
#include 

//线程函数
void *thread_func(void *msg);
sem_t sem;//信号量

#define MSG_SIZE 512

int main()
{
   int res = -1;
   pthread_t thread;
   void *thread_result = NULL;
   char msg[MSG_SIZE];
   //初始化信号量,其初值为0
   res = sem_init(&sem, 0, 0);
   if(res == -1)
   {
       perror("semaphore intitialization failed\n");
       exit(EXIT_FAILURE);
   }
   //创建线程,并把msg作为线程函数的参数
   res = pthread_create(&thread, NULL, thread_func, msg);
   if(res != 0)
   {
       perror("pthread_create failed\n");
       exit(EXIT_FAILURE);
   }
   //输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
   printf("Input some text. Enter 'end'to finish...\n");
   while(strcmp("end\n", msg) != 0)
   {
       fgets(msg, MSG_SIZE, stdin);
       //把信号量加1
       sem_post(&sem);
   }

   printf("Waiting for thread to finish...\n");
   //等待子线程结束
   res = pthread_join(thread, &thread_result);
   if(res != 0)
   {
       perror("pthread_join failed\n");
       exit(EXIT_FAILURE);
   }
   printf("Thread joined\n");
   //清理信号量
   sem_destroy(&sem);
   exit(EXIT_SUCCESS);
}

void* thread_func(void *msg)
{
   //把信号量减1
   sem_wait(&sem);
   char *ptr = msg;
   while(strcmp("end\n", msg) != 0)
   {
       int i = 0;
       //把小写字母变成大写
       for(; ptr[i] != '\0'; ++i)
       {
           if(ptr[i] >= 'a' && ptr[i] <= 'z')
           {
               ptr[i] -= 'a' - 'A';
           }
       }
       printf("You input %d characters\n", i-1);
       printf("To Uppercase: %s\n", ptr);
       //把信号量减1
       sem_wait(&sem);
   }
   //退出线程
   pthread_exit(NULL);
}

事件(Event) :线程间or进程间

事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。
信号量包含的几个操作原语:
  CreateEvent() 创建一个事件
  OpenEvent() 打开一个事件
  SetEvent() 回置事件
  WaitForSingleObject() 等待一个事件
  WaitForMultipleObjects()         等待多个事件
    WaitForMultipleObjects 函数原型:
     WaitForMultipleObjects(
     IN DWORD nCount, // 等待句柄数
     IN CONST HANDLE *lpHandles, //指向句柄数组
     IN BOOL bWaitAll, //是否完全等待标志
     IN DWORD dwMilliseconds //等待时间
     )
参 数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。 dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回 WAIT_TIMEOUT。

死锁:多个进程在运行过程中因争夺资源而造成的一种僵局

死锁产生原因:

1、产生死锁中的2种竞争资源

1、竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
2、竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

系统中的资源可以分为两类:
1、可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
2、不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。

2、进程间推进顺序非法

线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁_第4张图片

死锁产生的4个必要条件

产生死锁的必要条件:
1、互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2、请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
4、环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

解决死锁的基本方法

预防死锁:

1、资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
2、只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
3、可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
4、资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

1、以确定的顺序获得锁
  如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。
2、超时放弃

线程同步

信号量+互斥量+条件变量
(条件变量http://www.blogjava.net/fhtdy2004/archive/2009/07/05/285519.html)
 多线程编程因为无法知道哪个线程会在哪个时候对共享资源进行操作,因此让如何保护共享资源变得复杂,通过使用线程间的同步,可以解决线程间对资源的竞争。
包括两种基本方法,第一种是“信号量”,第二种是“互斥量”。选择哪种方法取决于程序的实际需要。例如控制共享内存,使之在任何一个时刻只有一个线程能够对它进行访问,使用互斥量更为合适。但如果需要控制一组同等对象的访问权,例如从5条电话线里给某个线程分配一条,计数信号量就更合适。

“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”
也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进 行操作。在有些情况下两者可以互换。
信号量,是可以用来保护两个或多个关键代码段,这些关键代码段不能并发调用。在进入一个关键代码段之前,线程必须获取一个信号量。如果关键代码段中没有任何线程,那么线程会立即进入该框图中的那个部分。一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

作用域:
 信号量: 进程间或线程间(linux仅线程间的无名信号量pthread semaphore)
 互斥锁: 线程间

上锁时
信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait使得线程阻塞,直到sem_post释放后value值加一,但是sem_wait返回之前还是会将此value值减一
互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源

互斥量和信号量的区别

  1. 互斥量用于线程的互斥,信号量用于线程的同步。
    这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源(lseek创建空洞文件→多线程同时写入?)

  1. 互斥量值只能为0/1,信号量值可以为非负整数。
    也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

  2. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

1.信号量(信号灯)sem同步
信号灯与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于”等待”操作,即资 源不可用的话,信号灯机制则侧重于点灯,即告知资源可用;
没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持 灯亮状态。当然,这样的操作原语也意味着更多的开销。

信号灯的应用除了灯亮/灯灭这种二元灯以外,也可以采用大于1的灯数,以表示资源数大于1,这时可以称之为多元灯
信号量相关的函数名字以“sem”作为前缀,线程里使用的基本信号量函数有4个,被包含在头文件semaphore.h中。
①创建与注销
POSIX信号灯标准定义了有名信号灯和无名信号灯两种,但LinuxThreads的实现仅有无名灯,同时有名灯除了总是可用于多进程之间以外,在使用上与无名灯并没有很大的区别,因此下面仅就无名灯进行讨论。

int sem_init(sem_t sem, int pshared, unsigned int value)
这是创建信号灯的API,其中value为信号灯的
初值*,pshared表示是否为多进程共享(不仅仅是用于一个进程)还是线程间共享。LinuxThreads没有实现 多进程共享信号灯,因此所有非0值(进程间共享,其他程序能共享)的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。初始化好的信号灯由sem变 量表征,用于以下点灯、灭灯操作。

int sem_destroy(sem_t * sem)
被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯 注销函数不做其他动作。
sem_destroy destroys a semaphore object, freeing the resources it might hold. No threads should be waiting on the semaphore at the time sem_destroy is called. In the LinuxThreads implementation, no resources are associated with semaphore objects, thus sem_destroy actually does nothing except checking that no thread is waiting on the semaphore.

②点灯与灭灯
int sem_post(sem_t * sem)-----------------点灯(释放信号量,让信号量的值加1。相当于V操作。)
点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。(如果此时有正在等待的线程,则唤醒该线程-该线程拿走了这个资源)

int sem_wait(sem_t * sem)----等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回。如果信号量的值为0,则线程阻塞。相当于P操作。成功返回0,失败返回-1。----阻塞版本
int sem_trywait(sem_t * sem)-----------------------------非阻塞版本
sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1(我拿走了),并返回0。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。如果sem等于0,则一直线程进入睡眠状态,直到信号量值大于0或者超时。

③ 获取灯值
int sem_getvalue(sem_t * sem, int * sval)
读取sem中的灯计数,存于*sval中,并返回0。

④其他

sem_wait()被实现为取消点。(取消点事什么意思???)
sem_wait is a cancellation point.
取消点的含义:
当用pthread_cancel()一个线程时,这个要求会被pending起来,当被cancel的线程走到下一个cancellation point时,线程才会被真正cancel掉。
而且在支持原子”比较且交换CAS”指令的体系结构上,sem_post()是唯一能用于异步信号处理函数的POSIX异步信号 安全的API。
On processors supporting atomic compare-and-swap (Intel 486, Pentium and later, Alpha, PowerPC, MIPS II, Motorola 68k),
the sem_post function is async-signal safe and can therefore be called from signal handlers. This is the only thread syn-
chronization function provided by POSIX threads that is async-signal safe.
On the Intel 386 and the Sparc, the current LinuxThreads implementation of sem_post is not async-signal safe by lack of the required atomic operations.

线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁_第5张图片
具体实例:https://www.cnblogs.com/jiqingwu/p/linux_semaphore_example.html

2.互斥量mutex同步
互斥量的作用犹如给某个对象加上一把锁,每次只允许一个线程去访问它。如果想对代码关键部分的访问进行控制,可以在进入这段代码之前锁定一个互斥量,完成操作之后再解开它。
 互斥量表现互斥现象的数据结构,也被当作二元信号灯。一个互斥基本上是一个多任务敏感的二元信号,它能用作同步多任务的行为,它常用作保护从中断来的临界段代码并且在共享同步使用的资源。
 
线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁_第6张图片
Mutex本质上说就是一把锁,提供对资源的独占访问,所以Mutex主要的作用是用于互斥。Mutex对象的值,只有0和1两个值。这两个值也分别代表了Mutex的两种状态。值为0, 表示锁定状态,当前对象被锁定,用户进程/线程如果试图Lock临界资源,则进入排队等待;值为1,表示空闲状态,当前对象为空闲,用户进程/线程可以Lock临界资源,之后Mutex值减1变为0。

使用互斥量要用到的基本函数与信号量需要使用的函数很相识,同样是4个,对应4个操作,它们的一般形式如下:

创建int pthread_mutex_init(pthread_mutex_tmutex, const pthread_mutexattr_tmutexattr);
加锁int pthread_mutex_lock(pthread_mutex_tmutex));
解锁int pthread_mutex_unlock(pthread_mutex_t
mutex);
销毁int pthread_mutex_destroy(pthread_mutex_t*mutex);

int pthread_mutex_init(pthread_mutex_tmutex, const pthread_mutexattr_tmutexattr);
pthread_mutex_init( )函数用于创建一个互斥量,第一个参数是指向互斥量的数据结构pthread_mutex_t的指针,第二个参数是定义互斥量属性的pthread_mutexattrt结构的指针,它的默认类型是fast。类似于信号量的使用方法。

Mutex被创建时可以有初始值,表示Mutex被创建后,是锁定状态还是空闲状态。在同一个线程中,为了防止死锁,系统不允许连续两次对Mutex加锁(系统一般会在第二次调用立刻返回)。也就是说,加锁和解锁这两个对应的操作,需要在同一个线程中完成。

不同操作系统中提供的Mutex函数:
线程同步=互斥锁+条件变量+信号量+文件锁(文件记录锁和文件锁) 死锁_第7张图片

对一个已经加了锁的互斥量调用pthread_mutex_lock()函数,那么程序本身就会被阻塞;而因为拥有互斥量的那个线程现在也是被阻塞的线程之一,所以互斥量就永远也打不开了,程序将进入死锁状态。

要避免死锁有两种做法:一是让它检测有可能发生死锁的这种现象并返回一个错误;二是让它递归地操作,允许同一个线程加上好几把锁,但前提是以后必须有同等数量的解锁钥匙。程序如下:

3.两种方法对比
Mutex是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的第一个。
Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binary semaphore。

Binary semaphore与Mutex的差异:

  1. mutex要由获得锁的线程来释放(谁获得,谁释放)。而semaphore可以由其它线程释放。

    2.初始状态可能不一样:mutex的初始值是1 ,而semaphore的初始值可能是0(或者为1)。

你可能感兴趣的:(linux)