linux网络编程之posix 线程(三):posix 匿名信号量与互斥锁 示例生产者--消费者问题

一、posix 信号量

信号量的概念参见这里。前面也讲过system v 信号量,现在来说说posix 信号量。

system v 信号量只能用于进程间同步,而posix 信号量除了可以进程间同步,还可以线程间同步。system v 信号量每次PV操作可以是N,但Posix 信号量每次PV只能是1。除此之外,posix 信号量还有命名和匿名之分(man 7 sem_overview):

1、命名信号量

名字以/somename 形式分辨,只能有一个/ ,且总长不能超过NAME_MAX - 4(一般是251)。

需要用sem_open 函数创建或打开,PV操作分别是sem_wait 和 sem_post,可以使用sem_close 关闭,删除用sem_unlink。

命名信号量用于不共享内存的进程间同步(内核实现),类似system v 信号量。

2、匿名信号量

存放在一块共享内存中,如果是线程共享,这块区域可以是全局变量;如果是进程共享,可以是system v 共享内存(shmget 创建,shmat 映射),也可以是 posix 共享内存(shm_open 创建,mmap 映射)。

匿名信号量必须用sem_init 初始化,sem_init 函数其中一个参数pshared决定了线程共享还是进程共享,也可以用sem_post 和sem_wait 进行操作,在共享内存释放前,匿名信号量要先用sem_destroy 销毁。

有关这些函数的具体参数可以man 一下。

二、互斥锁

对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,MutualExclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

Mutex用pthread_mutex_t类型的变量表示,pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attr为NULL则表示缺省属性,具体看结构体:有人建议开发中总是设置 PTHREAD_MUTEX_RECURSIVE 属性,避免死锁。

// 互斥量属性: 同一线程可多次加锁 pthread_mutexattr_t m_attr; pthread_mutexattr_init(&m_attr); pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE);

struct pthread_mutexattr_t
{
    enum lock_type    // 使用pthread_mutexattr_settype来更改
    {
         PTHREAD_MUTEX_TIMED_NP [default]//当一个线程加锁后,其余请求锁的线程形成等待队列,在解锁后按优先级获得锁。
         PTHREAD_MUTEX_ADAPTIVE_NP       // 动作最简单的锁类型,解锁后所有线程重新竞争。
         PTHREAD_MUTEX_RECURSIVE_NP      // 允许同一线程对同一锁成功获得多次。当然也要解锁多次。其余线程在解锁时重新竞争。
         PTHREAD_MUTEX_ERRORCHECK_NP     // 若同一线程请求同一锁,返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP动作相同。
    } type;
} attr;

用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。

一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。

如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

上面的具体函数可以man 一下。

三、生产者消费者问题

生产者消费者问题概念参见这里。下面使用posix 信号量和互斥锁一起来演示:

#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
#include <semaphore.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

#define CONSUMERS_COUNT 1
#define PRODUCERS_COUNT 1
#define BUFFSIZE 10

int g_buffer[BUFFSIZE];

unsigned short in = 0;
unsigned short out = 0;
unsigned short produce_id = 0;
unsigned short consume_id = 0;

sem_t g_sem_full;
sem_t g_sem_empty;
pthread_mutex_t g_mutex;

pthread_t g_thread[CONSUMERS_COUNT + PRODUCERS_COUNT];

void *consume(void *arg)
{
    int i;
    int num = (int)arg;
    while (1)
    {
        printf("%d wait buffer not empty\n", num);
        sem_wait(&g_sem_empty);
        pthread_mutex_lock(&g_mutex);

        for (i = 0; i < BUFFSIZE; i++)
        {
            printf("%02d ", i);
            if (g_buffer[i] == -1)
                printf("%s", "null");
            else
                printf("%d", g_buffer[i]);

            if (i == out)
                printf("\t<--consume");

            printf("\n");
        }
        consume_id = g_buffer[out];
        printf("%d begin consume product %d\n", num, consume_id);
        g_buffer[out] = -1;
        out = (out + 1) % BUFFSIZE;
        printf("%d end consume product %d\n", num, consume_id);
        pthread_mutex_unlock(&g_mutex);
        sem_post(&g_sem_full);
        sleep(1);
    }
    return NULL;
}

void *produce(void *arg)
{
    int num = (int)arg;
    int i;
    while (1)
    {
        printf("%d wait buffer not full\n", num);
        sem_wait(&g_sem_full);
        pthread_mutex_lock(&g_mutex);
        for (i = 0; i < BUFFSIZE; i++)
        {
            printf("%02d ", i);
            if (g_buffer[i] == -1)
                printf("%s", "null");
            else
                printf("%d", g_buffer[i]);

            if (i == in)
                printf("\t<--produce");

            printf("\n");
        }

        printf("%d begin produce product %d\n", num, produce_id);
        g_buffer[in] = produce_id;
        in = (in + 1) % BUFFSIZE;
        printf("%d end produce product %d\n", num, produce_id++);
        pthread_mutex_unlock(&g_mutex);
        sem_post(&g_sem_empty);
        sleep(5);
    }
    return NULL;
}

