Linux多线程编程(二)-----同步与互斥

来源:互联网 发布:淘宝上的宠物狗能买吗 编辑:程序博客网 时间:2024/05/26 05:51

Linux多线程编程(一):http://blog.csdn.net/llzk_/article/details/55670172
由上篇博客我们可以知道,多个线程是共享同一份内存的。这就意味着这么多线程共享同一份数据资源。这时就很可能出现多个线程因竞争同一份资源而发生冲突的问题。就算从程序不挂掉,运行结果也不会太正确。
例如,现在有两个线程A,B分别要对一个数据a = 5执行加1操作。正常来说,a最后的值应该为7。
加1操作共分下面三步:

  1. 将数据从内存单元读入寄存器。
  2. 在寄存器中对变量做增量操作。
  3. 把新的值写回内存单元。

由于加1操作分三步进行, 并不是原子操作(要么不做,要么一次性完成)。所以这就为错误的出现提供了无限的可能。如下图:
这里写图片描述

当线程A正在执行第二步的时候,线程B切入,从内存读到了a的值为5。并执行了加1操作,最后内存中a的值是6,并不是我们想要的7。这与线程B切入的时间有关。
要想的到正确的结果,我们不得不为这些线程加一些限制,就是我们所谓的同步与互斥。

互斥

一份资源在一个时刻内只能被一个进程或线程访问。
比如现在有一个门。门外墙上只挂着一把钥匙。一次只有一个人可以拿着钥匙打开门进去。钥匙也一并被拿进去。此时门外的人只能等待。门内的资源在这段等待时间内只被门内的人一人占用。这就是所谓的互斥。

同步

进程按一定的顺序访问临界资源,同步强调的是协同,一般都在互斥的前提下,但在有些场景下也不需要互斥。
门外的人按照一定的规则按顺序拿钥匙进门就是同步。

互斥量

互斥量,也叫互斥锁。是我们实现同步重要工具。在线程访问共享资源前对互斥量进行设置(加锁),在线程访问共享资源结束后释放互斥量(解锁)。
通过加锁解锁,我们可以将原来的分多步的操作变成一个“原子”操作。系统只会在执行完“锁内”的代码后,才可能会被切去执行其他线程。这个“锁”,也可以理解为上文所描述的“钥匙”。在一个线程占用锁资源时(钥匙),任何其他再试图申请同一份锁资源的线程,都会被阻塞。直到当前线程释放该锁资源。正规来说,对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞,直到当前线程释放该互斥锁。
在互斥锁被释放时,所有因为该锁被阻塞的线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁(获取钥匙),其他线程就会看到锁资源依然被占用,只能回去再次等待。
在这种方式下,我们可以达到一个时刻只有一个线程可以向前执行,并且多线程时会按照一定的顺序进行协同。
这种互斥锁也可以理解为二元信号量

互斥锁操作函数

  • 首先互斥锁的数据类型为pthread_mutex_t。

  • 创建互斥锁,由两种方式。

①调用函数pthread_mutex_init。

int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr)

参数mutex为在外面定义的一个pthread_mutex_t数据类型的数据指 针,init函数调用完成后,互斥锁的值会放在mutex指向的内存单元。要用默认的属性初始化互斥量,就将参数2attr设为NULL
②直接定义一个互斥锁,用下面宏定义给它赋值。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

此方式相当于方式①的attr设为NULL。即默认的属性初始化互斥量。


  • 加锁,解锁。

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);

解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

为了实现互斥锁的操作,大多数体系结构都提供了swap或者exchange指令,该指令的作用是吧寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,及时多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令也只能等待总线周期。

在上面说过,当一个线程已经占用锁资源时,其他线程就会因为得不到锁资源而被阻塞。如果一个线程不想被阻塞的话,可以使用pthread_mutex_trylock尝试对互斥量进行加锁,如果调用时互斥量处于未锁住状态,那么pthread_mutex_trylock将会锁住该互斥量,不会出现阻塞直接返回0。否则就会trylock失败,返回EBUSY。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

  • 有建立锁就有摧毁锁。
int pthread_mutex\_destroy(pthread_mutex_t *mutex);

补充:

  • 在最上面进行举例时(加1操作),线程A在执行过程中被切掉开始执行线程B。我们将这种行为称为线程切换。要发生线程切换一般需要满足以下条件:①当前线程的时间片到达,自动结束 ②操作系统由内核态切换到用户态。一般发生系统调用时,操作系统会从内核态切换到用户态。

还是补充—-死锁

当我们使用锁时,就要避免死锁的现象发生。什么叫死锁?
比如:(还是上面门的例子)A拿到了钥匙,可他忘记他拿到了,所以他就跟其他人一样继续等待钥匙(锁资源)。这时就出现了死锁。
常见死锁出现的原因:

  • 同一个线程申请两次同样的锁资源。
  • 两个线程互相申请彼此锁占有的锁资源。

死锁产生的必要条件:

  • 请求与保持
  • 互斥属性
  • 不可剥夺,抢占
    剥夺:Linux进程线程是可剥夺的(当时间片结束,系统强行剥夺,运行其他程序)
    抢占:当一个进程或线程比正在运行的进程或线程优先级高,则可发生抢占,Linux支持。
  • 环路等待

避免死锁:破坏任意一个死锁产生的必要条件。

实例验证

现有两个线程,分别对一个全局变量做5000次加1操作,为了可以明显观察到到线程切换带来的错误,我们在加1操作中间加上一句printf,printf会调用系统调用write。正确结果应为10000。试一下下列代码的结果:

#include<stdio.h>#include<pthread.h>int count = 0;void* thread_run(void* arg){    int i = 0;    int num = 5000;    int val = 0;    while(i < num)    {        val = count;        printf("val : %d\n",val);        count = val + 1;        i++;    }    return NULL;}int main(){    printf("thread\n");    pthread_t tid1;    pthread_t tid2;    pthread_create(&tid1,NULL,thread_run,NULL);    pthread_create(&tid2,NULL,thread_run,NULL);    //pthread_create()    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    printf("count: %d\n",count);    return 0;}

这里写图片描述

再验证下列代码,加锁之后的代码:

#include<stdio.h>#include<pthread.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;int count = 0;void* thread_run(void* arg){    int i = 0;    int num = 5000;    int val = 0;    while(i < num)    {               pthread_mutex_lock(&mutex);        val = count;        printf("val : %d\n",val);        count = val + 1;        i++;        pthread_mutex_unlock(&mutex);    }    return NULL;}int main(){    printf("thread\n");    pthread_t tid1;    pthread_t tid2;    pthread_create(&tid1,NULL,thread_run,NULL);    pthread_create(&tid2,NULL,thread_run,NULL);    //pthread_create()    pthread_join(tid1,NULL);    pthread_join(tid2,NULL);    pthread_mutex_destroy(&mutex);    printf("count: %d\n",count);    return 0;}

这里写图片描述

1 1
原创粉丝点击