Linux系统编程—多线程互斥

资源竞争导致的问题

以抢票问题为例,下面具体分析一下两个线程的执行情况。见图 1.

图1 抢票问题.png

蓝色框和红色框分别表示不同的线程。
实线表示真正的执行流程,而线程是无法感知到其它线程的存在,故线程自己认为自己是按照虚线流程来走的。
变量 t 是线程自己的局部变量,保存在线程自己的运行栈上。不同线程有各自的运行栈,所以两个线程的变量 t 并不是同一个 t,分别用蓝红颜色表示。

tickets 是一个全局变量,保存在全局数据区。被初始化为 3,两个线程访问到的 tickets 都是同一份(tickets 被称为竞态资源)。

根据图 1 我们很快就能知道原因:因为两个线程对全局变量 tickets 操作需要 3 个步骤,当两个线程对 tickets 的操作产生交叉时,就会产生错误。

如果对 tickets 仅仅只有读而没有写,即使产生步骤上的交叉也不会有问题。但是如果存在写操作,就很容易导致数据的不一致。

为了解决此问题我们希望线程 A 在执行这 3 个动作的时候,线程 B 就不允许执行这 3 个动作。此解决方案,称为多线程互斥。

多线程互斥

多线程互斥即我做某件事的时候你不允许做,而你做某件事的时候也不允许我做。
这个问题如果我们不使用信号量或者 pthread 提供的接口将会很难解决。当然你完全可以自己写一个程序来避免此问题。
也就是 pthread 提供的几把锁:
1、互斥锁 mutex
2、读写锁 rwlock
3、自旋锁 spinlock
“锁”是有它的状态的,即被锁住的状态和打开的状态。而 pthread 线程库中提供的锁也是有状态的。

自旋锁
在多核处理器系统中,系统不允许在不同的CPU上运行的内核控制路径,同时访问某些内核数据结构,在这种情况下如果修改数据结构所需的时间比较短,那么信号量(参考信号量)是低效的。这是系统会使用自旋锁,当一个进程发现锁被另一个进程锁着时,他就不停”旋转”,执行一个紧凑的循环指令直到锁被打开。

但是这种自旋锁在单核处理器下是无效的。当内核控制路径试图访问一个上锁的数据结构,他就开始无休止的循环。因此,内核控制路径可能因为正在修改受保护的数据结构而没有机会继续执行,也没有机会释放这个自旋锁。最后的结果可能是系统挂起。

因为自旋锁是一种多核的内核处理机制,这里我们先不做讨论。

互斥量 mutex

基本概念

为了确保同一时间只有一个线程访问数据,在访问共享资源前需要对互斥量上锁。一旦对互斥量上锁后,任何其他试图再次对互斥量上锁的线程都会被阻塞,即进入等待队列。

上面的文字用伪代码表示:

lock(&mutex);
// 访问共享资源
unlock(&mutex);

有些人可能觉得通过代码很容易实现,其实不然。比方说下面这样:

// 加锁
if (flag == 0) {
    flag == 1;
}
// 访问共享资源
// 解锁
flag == 0;

上面这种做法是错误的,你有没有想过,标记 flag 也是共享资源?

实际上,有一种称之为 peterson 的算法可以解决两线程互斥问题,它的原理并不容易,这里不去做过多介绍。

互斥量的数据类型

pthread 中,互斥量是用 pthread_mutex_t 数据类型表示的,通常它是一个结构体。在使用它前,必须先对它进行初始化。有两种方法可以对它进行初始化:

通过静态分配的方法,将它设置为常量 PTHREAD_MUTEX_INITIALIZER.

使用函数 pthread_mutex_init 进行初始化,如果是用此种方法初始化的互斥量,用完后还需要使用 pthread_mutex_destroy 对其进行回收。

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

在上面的函数中,有两点需要提一下:
(1) restrict 关键字的含义:访问指针 mutex 指针的内容的唯一方法是使用 mutex 指针。通常这是告诉编译器:除了 mutex 指针指向这个内存,再也没别的指针指向这里了。
(2) pthread_mutexattr_t 类型,用来描述互斥量的属性。现阶段,attr 指针默认设置为 NULL.

互斥量的加锁和解锁

// 用于加锁的两个函数
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_lock 函数说的。
如果使用 pthread_mutex_trylock,无论互斥量之前有没有上锁,线程会立即返回而不会阻塞,它是通过返回值来判断是否上锁成功:
如果 pthread_mutex_trylock 返回 0, 表示上锁成功。
如果 pthread_mutex_trylock 返回 EBUSY,表示上锁失败。

所以这两种上锁的函数唯一区别就是一个是阻塞函数,另一个是非阻塞函数。不过通常不使用非阻塞版本的,它会浪费 cpu,除非你别有用意。

解决抢票问题

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

int tickets = 3;
// 使用静态初始化的方式初始化一把互斥锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* zhangsan(void* arg) {
    int flag = 1;
    while(flag) {
        // 上锁
        pthread_mutex_lock(&lock);
        int t = tickets;
        usleep(1000*20);// 20ms
        if (t > 0) {
            printf("张三 buy a ticket\n");
            --t;
            usleep(1000*20);// 20ms
            tickets = t;
        }
        else flag = 0;
        // 解锁
        pthread_mutex_unlock(&lock);
        usleep(1000*20);// 20ms
    }
    return NULL;
}

void* lisi(void* arg) {
    int flag = 1;
    while(flag) {
        // 上锁
        pthread_mutex_lock(&lock);
        int t = tickets;
        usleep(1000*20);
        if (t > 0) {
            printf("李四 buy a ticket\n");
            --t;
            usleep(1000*20);// 20ms
            tickets = t;
        }
        else flag = 0;
        // 解锁
        pthread_mutex_unlock(&lock);
        usleep(1000*20);// 20ms
    }
    return NULL;
}
int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, zhangsan, NULL);
    pthread_create(&tid2, NULL, lisi, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容