您的位置:首页 > 运维架构 > Linux

【Linux】深入理解线程(线程同步、互斥量mutex、死锁、读写锁、条件变量、信号量)

2017-12-30 21:20 609 查看
一、同步概念

       1、线程同步:

            同步即协同步调,按预定的先后次序运行。

            线程同步,只一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时,其他线程为保证数据一致性,不能调用该功能。

            举例 1 :银行存款 5000。柜台,折:取 3000 ;提款机,卡:取 3000。 剩余:2000。

            举例 2 :内存中 100 字节,线程 T1 欲填入 全 1,线程 T2 欲填入 全 0 。但如果 T1 执行了 50 个字节失去 CPU,T2 执行,会将 T1 写过的内容覆盖。当  T1 再次获得 

CPU 继续从失去 cpu 的位置向后写入 1 ,当执行结束,内存中的 100 字节,既不是全 1,也不是全0。

           产生的现象叫做 “与时间有关的错误” 。为了避免这种数据混乱 ,线程需要同步。

           “ 同步 ” 的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。

            因此,所有 “ 多个控制流,共同操作一个共享资源 ”的情况下,都需要同步。

      2、数据混乱的原因:

            (1)、资源共享(独享资源则不会)

          (2)、调度随机(意味着数据访问会出现随机)

            (3)、线程间缺乏必要的同步机制

二、互斥量 mutex

              Linux 提供了一把互斥锁 mutex( 也称为互斥量 )。

             每个线程在对资源操作前都会尝试先加锁,成功加锁才能操作,操作结束解锁。

             资源还是共享的,线程间也还是竞争的。

             但通过 ” 锁  ” 就将资源的访问变成互斥操作,而后与时间有关的错误也就不会产生了。

                                               


     

             注意:同一时刻,只有一个线程持有该锁。

             当 A 线程对某个全局变量加锁访问,B 锁在访问前尝试加锁,拿不到锁,B阻塞。C 线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

             所以,互斥锁实际上是操作系统提供的一把 “ 建议锁 ”(右称 “ 协同锁 ”),建议程序中有多线程访问共享数据资源的时候使用该机制。但,并没有强制限定。

             因此,即使有了 Mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

     1、主要应用函数:

                  pthread_mutex_init   函数

                  pthread_mutex_destory  函数

                  pthread_mutex_lock  函数

                  pthread_mutex_trylock  函数

                  pthread_mutex_unlock  函数

            以上  5 个函数的返回值都是:成功:返回 0;         失败: 返回错误号

                  pthread_mutex_t   类型,其本质上是一个结构体。

                  pthread_mutex_t mutex;         变量 mutex 只有两种取值 1,0

     2、pthread_mutex_init  函数

                  初始化一个互斥锁(互斥量)------>     初值可看做 1

             

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,                   // 给 mutex 变量动态初始化
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;                         // 给 mutex 变量静态初始化


               参数 1 : 传出参数,调用时应传 &mutex
                               restrict  关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成,不能通过本指针以外的其他变量或指针修改。

               参数 2 : 互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)

               注意:

                        (1)、静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了 static 关键字修饰),可以直接使用宏进行初始化。eg : pthread_mutex_t mutex = 

PTHREAD_MUTEX_INITIALIZER;

                     (2)、动态初始化:局部变量应采用动态初始化。eg:pthread_mutex_init(&mutex);

      3、pthread_mutex_destory 函数

                  销毁一个互斥锁

                   int pthread_mutex_destroy(pthread_mutex_t *mutex);

      4、pthread_mutex_lock 函数

                  加锁。可理解为 mutex -- (或 -1)

                   int pthread_mutex_lock(pthread_mutex_t *mutex);

      5、pthread_mutex_trylock 函数

                  尝试加锁。

                  int pthread_mutex_trylock(pthread_mutex_t *mutex);

     

      6、pthread_mutex_unlock 函数

                    解锁。可理解为 mutex++ (或 +1)       

                    int pthread_mutex_unlock(pthread_mutex_t *mutex);

       7、加锁与解锁

             lock 与 unlock:

                      lock 尝试加锁,如果锁不成功,线程阻塞,阻塞到持有互斥量的线程解锁为止。

                      unlock 主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞,先唤醒。

                      例如:T1 T2 T3 T4  使用一把 互斥锁。T1 加锁成功,其他线程均阻塞,直至 T1 解锁。T1 解锁后,T2,T3,T4均被唤醒,并自动再次尝试加锁。

             lock 与 trylock:

                      lock 加锁失败会阻塞,等待锁释放。

                      trylock 加锁失败直接返回错误号(如:EBUSY),不阻塞。

              小结:

                      在访问共享资源前加锁,访问结束后立即解锁。锁的 “ 粒度 ” 应越小越好。

             互斥锁代码示例:

                   

