APUE笔记---线程同步之互斥锁、读写锁、条件变量、POSIX信号量

APUE笔记—线程同步之互斥锁、读写锁、条件变量、POSIX信号量

1. 线程要同步的原因

  1. 共享资源,多个线程都可对共享资源操作
  2. 线程操作共享资源的先后顺序不确定
  3. 处理器对存储器的操作一般不是原子操作

下面代码演示线程对修改同一变量发生的问题,首先了解增量操作的具体步骤:
1. 从内存读入寄存器
2. 在寄存器中对变量进行增量操作
3. 把新值写会内存

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

#define NLOOP   5000
int counter;

void *thr_fun(void *arg)
{
        int i, val;
        for(i = 0; i < NLOOP; i++) {
        //下面的三条代码模拟增量操作的过程
                val = counter;
                printf("%d\n", val+1);
                counter = val + 1;
        }

        return NULL;
}

int main()
{
        pthread_t tid1, tid2;

        pthread_create(&tid1, NULL, thr_fun, NULL);
        pthread_create(&tid2, NULL, thr_fun, NULL);

        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);

        return 0;
}

运行结果:

1
2
......
4999
5000

运行结果每次并不一定相同,但是counter的值并不会加到10000。所以,当多个线程对临界资源操作时,线程要同步。

2. 互斥量

基于上述原因,因此可以使用pthread的互斥接口来保护数据,确保同一时间只有一个线程访问数据。

  • 互斥量(mutex)本质上是一把锁,在访问共享资源时对互斥量进行设置(加锁),在访问完成后释放该互斥量(解锁)

2.1 临界区(critical section)

  • 定义:临界区指的是一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待,有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用。
  • 解释:在任意时刻值只允许一个线程对其共享资源进行访问,如果有多个线程试图同时访问临界区,那么在一个线程进入后,其他所有试图访问次临界区的线程将被阻塞,并一直持续到进入临界区的线程离开。临界区在被释放后,其它线程可以继续抢占,并以此达到原子方式操作共享资源的目的。

  • 临界区的选定应该尽可能的小,如果选定太大会影响程序的并行处理性能。

2.2 互斥量实例

互斥量是一个具有加锁和解锁状态的变量,所以它的数据类型用pthread_mutex_t来表示。

2.2.1互斥量的初始化和销毁函数

互斥量的初始化有两种方式。

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//返回值:若成功,返回0,否则返回错误编号。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//将mutex设置成PTHREAD_MUTEX_INITIALIZER初始化的方式只适用于静态分配的互斥量。
2.2.2 加锁和解锁
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//返回值:若成功,返回0,否则返回错误编号。
  • 如果希望线程不被阻塞,pthread_mutex_trylock函数尝试对互斥量进行加锁,如果互斥量处于未加锁状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞直接返回0,否则pthread_mutex_trylock就会失败,无法锁住互斥量,返回EBUSY。

将刚才代码的thr_fun函数加入互斥量如下:

pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;//初始化

void *thr_fun(void *arg)
{
        int i, val;
        for(i = 0; i < NLOOP; i++) {

                pthread_mutex_lock(&counter_mutex);//加锁

                val = counter;
                printf("%d\n", val+1);
                counter = val + 1;

                pthread_mutex_unlock(&counter_mutex);//解锁
        }

        return NULL;
}

此时,运行结果正常,counter计数结果为10000。

2.3 临界区与互斥量的区别。

  1. 首先互斥量是一个处于加锁和解锁这两态之一的变量,而临界区是是一个访问公用资源的程序片段。在上述程序中,互斥量是counter_mutex变量,而临界区是pthread_mutex_lock函数和pthread_mutex_unlock函数之间的代码片段,在这片代码中,访问了公共资源counter。
  2. 互斥量是为在一个时刻只有一个线程对公共资源进行访问而存在的,而临界区是将多线程访问公共资源的操作变为“原子操作”,进而使多个线程实现对公共资源的串行化访问。

3.死锁

死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

4. 读写锁

读写锁(reader-writer lock)与互斥锁类似,不过读写锁允许更高的并行性。读写锁也叫共享互斥锁(shared-exclusive lock),它的特点为:读共享,写互斥。适合于对数据结构读的次数大于写的次数

4.1 读写锁的函数

  • 初始化和销毁函数
#include <pthread.h>

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

//返回值:若成功,返回0,否则返回错误编号。
  • 加锁解锁函数
#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写加锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

//返回值:若成功,返回0,否则返回错误编号。
//trylock的两个函数,成功返回0,失败返回EBUSY。

4.2 读写锁的实例

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

int counter;        //公共资源
pthread_rwlock_t rwlock;    //读写锁

void *th_rd(void *arg)
{
        while(1) {
                pthread_rwlock_rdlock(&rwlock); //读加锁
                printf("read:%d %d\n", (int)pthread_self(), counter);
                pthread_rwlock_unlock(&rwlock); //解锁
                usleep(100);    //睡眠100微秒
        }

        return NULL;
}

