可以知道,一条语句对一个变量进行+1操作,转成汇编指令共有三条:将这个变量从内存中取出;将其值加1;再将加后的结果放回内存;当一个进程中的两个线程同时进行这个操作时,本来期望的是将变量进行两次加1,但中途有可能当一个线程刚从内存中将变量取出就被切换暂停了,此时线程会保存硬件上下文,第二个线程将变量加1之后前面切出去的线程回来继续执行,这时保存的还是变量原来的值,再将变量加1,会发现变量的最终结果并没有加2而是只加了1,因此这种操作并不是原子的。
-------------------------------------------------------------------------------------------
栗子时间:
上面的程序中创建了两个线程执行同一个函数,都是将全局变量的值从0加到500,预期的结果应该是1000;但是运行程序,会发现结果不为预期那样,有时是500,有时是500多或者600多,这就是两个线程在访问同一块数据代码时产生了冲突,因为操作不是原子的,所以会有中间值的产生。
-------------------------------------------------------------------------------------------
互斥量(mutex)
要解决上面的问题,可以引入互斥量,在解决进程间通信所产生的冲突问题时,有一种机制就是信号量,互斥量和信号量基本上是相同的概念,就是当一个线程在访问某个数据时可以加上一把互斥锁,当别的线程也要访问这个数据时就要请求加锁,而此时锁已经被别的线程申请要过去了,那这个线程就需要等待,直到有锁可用才能再加锁访问数据,如此一来,就可以将“读取―执行―写入”这三部化成一个原子性的问题,要么都执行,要么一步也不执行,不会被中途打断。
互斥锁的初始化和销毁
当定义出了一个pthread_mutex_t类型的互斥锁之后,如果是全局变量或者static变量可以直接将其初始化为PTHREAD_MUTEX_INITIALIZER,相当于调用init的初始化函数来将其初始化;
函数参数中,
mutex是一个指向pthread_mutex_t类型的一个互斥量的指针;
attr可以设置mutex的属性,若为NULL直接用系统默认属性;
两个函数执行成功返回0,失败返回错误码;
b. 加锁与解锁
函数参数为指向互斥量的一个指针;
当一个线程调用函数pthread_mutex_lock时,若锁已被其他线程申请使用,则该线程需要挂起等待,直到锁被使用完pthread_mutex_unlock释放回来,该线程才被唤醒继续申请锁;如果不想未获得锁挂起等待,可以调用pthread_mutex_trylock函数,如果锁已被其他线程获得,这个函数会失败返回EBUSY而不会挂起等待。
-------------------------------------------------------------------------------------------
栗子时间:
在公共的函数中加入了互斥锁,当一个线程进行循环体时,另一个线程要进入就要申请锁,此时锁已将被占用,需要挂起等待直到锁释放,这样两个线程就不会引起冲突而将数据加1的过程转换成原子性的;运行程序会得到结果始终稳定为预期值1000:
上面的栗子中,加锁和解锁是在while循环体外部进行的,也就是一个线程进入函数内部之后申请得到锁,完成了500次加1之后退出循环再释放锁;其实也可以将锁的申请和释放加在循环体的内部,只是这样加锁和释放锁的次数就随着每一次循环而进行一次;因此,加锁的粒度可以依据不同的需求和场景而定。
-------------------------------------------------------------------------------------------
2. 死锁
上面谈论到加锁和解锁,试想,如果一个线程连续两次申请锁,当其第一次申请的时候获得了这把锁,而第二次申请的时候因为锁被占用着会挂起等待,而占用这把锁的正是自身,那么该线程将永远不会释放锁而会一直处于挂起等待的状态;还有,如果线程A获得了一把锁,线程B获得了另一把锁,而线程A还需要再获得线程B所拥有的那把锁才能继续往下执行,同样,线程B也正需要线程A占用的那把锁,但这时两个线程都会挂起等待对方释放那把需要的锁,这样一来两个线程也就一直僵持着处于挂起状态了;像上面所说的这两种情况,也就是两个进程或线程为了争夺资源而造成互相挂起等待,就是死锁。
产生死锁的四个必要条件:
(1) 互斥条件:资源不能被共享,一个资源每次只能被一个进程使用;
(2) 请求与保持条件:已经得到资源的进程可以再次申请新的资源;
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
(4) 循环等待条件:若干进程之间形成环路,环路中的每个进程都在等待相邻进程正占用的资源。
上述说的是进程,对线程也同样适用;
因此,只要避免了上述条件,也就可以避免死锁,在一个线程中应避免同时获得多个锁,如若必须,则有一个原则:如果所有线程在需要多个锁是都按相同的先后顺序获得锁,就不会出现死锁的情况。比如一个线程需要获得锁1、锁2、锁3,那么其他线程也需要按相同的顺序来获得锁;如果不按顺序,也可以调用pthread_mutex_trylock来代替pthread_mutex_lock来获取锁。
-------------------------------------------------------------------------------------------
栗子时间:
上面程序中定义了两个线程分别执行不同的线程函数,在线程1中先获取互斥锁a,然后再获取锁b,在线程2中先获取锁b再获取锁a,执行结果如下:
可以看到程序运行结果就是两个线程都卡住不动了,也就是进入了死锁状态,因为两个线程获取了各自占有了对方所需的锁而导致双方一直僵持挂起着,这也是满足了上面所说的满足了产生死锁的四个必要条件。
将程序中两个线程函数申请获得锁的顺序改为一致,也就是都按先申请锁a再申请锁b的方式,或者将函数thread_mutex_lock改为thread_mutex_trylock,这样就不会产生死锁了。
-------------------------------------------------------------------------------------------
3. 条件变量(condition variable)
前面提到互斥的概念就是某一时段只能有一个进程或线程访问某个资源,而同步就是有顺序性的执行访问某个资源,比如一个线程需要满足某个条件成立才能继续往下执行,条件不成立就会阻塞等待,而此时另一个线程在执行过程中使这个条件成立了,那该线程就会被唤醒继续执行。
pthread库中通过条件变量来阻塞等待一个条件,或者唤醒等待这个条件的线程,
该类型变量的初始化和销毁如下:
和mutex的初始化和销毁类似,当定义了一个类型为pthread_cond_t的条件变量时,如果为全局的或者static类型的可以直接初始化为PTHREAD_COND_INITALIZER,和使用初始化函数一样;
函数参数中,
cond为pthread_cond_t类型的一个指针;
attr同样是设置条件变量的属性,可以为NULL使用默认属性;
b. 条件变量的等待与唤醒
函数参数中,可以看到一个条件变量总是和一个互斥量搭配使用;
当一个线程调用wait函数阻塞等待时,一般会做以下三步操作:
释放mutex
阻塞等待
当被唤醒时,重新获得mutex并返回
而pthread_cond_timedwait函数参数中,abstime可以设定等待的时间,若等待超时任然没有别的线程来唤醒当前线程,就会返回ETIMEDOUT;
可以调用pthread_cond_signal函数来唤醒某个正在等待的线程;
也可以调用pthread_cond_boradcast函数来唤醒所有等待的线程;
-------------------------------------------------------------------------------------------
栗子时间:
上面栗子中,一方作为接受信息方另一方作为发送信息方,当发送方未编辑好信息时接收方需要等待,发送方编辑好信息后唤醒接收方接收信息,运行程序如下:
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
总结:
当一个进程中的多个线程对某个资源进行访问不能保证原子性时,可以使用互斥量来为该资源加上互斥锁保证结果的原子性;
一个线程不能连续两次申请互斥锁,也应该避免多个线程申请多个不同的锁,否则就会出现死锁的问题,避免方法就是:要么所有线程都按照相同的申请锁的顺序申请互斥锁,要么就用非阻塞方式申请锁;
条件变量可以使进程之间同步,一个线程可以满足另一个线程需要的条件而唤醒等待的线程,要和互斥量搭配使用。
《完》