#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<stdio.h>

pthread_mutex_t mutex;        // 定义锁

void *thd_func(void *arg)
{
srand(time(NULL));

while(1)
{
pthread_mutex_lock(&mutex);
printf("hello ");
sleep(rand()%3);
printf("world\n");
// sleep(rand()%3);
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
return NULL;
}

int main(void)
{
pthread_t tid;
int flag = 5;
srand(time(NULL));
pthread_mutex_init(&mutex,NULL);        // 初始化锁
pthread_create(&tid,NULL,thd_func,NULL);

while(flag--)
{
pthread_mutex_lock(&mutex);   // 加锁
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
//        sleep(rand()%3);
pthread_mutex_unlock(&mutex);   // 解锁
sleep(rand()%3);
}
pthread_cancel(tid);     // 杀死线程
pthread_join(tid,NULL);   // 回收子线程
pthread_mutex_destroy(&mutex);   // 销毁锁
return 0;
}


       8、死锁

                (1)、线程试图对一个线程 A 加锁两次。

                (2)、线程 1 拥有 A 锁,请求获得 B 锁;线程 2 拥有 B 锁,请求获得 A 锁。

               



三、读写锁

              与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。

        1、读写锁状态:

                    (1)、读模式下加锁状态(读锁)

                    (2)、写模式下加锁状态(写锁)

                    (3)、不加锁状态

        2、读写锁特性:

                     (1)、读写锁是 “ 写模式加锁  ” 时,解锁前,所有对该锁加锁的线程都会被阻塞。

                     (2)、读写锁是 “  读模式加锁 ” 时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。

                     (3)、读写锁是 “  读模式加锁 ” 时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式请求。优先满足写模式锁。读

锁、写锁并行阻塞,写锁优先级高。

              读写锁也叫共享 --- 独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式 锁住时,它是以独占模式锁住的 。写独占,读共享。

              读写锁是适用于:对数据结构读的次数大于写的次数。

         

         3、主要应用函数:

          

                 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

                 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

                 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

                 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

                 int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

                 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

                 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

              以上 7 个函数的返回值:  成功返回 0 ,失败返回错误号。

                pthread_rwlock_t 类型,用于定义一个读写锁变量。

                pthread_rwlock_t  rwlock;

         4、pthread_rwlock_init 函数

                       初始化一把读写锁 。           

                                      int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

                       餐数 2 :attr 表示读写锁属性,通常使用 默认属性,传 NULL 即可。    

                       也可以使用静态初始化的方法,初始化条件变量:

                       

         5、pthread_rwlock_destroy 函数

                       销毁一个读写锁变量 

                       int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

         6、读写锁示例程序:

               

/*
3 个线程不定时“写”全局资源, 5 个线程不定时“读” 同一全局资源
*/

#include<unistd.h>
#include<pthread.h>
#include<stdio.h>

int counter;                 // 全局资源
pthread_rwlock_t rwlock;     //  读写锁变量

void thd_write(void *arg)
{
int t;
int i = (int) arg;

while(1)
{
t = counter;
usleep(1000);       // 睡眠 1000 ms

// 设置写锁
pthread_rwlock_wrlock(&rwlock);
printf("========wirte %d:  %lu: counter=%d ++counter=%d\n",i,pthread_self(),t,++counter);

// 设置解锁
pthread_rwlock_unlock(&rwlock);

// 睡眠 5000 ms
usleep(5000);

}

return NULL;
}

void thd_read(void *arg)
{
int i = (int)arg;

while(1)
{
// 设置读锁
pthread_rwlock_rdlock(&rwlock);
printf("----------------------------read %d: %lu: %d\n",i,pthread_self(),counter);

// 设置读锁解锁

pthread_rwlock_unlock(&rwlock);

// 睡眠 900 ms
usleep(900);

}
return NULL;
}

int main(void)
{
pthread_t tid[8];
int i;

// 初始化读写锁
pthread_rwlock_init(&rwlock,NULL);

// 循环创建 3 个线程进行写操作

for( i=0;i<3;i++ )
pthread_create(&tid[i],NULL,thd_write,(void*)i);

// 循环创建 5 个线程进行读操作

for( i=0;i<5;i++ )
pthread_create(&tid[i],NULL,thd_read,(void*)i);

// 回收 8 个线程资源
for( i=0;i<8;i++ )
pthread_join(tid[i],NULL);

// 释放读写锁资源
pthread_rwlock_destroy(&rwlock);

return 0;
}


四、条件变量

             条件变量本身不是锁!但它可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个汇合的场所。

        主要应用函数:

                 pthread_cond_init     函数

                 pthread_cond_destory 函数

                 pthread_cond_wait   函数

                 pthread_cond_timedwait  函数

                 pthread_cond_signal 函数

                 pthread_cond_broadcast  函数

         以上  6  个函数的返回值都是:成功返回 0 ,失败直接返回错误号。

                 pthread_cond_t  类型     用于定义条件变量

                 pthread_cond_t cond;

       1、pthread_cond_init  函数

                  初始化一个条件变量

                       int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

                  参数 2 :attr 表示条件变量属性,通常为默认值,传 NULL即可,也可以使用静态初始化的方法,初始化条件变量。  pthread_cond_t cond 

= PTHREAD_COND_INITIALIZER;

       2、pthread_cond_destroy 函数

                销毁一个调件变量

                int pthread_cond_destroy(pthread_cond_t *cond);

       3、pthread_cond_wait 函数

                 阻塞一个条件变量。

                        int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

                 函数作用:

                                 (1)、阻塞等待条件变量 cond (参数1)满足

                                 (2)、释放已经掌握的互斥锁(解锁互斥量),相当于 pthread_mutex_unlcok(&mutex);

                                         (1),(2)两步为原子操作

                                 (3)、当被唤醒,pthread_cond_wait 返回时,解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);

       4、pthread_cond_timedwait 函数

                  限时等待一个条件变量。

                        int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

                  参数 3 :查看 man sem_timedwait 函数,查看 struct timespec 结构体。

                                   struct timespec {

                                                                 time_t tv_sec;      /* Seconds */

                                                                 long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */

                                                            };

                                  形参abstime : 绝对时间。

                                          如:time(NULL) 返回的就是绝对时间。而 alarm(1)是相对时间,相对当前时间定时 1 秒。

                                                 struct timespec t = {1,0};

                                                 pthread_cond_timedwait(&cond,&mutex,&t);          只能定时到 1970 年 1 月 1 日 00:00:01秒(早已过去)

                                  正确的用法 :

                                                time_t cur = time(NULL);

                                                struct timespec_t;       定义 timespec 结构体变量 t 

                                                t.tv_sec = cur + 1;       定时 1 秒

                                                pthread_cond_timedwait(&cond,&mutex,&t);     传参

        5、pthread_cond_signal 函数

                     唤醒至少阻赛在条件变量上的线程。                                                                                                   

                             int pthread_cond_broadcast(pthread_cond_t *cond);

       

        6、pthread_cond_broadcast 函数       

                     唤醒至少阻赛在条件变量上的线程。    

                             int pthread_cond_signal(pthread_cond_t *cond);

        7、生产者消费者模型

             线程同步的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者的行为,一个模拟消费者的行