void *th_wt(void *arg)
{
        int t;
        while(1) {
                pthread_rwlock_wrlock(&rwlock); //写加锁
                printf("                write:%d %d\n", (int)pthread_self(), counter++);
                pthread_rwlock_unlock(&rwlock); //解锁
                usleep(1000);   //睡眠1000微秒
        }

        return NULL;
}
int main()
{
        pthread_t tid[8];
        pthread_rwlock_init(&rwlock, NULL); //初始化

        for(int i = 0; i < 3; i++) {
                pthread_create(&tid[i], NULL, th_rd, NULL);
        }   //3个读线程
        for(int i = 0; i < 5; i++) {
                pthread_create(&tid[i+3], NULL, th_wt, NULL);
        }   //5个写线程
        pthread_rwlock_destroy(&rwlock);    //释放读写锁

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

        return 0;
}

3个读线程和5个写线程,读线程每次睡眠100微秒,写线程每次睡眠1000微秒。运行结果如下:

...
read:1480128256 2058
read:1496913664 2058
read:1488520960 2058
read:1480128256 2058
                    write:1471735552 2058
                    write:1446557440 2059
read:1496913664 2060
read:1496913664 2060
read:1488520960 2060
read:1480128256 2060
                    write:1463342848 2060
                    write:1438164736 2061
                    write:1454950144 2062
read:1488520960 2063
read:1480128256 2063
read:1488520960 2063
read:1480128256 2063
...

由上述结果可知read模式下,不同的线程可以同时访问被读写锁保护的counter,在write模式下,每次有且只有一个线程对被读写锁保护的counter。

5. 条件变量

条件变量是线程可用的另一种同步机制,条件变量给多个线程提供一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

#include <pthread.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);  //初始化条件变量
int pthread_cond_destroy(pthread_cond_t *cond); //释放条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);    //超时等待条件变量为真
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);  //等待条件变量为真
int pthread_cond_signal(pthread_cond_t *cond);      //唤醒至少一个等待条件的线程
int pthread_cond_broadcast(pthread_cond_t *cond);   //黄兴所有等待条件的线程

//返回值:若成功,返回0,否则返回错误编号。

5.1 条件变量实现一个生产者与一个消费者

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

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

struct msg *head;       //队列头指针
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;  //条件变量
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;       //互斥锁

void *consumer(void *p)
{
        struct msg *mp;

        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 *producer(void *p)
{
        struct msg *mp;

        for(;;) {
                mp = 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(void)
{
        pthread_t pid, cid;

        srand(time(NULL));

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

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

        return 0;
}

生产者和消费者每次睡眠时间不相等,运行结果如下:

Produce 801
Produce 758
Consume 758
Produce 36
Produce 987
Consume 987
Consume 36
Consume 801
Produce 730
Produce 276
Produce 343
Produce 179

当消息队列的头指针(head)为空时,条件不成立,消费者consumer阻塞在pthread_cond_wait,当生产者生产一个消息时,此时头指针不为空,然后调用pthread_cond_signal唤醒消费者,此时head不为NULL时,消费者读数据。

5. POSIX信号量

POSIX信号量也是一种可以实现多线程同步的机制,类似于互斥量,互斥量是只有两个值,解锁和加锁,而信号量可以有多个大于零的值。

5.2 POSIX信号量的函数

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);

//返回值:成功返回0,失败返回-1并设置errno。
  • sem_init函数是初始化信号量,pshared参数如果为0,表示这个信号量是当前进程的局部信号量,否则该信号量可以在多个进程间共享。value参数指定信号量的初始值。
  • sem_destroy函数用于销毁一个信号量。
  • sem_wait函数以原子操作的方式将信号量的值减1,如果信号量为0,则sem_wait被阻塞,知道信号量为非零值。
  • sem_trywait函数是sem_wait非阻塞版本,当信号量为零,返回-1并设置errno为EAGAIN。
  • sem_post函数以原子操作的方式将信号量的值加1,当信号量值大于0,则唤醒调用sem_wait正在等待信号的线程。

5.3 POSIX信号量的函数实现生产者消费者

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

#define NUM 5
int queue[NUM]; //环形队列
sem_t blank_number, product_number;

void *producer(void *arg)
{
        int p = 0;
        while(1) {
                sem_wait(&blank_number);    
                queue[p] = rand() % 1000 + 1;   //生产者生产1到1000随机值
                printf("produce %d\n", queue[p]);
                sem_post(&product_number);
                p = (p+1) % NUM;
                sleep(rand() % 5);
        }
}

void *consumer(void *arg)
{
        int c = 0;
        while(1) {
                sem_wait(&product_number);
                printf("consume %d\n", queue[c]);
                queue[c] = 0;   //消费者将队列值置为零
                sem_post(&blank_number);
                c = (c + 1) % NUM;
                sleep(rand() % 5);
        }
}

int main(void)
{
        pthread_t pid, cid;

        srand(time(NULL));
        sem_init(&blank_number, 0, NUM);
        sem_init(&product_number, 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;
}

运行结果如下:生产者可以连续生产,但是消费者无法过度消费

produce 193
consume 193
produce 834
produce 68
consume 834
produce 646
produce 13
consume 68
produce 382
produce 322
consume 646
produce 398
produce 103
consume 13
produce 124
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值