linux系统编程——同步——互斥锁和条件变量

1. 互斥锁

互斥锁和条件变量 能用于 线程同步
如果 互斥锁 和 条件变量 存放在 共享内存中,还能 实现 进程同步

1.1 初始化互斥锁

       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
           const pthread_mutexattr_t *restrict attr);
       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 静态分配互斥锁
static pthread_mutex_t lock = PTHREAD_MUTEX_INITALIZER;
// 动态分配
pthread_mutex_t *lock2 = malloc(sizeof(*lock2));
phread_mutex_init()

1.2 上锁

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

若资源已经被上锁,

  • 调用 pthread_mutex_trylock,返回 EBUSY
  • 调用 pthread_mutex_lock,被挂起。若多个线程被挂起,锁释放时,内核将唤醒 优先级最高的线程(所以必须设置不同的优先级给线程)

锁数据,不是函数

1.3 示例

生产者消费者问题
使用共享内存之类需要显示同步的情况

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

#define MAXNITEMS               0x0fffffff
#define MAXNTHREADS             100

int nitems;
struct {
        pthread_mutex_t mutex;
        int buf[MAXNITEMS];
        int nput;
        int nval;
} shared = {
        .mutex = PTHREAD_MUTEX_INITIALIZER
};

void *produce(void *arg);
void *consume(void *arg);

int main(int argc, char *argv[])
{
        int i, nthreads, count[MAXNTHREADS];
        pthread_t tid_produce[MAXNTHREADS], tid_consume;

        if (argc != 3) {
                printf("usage : %s <items> <threads>\n", argv[0]);
                return -1;
        }

        nitems = atoi(argv[1]) > MAXNITEMS ? MAXNITEMS : atoi(argv[1]);
        nthreads = atoi(argv[2]) > MAXNTHREADS ? MAXNTHREADS : atoi(argv[2]);

        for (i = 0; i < nthreads; i++) {
                count[i] = 0;   // 给每个线程一个计数器,记录线程 对共享内存区域操作了多少次
                pthread_create(&tid_produce[i], NULL, produce, &count[i]);
        }

        for (i = 0; i < nthreads; i++) {
                pthread_join(tid_produce[i], NULL);
                printf("count[%d] = %d\n", i, count[i]);
        }

        pthread_create(&tid_consume, NULL, consume, NULL);
        pthread_join(tid_consume, NULL);

        return 0;
}

void *produce(void *arg)
{
        for (;;) {
                pthread_mutex_lock(&shared.mutex);
                if (shared.nput >= nitems) {                    // 所有的区域都写完了,退出
                        pthread_mutex_unlock(&shared.mutex);
                        return NULL;
                }
                shared.buf[shared.nput] = shared.nval;
                shared.nput++;                                          // nput 和 nval 增量都是 1,且初始值都是 0, 所以没有冲突情况下 shared.buf[i] == i
                shared.nval++;
                pthread_mutex_unlock(&shared.mutex);
                *(int *)arg += 1;      // 增加线程操作计数
        }
}

void *consume(void *arg)
{
        int i;

        for (i = 0; i < nitems; ++i) {
                if (shared.buf[i] != i) {                       // 若出现冲突,则输出
                        printf("buf[%d] != %d\n", i, shared.buf[i]);
                }
        }
        return NULL;
}

加锁输出
[root@VM-0-12-centos test]# ./a.out 1000000000 5
count[0] = 54572343
count[1] = 53444696
count[2] = 47935356
count[3] = 56164561
count[4] = 56318499
不加锁
[root@localhost test]# ./a.out 10000 4
count[0] = 8596
count[1] = 8192
count[2] = 0
count[3] = 0
buf[8978] != 8977
buf[9152] != 9151

对代码进行修改,让消费者和生产者一起工作

void consume_wait(int i)
{
        for (;;) {
                pthread_mutex_lock(&shared.mutex);
                if (i < shared.nput) {
                        pthread_mutex_unlock(&shared.mutex);
                        return;
                }
                pthread_mutex_unlock(&shared.mutex);
        }
}