为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

/*
借助条件变量模拟 生产者 - 消费者  问题
*/

#include<unistd.h>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>

// 链表作为共享数据,需要被互斥量保护

struct msg
{
struct msg* next;
int num;
};

struct msg* head;
struct msg* mp;

// 静态初始化一个条件变量和一个互斥变量
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;

pthread_mutex_t lock =  PTHREAD_MUTEX_INITIALIZER;

// 消费者线程的操作

void *consumer(void* p)
{
for( ; ; )
{
// 加上互斥锁
pthread_mutex_lock(&lock);
while( head == NULL )           // 头指针为空,说明没有节点
{
pthread_cond_wait(&has_product,&lock);

}
mp = head;
head = mp->next;                // 模拟消费掉一个产品
// 解锁
pthread_mutex_unlock(&lock);
printf("-Consume ---%d\n",mp->num);
free(mp);
sleep( rand() % 5 );
}
}

// 生产者线程的操作

void *producter(void *p)
{
for( ; ; )
{
mp = (struct msg*)malloc(sizeof(struct msg));

mp->num = rand() % 1000 + 1;           // 模拟生产一个产品

printf("-Produce ---%d\n",mp->num);

// 加锁
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;

// 解锁
pthread_mutex_unlock(&lock);

// 将等待在条件变量上的一个线程唤醒

pthread_cond_signal(&has_product);
sleep( rand() % 5 );

}

}