int main(void)
{
    int i;
    for (i = 0; i < BUFFSIZE; i++)
        g_buffer[i] = -1;

    sem_init(&g_sem_full, 0, BUFFSIZE);
    sem_init(&g_sem_empty, 0, 0);

    pthread_mutex_init(&g_mutex, NULL);


    for (i = 0; i < CONSUMERS_COUNT; i++)
        pthread_create(&g_thread[i], NULL, consume, (void *)i);

    for (i = 0; i < PRODUCERS_COUNT; i++)
        pthread_create(&g_thread[CONSUMERS_COUNT + i], NULL, produce, (void *)i);

    for (i = 0; i < CONSUMERS_COUNT + PRODUCERS_COUNT; i++)
        pthread_join(g_thread[i], NULL);

    sem_destroy(&g_sem_full);
    sem_destroy(&g_sem_empty);
    pthread_mutex_destroy(&g_mutex);

    return 0;
}

这里的程序相比,程序逻辑没太大变化,只是用pthread_mutex_lock 替代了 sem_mutex,其次这里是演示线程间同步,现在上述程序生产者消费者各一个线程,但生产者睡眠时间是消费者的5倍,故消费者会经常阻塞在sem_wait(&g_sem_empty) 上面,因为缓冲区经常为空,可以将PRODUCTORS_COUNT 改成5,即有5个生产者线程和1个消费者线程,而且生产者睡眠时间还是消费者的5倍,从动态输出可以看出,基本上就动态平衡了,即5个生产者一下子生产了5份东西,消费者1s消费1份,刚好在生产者继续生产前消费完。

四、自旋锁和读写锁简介

(一)、自旋锁

自旋锁类似于互斥锁,它的性能比互斥锁更高。 自旋锁与互斥锁很重要的一个区别在于,线程在申请自旋锁的时候,线程不会被挂起,它处于忙等待的状态,一般用于等待时间比较短的情形。 pthread_spin_init pthread_spin_destroy pthread_spin_lock pthread_spin_unlock

(二)、读写锁

1、只要没有线程持有给定的读写锁用于写,那么任意数目的线程可以持有读写锁用于读 2、仅当没有线程持有某个给定的读写锁用于读或用于写时,才能分配读写锁用于写 3、读写锁用于读称为共享锁,读写锁用于写称为排它锁 pthread_rwlock_init pthread_rwlock_destroy int pthread_rwlock_rdlock int pthread_rwlock_wrlock int pthread_rwlock_unlock

更多有关linux中的锁问题可以参考这篇文章 :《透过Linux内核看无锁编程》

http://www.ibm.com/developerworks/cn/linux/l-cn-lockfree/

参考:

《linux c 编程一站式学习》

《UNP》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯云数据库(TencentDB)

【腾讯云CDB】源码分析 · MySQL binlog组提交和Multi-Threaded-Slave

MySQL 5.6引入了基于schema的并行复制,即如果binlog events操作的是不同schema的对象,不是DDL,且操作的对象没有对其他schem...

7791
来自专栏小勇DW3

并发工具箱 concurrent包的原理分析以及使用

BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:

1103
来自专栏开发与安全

linux系统编程之进程(四):wait/waitpid函数与僵尸进程、fork 2 times

一、僵尸进程 当子进程退出的时候,内核会向父进程发送SIGCHLD信号,子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止) 子进程退出时,内核将...

2127
来自专栏Janti

由浅入深理解Java线程池及线程池的如何使用

前言 多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担。线程本身也要占用内存空间,大量的线程会占用内存资源...

7639
来自专栏积累沉淀

线程的分类

1.主线程 main方法。 2.精灵线程 特点: (1)设置为精灵线程的方法:setDaemon(true); (2)其他线程结束了 精灵线程也...

1758
来自专栏Java学习123

数据表或记录被锁住,解锁方法,请大家指教!

2668
来自专栏Java面试通关手册

面试必备之深入理解自旋锁

分享一个我自己总结的Java学习的系统知识点以及面试问题,目前已经开源,会一直完善下去,欢迎建议和指导欢迎Star: https://github.com/Sn...

1933
来自专栏三丰SanFeng

Linux同步机制(一) - 线程锁

1 互斥锁 在线程实际运行过程中,我们经常需要多个线程保持同步。 这时可以用互斥锁来完成任务。互斥锁的使用过程中,主要有 pthread_mutex_init ...

2468
来自专栏小鄧子的技术博客专栏

About ExecutorService(2),自定义线程池

琢磨了一下,还是把这篇提前了,本片篇幅可能会有些长,甚至冗余,请各位看官原谅我这拙劣的写作能力。

632
来自专栏鹅厂少年的奇妙之旅

【春节红包系列】一次"内存泄漏"引发的血案

2017年末,手Q春节红包项目期间,为保障活动期间服务正常稳定,我对性能不佳的Ark Server进行了改造和重写。重编发布一段时间后,结果发现新发布的Svr的...

3169

扫码关注云+社区