void *consume(void *arg)
{
        int i;

        for (i = 0; i < nitems; ++i) {
                consume_wait(i);        // 消费 下标为 i 的元素前,检查是否被生产
                if (shared.buf[i] != i) {                       // 若出现冲突,则输出
                        printf("buf[%d] != %d\n", i, shared.buf[i]);
                }
        }
        return NULL;
}

1.4 互斥锁的不足

生产者使用轮询的方式检查条件,浪费cpu,所以希望 能阻塞 直到 条件满足。
这需要条件变量。

2. 条件变量

互斥锁用于 上锁,条件变量用于等待
条件变量类型:pthread_cond_t

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

pthread_cond_wait,用于等待条件为真,指定相关条件变量的地址,和关联互斥锁的地址
pthread_cond_signal,与信号无关,用于通知条件已为真

2.1 使用

生产者 需要生产缓存,这里是 shared变量,由于多个生产者都要操作此变量,所以shared需要关联锁。
生产消费行为 需要同步,先有生产在有消费,所以需要计数器 nready.nready,而两个线程都要操作此计数器,所以需要锁 nready.mutex,若 消费者通过阻塞在锁上实现的同步 本质 还是轮询,所以需要通知机制,所以需要 条件变量 nready.cond,让消费者能阻塞在 条件

struct {
        pthread_mutex_t mutex;
        int buf[MAXNITEMS]; // 存放生产者输出数据的缓存
        int nput;           // next 生产数据缓存 索引
        int nval;           // next 生产数据的值得因子
} shared = {
        .mutex = PTHREAD_MUTEX_INITIALIZER
};

struct {
        pthread_mutex_t mutex;
        pthread_cond_t cond;
        int nready;
} nready = {
        .mutex = PTHREAD_MUTEX_INITIALIZER, 
        .cond = PTHREAD_COND_INITIALIZER
};
void *produce(void *arg)
{
        for (;;) {
                pthread_mutex_lock(&shared.mutex);
                if (shared.nput >= nitems) {                    // 所有的区域都写完了,退出
                        pthread_mutex_unlock(&shared.mutex);
                        return NULL;
                }
                shared.buf[shared.nput] = shared.nval;
                shared.nput++;                                  // nput 和 nval 增量都是 1,且初始值都是 0, 所以没有冲突情况下 shared.buf[i] == i
                shared.nval++;
                pthread_mutex_unlock(&shared.mutex);

                pthread_mutex_lock(&nready.mutex);
                if (nready.nready == 0)
                        pthread_cond_signal(&nready.cond);
                nready.nready++;
                pthread_mutex_unlock(&nready.mutex);

                *(int *)arg += 1;
        }
}

void *consume(void *arg)
{
        int i;

        for (i = 0; i < nitems; ++i) {
                pthread_mutex_lock(&nready.mutex);
                while (nready.nready == 0)
                        pthread_cond_wait(&nready.cond, &nready.mutex);
                nready.nready--;
                pthread_mutex_unlock(&nready.mutex);

                if (shared.buf[i] != i) {                       // 若出现冲突,则输出
                        printf("buf[%d] != %d\n", i, shared.buf[i]);
                }
        }
        return NULL;
}

pthread_cond_wait :

  • 调用时,完成两个工作
    释放锁
    线程进入睡眠,直到内核收到相关条件变量的通知操作,而唤醒线程
  • 返回时(被通知唤醒),完成工作
    尝试获得锁,若失败,阻塞在锁上

由于 pthread_cond_wait 返回前会尝试获得锁,若此时锁被 其他线程持有,被唤醒且被调度的线程将再次进入阻塞,为避免这种情况,可修改上面代码。

int dosignal;

phtread_mutex_lock(&nready.mutex);
dosignal = (nready.nready == 0);
nready.nready++;
phtread_mutex_unlock(&nready.mutex);

if (dosignal)
  pthread_cond_signal(&nready.cond);

2.2 总结

条件变量的使用大致如下:

struct {
        pthread_mutex_t mutex;
        pthread_cond_t cond;
        维护本条件的各个变量
} var = {
        .mutex = PTHREAD_MUTEX_INITIALIZER, 
        .cond = PTHREAD_COND_INITIALIZER
};