int main(int argc,char *argv[])
{
pthread_t tid,cid;
srand(time(NULL));

pthread_create(&tid,NULL,producter,NULL);
pthread_create(&cid,NULL,consumer,NULL);

pthread_join(tid,NULL);
pthread_join(cid,NULL);
sleep(rand() % 5);
return 0;
}


         8、条件变量的优点:

                     相较于 Mutex 而言,条件变量可以减少竞争。

                     如果直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争资源是无意义的。

有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序的效率。

             

五、信号量

       进化版的互斥锁(1 ---> N )

               由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的某一部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据锁住。这样虽然达到了多

线程操做共享数据时,保证数据的正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。

               信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

       主要应用函数:

                 sem_init  函数

                 sem_destroy函数

                 sem_wait 函数

                 sem_trywait  函数

                 sem_timewait 函数

                 sem_post  函数

          以上 6 个函数的返回值都是: 成功返回 0,失败返回 -1 ,同时设置 errrno。(注意,它们没有 pthread 前缀)

                  sem_t  类型,本质上仍是结构体

                  sem_t sem;   规定信号量 sem 不能 <0 。头文件 <semaphore.h>

       1、信号量的基本操作

             sem_wait :          (a)、信号量大于 0,则信号量 --         ( 类比 pthread_mutex_lock(&lock) )

                  |                       (b)、信号量等于 0 ,造成线程阻塞

                  |

               对应

                 |

                 |

              sem_post : 将信号量 ++,同时唤醒阻塞在信号量上的线程  ( 类比 pthread_mutex_unlock )

      但,由于 sem_t 的实现对用户隐藏,所以所谓的 ++,-- 操作只能通过函数来实现,而不能直接 ++、-- 符号。

               信号量的初值,决定了占用信号量的线程个数。              

        2、sem_init 函数

                    初始化一个信号量

                                int sem_init(sem_t *sem, int pshared, unsigned int value);

                     参数 1 : sem 信号量

                     参数 2: :pshared 取 0 用于线程间;取非 0 ( 一般为 1 )用于进程间

                     参数 3  :value  指定信号量初值

        3、sem_destroy函数

                     销毁一个信号量

                                  int sem_destroy(sem_t *sem);

        4、sem_wait 函数

                      给信号量加锁 --

                                 int sem_wait(sem_t *sem);

         5、sem_post  函数

                     给信号量解锁  ++

                                 int sem_post(sem_t *sem);

         6、sem_trywait 函数

                     尝试对信号量加锁 -- ( 与 sem_wait 的区别类比 lock 与 trylock)

                                 int sem_trywait(sem_t *sem);

         7、sem_timedwait 函数

                       限时阻塞信号量

                                 int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

         8、生产者消费者信号量模型

               

// 信号量实现  生产者 消费者问题

#include<unistd.h>
#include<pthread.h>
#include<stdio.h>
#include<semaphore.h>

#define NUM 5

int queue[NUM];                         // 全局数组实现环形队列
sem_t blank_number,product_number;      // 空格子信号量,产品信号量

// 生产者

void *producer(void *arg)
{
int i = 0;

while(1)
{
sem_wait(&blank_number);        // 生产者将格子数 -- , 为 0 则阻塞等待
queue[i]  = rand() % 1000 + 1;  // 生产一个产品
printf("-------Produce---%d\n",queue[i]);
sem_post(&product_number);      // 将产品数 ++

i = (i+1) % NUM;                // 借助下标实现环形
sleep(rand() % 3 );
}

}

// 消费者

void *consumer(void *arg)
{
int i = 0;
while(1)
{
sem_wait(&product_number);      // 消费者将产品数 -- ,为 0 则阻塞
printf("-Consumer---%d\n",queue[i]);
queue[i] = 0;                   // 消费掉一个产品
sem_post(&blank_number);        // 消费掉产品以后将空格子数 ++

i = (i+1) % NUM;
sleep(rand()%3);

}

}

int main(void)
{
pthread_t pid,cid;
sem_init(&blank_number,0,NUM);      // 初始化空格子信号量为 5
sem_init(&product_number,0,0);      // 产品数  为 0

pthread_create(&pid,NULL,producer,NULL);
pthread_create(&cid,NULL,consumer,NULL);

pthread_join(pid,NULL);
pthread_join(cid,NULL);

sem_destroy(&blank_number);
sem_destroy(&product_number);

return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  多线程
相关文章推荐