Linux下的线程同步方式

Louis
发布于 2021-04-28 / 2700 阅读 / 0 评论 / 1 点赞

Linux下的线程同步方式

上次讲到进程间通信的四个办法,这次来掰扯一下面试中也会经常问到的线程同步问题。当多个线程需要访问共享的资源的时候,会出现线程同步问题,需要采取某些方式进行资源保护。

不进行资源保护时出现的问题

给出一个多窗口售票问题的场景:

  • 一共有20张票需要被销售
  • 销售窗口有4个
  • 当余票数量大于1时,窗口售出一张票,否则无法售票
#include <iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

//共享资源
int ticket_sum = 20;

void *sell_ticket(void *arg) {
    for (int i = 0; i < 20; i++) {
      //模拟卖票,票没卖完就卖出去一张
        if (ticket_sum > 0) {
            sleep(1);
            cout << "sell the " << 20 - ticket_sum + 1 << "th" << endl;
            ticket_sum--;
        }
    }
    return 0;
}

int main() {
    int flag;
    pthread_t tids[4];
  	//开四个线程(窗口)进行售票
    for (int i = 0; i < 4; i++) {
        flag = pthread_create(&tids[i], nullptr, &sell_ticket, nullptr);
    }
    sleep(20);
    for (int i = 0; i < 4; i++) {
        pthread_join(tids[i], nullptr);
    }
    return 0;
}

程序输出的结果是一共卖出了22张票,出现了超售。造成该问题的原因是当ticket_sum=1时,四个线程同时读到ticket_sum > 0条件达成,进入售票程序,当ticket_sum < 0时没有再次进行判断。

1.在并发情况下,指令执行的先后顺序由内核决定,同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清楚是哪一个先执行,如果运行的结果依赖于不同线程执行的先后的话,那么就会形成竞争条件,在这样的情况下,计算的结果很难预知,所以应该尽量避免竞争条件的形成

2.最常见的解决竞争条件的方法是将原先分离的两个指令构成一个不可分割的原子操作,而其他任务不能插入到原子操作中!

3.对多线程来说,同步指的是在一定时间内只允许某一个线程访问某个资源,而在此时间内,不允许其他线程访问该资源!

4.线程同步的常见方法:互斥锁,条件变量,读写锁,信号量。

互斥锁

本质就是一个特殊的全局变量,拥有lock和unlock两种状态,unlock的互斥锁可以由某个线程获得,一旦获得,这个互斥锁会锁上变成lock状态,此后只有该线程由权力打开该锁,其他线程想要获得互斥锁,必须得到互斥锁再次被打开之后。

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

using namespace std;

int ticket_sum = 20;
pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER;//static init mutex

void *sell_ticket(void *arg) {
    for (int i = 0; i < 20; i++) {
        pthread_mutex_lock(&mutex_x);//atomic opreation through mutex lock
        if (ticket_sum > 0) {
            sleep(1);
            cout << "sell the " << 20 - ticket_sum + 1 << "th" << endl;
            ticket_sum--;
        }
        pthread_mutex_unlock(&mutex_x);
    }
    return 0;
}

int main() {
    pthread_t tids[4];
    for (int i = 0; i < 4; i++) {
        pthread_create(&tids[i], nullptr, &sell_ticket, nullptr);
    }
    sleep(20);
    for (int i = 0; i < 4; i++) {
        pthread_join(tids[i], nullptr);
    }
    return 0;
}

互斥锁的四种类型:

  1. PTHREAD_MUTEX_NOMAL:标准互斥锁,第一次上锁成功,第二次上锁会失败并阻塞

  2. PTHREAD_MUTEX_RECURSIVE:递归互斥锁,第一次上锁成功,第二次上锁还是会成功,可以理解为内部有一个计数器,每加一次锁计数器加1,解锁减1

  3. PTHREAD_MUTEX_ERRORCHECK:检查互斥锁,第一次上锁会成功,第二次上锁出错返回错误信息,不会阻塞

  4. PTHREAD_MUTEX_DEFAULT:默认互斥锁,第一次上锁会成功,第二次上锁会失败

条件变量

互斥量不是万能的,比如某个线程正在等待共享数据内某个条件出现,可可能需要重复对数据对象加锁和解锁(轮询),但是这样轮询非常耗费时间和资源,而且效率非常低,所以互斥锁不太适合这种情况

