多线程的互斥锁和条件变量

在Linux下使用C语言编写多线程程序时,条件变量和互斥锁是常用的同步机制,用于确保线程之间的正确协作。以下是它们的基本用法和理解:

一、互斥锁和条件变量的基本用法和理解

1.1 互斥锁(Mutex):

  • 互斥锁用于确保在任何给定时刻只有一个线程可以访问被保护的共享资源,以防止竞态条件(Race Condition)。
  • 互斥锁有两个主要操作:锁定(Lock)和解锁(Unlock)。
  • 锁定互斥锁后,其他线程会被阻塞,直到拥有锁的线程释放它。
  • 在C语言中,你可以使用pthread_mutex_init初始化互斥锁,pthread_mutex_lock锁定它,pthread_mutex_unlock解锁它,最后使用pthread_mutex_destroy销毁它。

1.2 条件变量(Condition Variable):

  • 条件变量用于线程之间的通信和协作,允许线程等待某个特定条件的发生。
  • 条件变量通常与互斥锁一起使用,以确保线程在检查条件和等待条件满足之间的操作是原子的。
  • 条件变量有两个主要操作:等待(Wait)和通知(Signal)。
  • 等待操作使线程等待某个条件的满足,并且会在条件满足或者被其他线程发出的通知后继续执行。
  • 通知操作用于通知等待的线程条件已满足。

1.3 总结

理解互斥锁和条件变量的关键点在于:

  • 互斥锁用于保护共享资源,确保一次只有一个线程能够修改或访问它。

  • 条件变量用于线程之间的协作,使一个线程能够等待另一个线程发出的信号,以便在满足某些条件时继续执行。

二、经典示例

多线程经典的示例是生产者-消费者问题,其中生产者线程生产数据,消费者线程消费数据,它们必须同步以避免竞态条件。

其中,互斥锁用于保护共享缓冲区条件变量用于通知消费者何时可以消费数据。

生产者-消费者示例代码如下:

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

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0; // 记录缓冲区中的数据数量
pthread_mutex_t mutex;
pthread_cond_t not_full, not_empty;

void *producer(void *arg) {
    int item = 1;
    while (1) {
        pthread_mutex_lock(&mutex); // 锁定互斥锁
        while (count == BUFFER_SIZE) { // 如果缓冲区已满,等待
            pthread_cond_wait(&not_full, &mutex);
        }
        buffer[count] = item; // 将数据放入缓冲区
        count++;
        printf("Produced: %d\n", item);
        pthread_cond_signal(&not_empty); // 通知消费者缓冲区不为空
        pthread_mutex_unlock(&mutex); // 解锁互斥锁
        item++;
        sleep(1); // 模拟生产时间
    }
}

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {
            pthread_cond_wait(&not_empty, &mutex);
        }
        int item = buffer[count - 1];
        count--;
        printf("Consumed: %d\n", item);
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
        sleep(2); // 模拟消费时间
    }
}

int main() {
    pthread_t producer_thread, consumer_thread;
    
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);
    
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);
    
    return 0;
}

这个示例演示了一个简单的生产者-消费者问题,其中有一个生产者线程和一个消费者线程,它们共享一个有限大小的缓冲区(buffer)。互斥锁 mutex 用于保护缓冲区的访问,条件变量not_fullnot_empty用于通知生产者何时可以继续生产和消费者何时可以继续消费。

生产者线程生成数据并将其放入缓冲区,消费者线程从缓冲区中取出数据并处理。它们使用条件变量来等待合适的时机来执行这些操作,以避免竞态条件。

此示例中的sleep函数用于模拟生产和消费的时间,以便更容易观察线程的交互。

三、代码详解

3.1 头文件

首先,包括必要的头文件和定义了一些全局变量

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

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int count = 0; // 记录缓冲区中的数据数量
pthread_mutex_t mutex;
pthread_cond_t not_full, not_empty;
  • buffer 是一个大小为5的整数数组,用于表示共享的缓冲区
  • count 用于跟踪缓冲区中的数据数量
  • pthread_mutex_tpthread_cond_t 分别是互斥锁和条件变量的类型