pthread_mutex_lock(&var.mutex);
设置条件为真
pthread_cond_signal(&var.cond);
pthread_mutex_unlock(&var.mutex);

pthread_mutex_lock(&var.mutex);
while (条件为假)
  pthread_cond_wait(&var.cond, &var.mutex);
修改条件
pthread_mutex_unlock(&var.mutex);

2.4 定时等待和广播

pthread_cond_signal 只唤醒一个线程,若要唤醒多个线程,应使用 pthread_cond_broadcast。

  • 为什么需要唤醒多个线程
    如生产消费者模型,当生产者完成的生产足够满足多个消费者,则应该唤醒多个线程
  • 推荐始终使用广播
    使用单播能优化程序,但由于必须明确每个等待线程,且唤醒哪个等待线程无关紧要。所以使用广播更安全,简单。
       int pthread_cond_broadcast(pthread_cond_t *cond);

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

struct timespec {
  time_t tv_sec;  // sec
  time_t tv_nsec; // nanosec

pthread_cond_timedwait 可以设置超时时间,若超时则返回 ETIME。
超时是绝对时间,不是相对时间。

3. 互斥量和条件变量属性

3.1 静态初始化

用于线程间同步,我们使用 常量值 PTHREAD_MUTEX_INITIALIZER 和 PTHREAD_COND_INITIALIZER 初始化 互斥量 和 条件变量。
这种方式让其 使用默认属性。

       pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

3.2 动态初始化

使用如下函数,初始化和销毁

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

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

attr参数若为NULL,则使用默认值初始化。

通过如下函数 初始化和销毁 attr

       int pthread_mutexattr_init(pthread_mutexattr_t *attr);
       int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

       int pthread_condattr_destroy(pthread_condattr_t *attr);
       int pthread_condattr_init(pthread_condattr_t *attr);

3.3 启用属性

一旦 互斥锁属性对象 条件变量属性对象 已初始化,就调用 不同函数启用或禁止特定属性。
如开启进程间共享的函数

      int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);
      int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int value);

      int pthread_condattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);
      int pthread_condattr_setpshared(pthread_mutexattr_t *attr, int value);   

value 的值可以是:
PTHREAD_PROCESS_PRIVATE 或 PTHREAD_PROCESS_SHARED

示例

pthread_mutex_t *mptr;
pthread_mutexattr_t mattr;

mptr = /* 进程间共享内存区 分配的 pthread_mutex_t 对象 */
pthread_mutexattr_init(&mattr);
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(mptr, &mattr);

4. 持有锁期间进程终止

一个进程或线程持有锁,若持有锁期间被终止,锁是否被系统自动释放?

4.1 进程间共享

  • 互斥锁 : 不会
  • 读写锁 : 不会
  • fnctl记录锁 : 进程终止时,内核自动清理锁
  • systemV信号量 :应用程序可以选择进程终止时,内核是否自动清理某个信号量(SEM_UNDO)
  • POSIX信号量 : 不会

4.2 线程间共享

当一个线程持有锁时,其可能退出,如 另一个线程取消他,或他自己调用 pthread_exit。

  • 自愿(pthread_exit):线程应该知道自己持有锁,应该自己释放
  • 非自愿(另一个线程取消):线程可以安装退出清理函数释放持有锁

若由于段错误等原因导致整个进程退出,就回到进程间共享情况

4.3 内核释放锁的意义

若进程退出,锁由内核释放,通常是没意义的,因为同步区数据是脏的。

但以下情况,内核释放锁是有意义的:

  • 服务器可能使用一个 systemv信号量(打开SEM_UNDO)来统计当前处理客户数,如 fork一个子进程,信号量加一,子进程终止时,内核将信号量减一
  • 守护进程希望系统只运行一个实例,守护进程在开始时在自己某个数据文件获得写入锁,然后运行期间持有锁,若有人试图启动守护进程的另外副本,则新副本由于无法持有锁而退出,若守护进程终止时,内核释放写入锁,从而允许启动该守护进程的副本。

posted on 2021-09-03 00:01  开心种树  阅读(158)  评论(0编辑  收藏  举报