我们需要这样一种方法:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就换线因等待满足特定条件而睡眠的线程

如果我们能够实现这样一种方法,程序的效率无疑会大大提高,而这种方法正是条件变量!

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

using namespace std;

pthread_cond_t qready = PTHREAD_COND_INITIALIZER;   //cond
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;  //mutex

int x = 10, y = 20;

void *f1(void *arg) {
    cout << "f1 start" << endl;
    pthread_mutex_lock(&qlock);
    while (x < y) {
        pthread_cond_wait(&qready, &qlock);
    }
    pthread_mutex_unlock(&qlock);
    sleep(3);
    cout << "f1 end" << endl;
    return 0;
}

void *f2(void *arg) {
    cout << "f2 start" << endl;
    pthread_mutex_lock(&qlock);
    x = 20;
    y = 10;
    cout << "has a change,x=" << x << " y=" << y << endl;
    pthread_mutex_unlock(&qlock);
    if (x > y) {
        pthread_cond_signal(&qready);
    }
    cout << "f2 end" << endl;
    return 0;
}

int main() {
    pthread_t tids[2];
    pthread_create(&tids[0], nullptr, f1, nullptr);
    sleep(2);
    pthread_create(&tids[1], nullptr, f2, nullptr);
    pthread_join(tids[0], nullptr);
    pthread_join(tids[1], nullptr);
    return 0;
}

条件变量通过运行线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,常常和互斥锁一起使用,使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开响应的互斥锁并等待条件发生变化,一旦其他的某个线程改变了条件变量,它将通知响应的条件变量换线一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并且重新测试条件是否满足。

读写锁

互斥锁在读取变量的时候需要等待获得锁,对于并发的读取并不友好。读写锁可以解决这个问题。

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

using namespace std;

int num = 5;
pthread_rwlock_t rwlock;

void *reader(void *arg) {
    pthread_rwlock_rdlock(&rwlock);
    cout << "reader " << (long) arg << " got the lock" << endl;
    pthread_rwlock_unlock(&rwlock);
    return 0;
}

void *writer(void *arg) {
    pthread_rwlock_wrlock(&rwlock);
    cout << "writer " << (long) arg << " got the lock" << endl;
    pthread_rwlock_unlock(&rwlock);
    return 0;
}

int main() {
    long n = 1, m = 1;
    pthread_t wid, rid;
    pthread_attr_t attr;
    pthread_rwlock_init(&rwlock, nullptr);
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//thread sepatate

    for (int i = 0; i < num; i++) {
        if (i % 3) {
            pthread_create(&rid, &attr, reader, (void *) n);
            cout << "create reader " << n << endl;
            n++;
        } else {
            pthread_create(&wid, &attr, writer, (void *) m);
            cout << "create writer " << m << endl;
            m++;
        }
    }

    sleep(5);//wait other done
    return 0;
}

读写锁的特性:

  1. 当读写锁是写状态时,在锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞

  2. 当读写锁是读状态时,在锁被解锁之前,所有视图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程会被阻塞。

信号量

信号量(sem)和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区。

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

using namespace std;

int num = 10;
sem_t sem;

void *get_service(void *cid) {
    int id = *((int *) cid);
    if (sem_wait(&sem) == 0) {
        sleep(10);
        cout << "customer " << id << " get the service" << endl;
        cout << "customer " << id << " done " << endl;
        sem_post(&sem);
    }
    return 0;
}

int main() {
    sem_init(&sem, 0, 2);
    pthread_t customer[num];
    int flag;

    for (int i = 0; i < num; i++) {
        int id = i;
        flag = pthread_create(&customer[i], nullptr, get_service, &id);
        if (flag) {
            cout << "pthread create error" << endl;
            return flag;
        } else {
            cout << "customer " << i << " arrived " << endl;
        }
        sleep(1);
    }

    //wait all thread done
    for (int j = 0; j < num; j++) {
        pthread_join(customer[j], nullptr);
    }
    sem_destroy(&sem);
    return 0;
}

该代码限制了只能有两个线程进入临界区,否则会等待信号量释放。

尾声

本文主要是为了复习Linux的基本线程库pthread,在C++14中有一些为cpp提供的更好的多线程库,应该是下一篇文章讲解,这里挖个坑

参考网址

Linux多线程同步的四种方式


评论