3.2 生产者代码

接着,producer 函数是生产者线程的入口函数。它使用互斥锁来确保在访问共享资源(buffercount)时没有竞争。

void *producer(void *arg) {
    int item = 1;
    while (1) {
        pthread_mutex_lock(&mutex); // 锁定互斥锁
        while (count == BUFFER_SIZE) { // 如果缓冲区已满,等待
            pthread_cond_wait(&not_full, &mutex);
        }
        buffer[count] = item; // 将数据放入缓冲区
        count++;
        printf("Produced: %d\n", item);
        pthread_cond_signal(&not_empty); // 通知消费者缓冲区不为空
        pthread_mutex_unlock(&mutex); // 解锁互斥锁
        item++;
        sleep(1); // 模拟生产时间
    }
}

具体来说:

  • pthread_mutex_lock(&mutex) 锁定互斥锁,防止其他线程同时进入临界区。
  • while (count == BUFFER_SIZE) 检查缓冲区是否已满,如果满了就调用 pthread_cond_wait 进入等待状态,等待消费者线程通知它缓冲区不再满。
  • 一旦条件允许,生产者将数据放入缓冲区,增加 count 计数,并打印出生产的数据。
  • pthread_cond_signal(&not_empty) 通知消费者线程缓冲区不再为空,可以继续消费。
  • 最后,解锁互斥锁 pthread_mutex_unlock(&mutex),以允许其他线程进入临界区。

3.3 消费者代码

然后,consumer 函数是消费者线程的入口函数,它执行类似的操作:

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {
            pthread_cond_wait(&not_empty, &mutex);
        }
        int item = buffer[count - 1];
        count--;
        printf("Consumed: %d\n", item);
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&mutex);
        sleep(2); // 模拟消费时间
    }
}
  • 先锁定互斥锁,确保在访问共享资源之前没有竞争。
  • 然后检查缓冲区是否为空,如果是,就等待条件变量 not_empty 的通知。
  • 一旦条件满足,消费者从缓冲区中取出数据,减少 count 计数,并打印出消费的数据。
  • 最后,通过 pthread_cond_signal(&not_full) 通知生产者线程缓冲区不再满,可以继续生产。

3.4 main函数

最后,main 函数是程序的入口点,它初始化互斥锁和条件变量,然后创建了生产者和消费者线程。通过 pthread_join 等待线程的结束,并在程序结束前销毁互斥锁和条件变量。

int main() {
    pthread_t producer_thread, consumer_thread;
    
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);
    
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);
    
    return 0;
}

3.5 总结

这个示例演示了如何使用互斥锁和条件变量来实现线程之间的协作,确保生产者和消费者线程可以正确地共享和操作缓冲区,同时避免竞态条件。这是一个基本的多线程同步示例,用于理解互斥锁和条件变量的基本概念。在实际应用中,可能需要更复杂的同步和错误处理机制。

四、Q&A

4.1 为什么一个线程上锁了,另一个线程就无法访问,另一个线程怎么知道上锁了?

在程序中人为规定代码,线程只有成功获取锁,才可以访问共享资源!

例如以下模板就是获取锁后才进行操作:

// 在线程中尝试获取锁
if (pthread_mutex_lock(&mutex) == 0) {
    // 成功获取锁,可以访问共享资源
    // ...
    // 释放锁
    pthread_mutex_unlock(&mutex);
} else {
    // 锁已经被其他线程占用,无法访问共享资源
    // 在此处可以选择等待或执行其他操作
}

或者

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&mutex); //等待获取锁,才可以访问共享资源
        ...
        pthread_mutex_unlock(&mutex); //对共享资源操作完,释放锁
        ... //执行自己的逻辑
    }
}

在多线程的线程代码中,执行前首先判断是否能获取锁,获取锁后才能继续执行程序,否则等待。

注意:多线程的应用中通常会有多个锁,每个锁用于不同的目的或保护不同的共享资源,线程需要获取自己需要的锁,锁的数量和种类取决于应用的复杂性和需要。

举一个例子,考虑一个简单的文件系统模拟,其中有多个文件对象,每个文件对象都需要保护以防止并发访问。在这种情况下,可以使用多个锁,每个锁用于保护一个文件对象。这样,不同的线程可以并行访问不同的文件对象,而不会产生争用条件。

当一个线程获取了互斥锁(Mutex)并锁定了共享资源时,其他线程尝试获取相同互斥锁会被阻塞。这是因为互斥锁的主要作用是确保在任何给定时刻只有一个线程可以访问被保护的共享资源,以防止多个线程同时修改它,从而避免竞态条件(Race Condition)。

另一个线程如何知道互斥锁已经被上锁了?这是因为线程在尝试获取锁时,如果锁已经被其他线程占用,那么获取锁的操作会被阻塞,直到互斥锁被释放。在这种情况下,线程会等待在获取锁的地方,无法继续执行。这种等待是线程被动感知锁的状态的方式。

要知道是否成功获取互斥锁,通常线程可以检查获取锁的函数的返回值。在C语言中,使用pthread_mutex_lock来尝试获取锁,它会返回0表示成功获取锁,如果锁已被其他线程占用,则会阻塞等待,并在成功获取锁后返回。

4.2 pthread_cond_wait(&not_empty, &mutex);这里为什么还要用mutex变量这个函数干什么?

pthread_cond_wait 函数用于在等待条件变量的同时,释放互斥锁。这个函数通常与互斥锁一起使用,以实现复杂的线程同步。

当一个线程调用 pthread_cond_wait 时,它会进入等待状态,并且会释放与传递给函数的互斥锁相关联的互斥锁。这是因为等待条件变量的线程期望在某个条件满足时被唤醒,而在等待期间允许其他线程访问互斥锁保护的共享资源。

具体到代码行 pthread_cond_wait(&not_empty, &mutex);

  • &not_empty 是条件变量,它用于等待某个条件的发生。在这个示例中,它用于等待缓冲区不为空。
  • &mutex 是互斥锁,它用于保护共享资源(这里是 buffercount)。在等待条件变量期间,互斥锁会被释放,以允许其他线程访问共享资源。

当其他线程修改了共享资源并且认为某个条件已满足时,它们可以调用 pthread_cond_signalpthread_cond_broadcast 来通知等待的线程条件已经满足,从而唤醒等待的线程。一旦被唤醒,等待的线程会尝试重新获取互斥锁,以继续执行,并检查条件是否真的满足。

所以,pthread_cond_wait 的作用是等待条件变量的信号,同时释放互斥锁以允许其他线程访问共享资源,从而实现线程之间的协作和同步。

4.3 sleep的时间

这里的sleep时间模仿的是实际代码逻辑处理时间,如进行协议解析等。

sleep时间的不同将导致程序处于不同的状态,在生产者-消费者问题示例中,sleep 的时间设置将影响缓冲区的状态线程之间的交互方式

具体来说,不同的 sleep 时间设置可以影响以下方面:

1. 缓冲区状态

  • 如果生产者和消费者的 sleep 时间相对较短,缓冲区可能很快填满或清空,取决于生产速度和消费速度。

  • 如果 sleep 时间较长,可能会导致缓冲区保持在某种状态,例如,生产者和消费者都处于等待状态,或者缓冲区在某个特定数量的数据上保持平衡。

2. 线程交互:

  • 线程的 sleep 时间设置会影响线程之间的交互方式。较短的 sleep 时间可能导致线程频繁地争夺互斥锁,从而增加竞争,但也可能导致较高的上下文切换。

  • 较长的 sleep 时间可能减少线程争夺锁的频率,但也可能导致线程在等待期间浪费时间

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

推荐阅读更多精彩内容