首页 > 编程笔记

Linux线程同步详解

多线程程序中,各线程除了可以使用自己的私有资源(局部变量、函数形参等)外,还可以共享全局变量、静态变量、堆内存、打开的文件等资源,这已经在《线程是什么》一节做了详细的讲解,不再过多赘述。

举个例子,编写一个多线程程序模拟“4个售票员共同卖 20 张票”的过程,代码如下所示:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
//全局变量,模拟总的票数
int ticket_sum = 10;
//模拟4个售票员一起售卖票的过程
void *sell_ticket(void *arg){
    int i;
    //4个售票员负责将 10 张票全部卖出
    for (i = 0; i < 10; i++)
    {
        //直至所有票全部卖出,4 个售票员才算完成任务
        if (ticket_sum > 0)
        {
            sleep(1);
            //每个线程代表一个售票员
            printf("%u 卖第 %d 张票\n", pthread_self(), 10 - ticket_sum + 1);
            ticket_sum--;
        }
    }
    return 0;
}

int main(){
    int flag;
    int i;
    void *ans;
    //创建 4 个线程,代表 4 个售票员
    pthread_t tids[4];
    for (i = 0; i < 4; i++)
    {
        flag = pthread_create(&tids[i], NULL, &sell_ticket, NULL);
        if (flag != 0) {
            printf("线程创建失败!");
            return 0;
        }
    }
    sleep(10); // 阻塞主线程,等待所有子线程执行结束
    for (i = 0; i < 4; i++)
    {
        flag = pthread_join(tids[i], &ans);
        if (flag != 0) {
            printf("tid=%d 等待失败!", tids[i]);
            return 0;
        }
    }
    return 0;
}
程序中新建了 4 个子线程,每个线程都可以访问 ticket_sum 全局变量,它们共同执行 sell_ticket() 函数,模拟“4个售票员共同售卖 10 张票”的过程。

假设程序编写在 thread.c 文件中,执行过程如下:

[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
3296569088 卖第 1 张票
3265099520 卖第 2 张票
3286079232 卖第 3 张票
3275589376 卖第 4 张票
3286079232 卖第 5 张票
3265099520 卖第 6 张票
3296569088 卖第 7 张票
3275589376 卖第 8 张票
3286079232 卖第 9 张票
3265099520 卖第 10 张票
3275589376 卖第 11 张票
3296569088 卖第 12 张票
3286079232 卖第 13 张票


程序的执行结果并不唯一,还可能输出如下类似的信息:

1492682496 卖第 1 张票
1503172352 卖第 1 张票
1482192640 卖第 1 张票
1471702784 卖第 1 张票
1503172352 卖第 5 张票
1482192640 卖第 6 张票
1492682496 卖第 6 张票
1471702784 卖第 6 张票
1503172352 卖第 9 张票
1492682496 卖第 9 张票
1471702784 卖第 9 张票
1482192640 卖第 12 张票
1503172352 卖第 13 张票

程序执行过程中,出现了“多个售票员卖出同一张票”以及“4个售票员多卖出 3 张票”的异常情况。造成此类问题的根本原因在于,进程中公有资源的访问权限是完全开放的,各个线程可以随时访问这些资源,程序运行过程中很容易出现“多个线程同时访问某公共资源”的情况。

例如,之所以会出现“多个售票员卖出同一张票”的情况,因为这些线程几乎同一时间访问 ticket_sum 变量,得到的是相同的值。出现“4 个售票员多卖出 3 张票”的原因是:4 个线程访问 ticket_sum 变量得到的都是一个大于 0 的数,每个线程都可以继续执行 if 语句内的代码。由于各个线程先后执行的顺序不同,有的线程先执行ticket_sum--操作,导致其它线程计算10-ticket_sum+1表达式时,读取到的 ticket_num 变量的值为负数,因此表达式的值会出现大于 10 的情况。

我们通常将“多个线程同时访问某一公共资源”的现象称为“线程间产生了资源竞争”或者“线程间抢夺公共资源”,线程间竞争资源往往会导致程序的运行结果出现异常,感到匪夷所思,严重时还会导致程序运行崩溃。

幸运地是,Linux 提供了很多种解决方案,确定各个线程可以同步访问进程提供的公共资源(简称“线程同步”)。所谓线程同步,简单地理解就是:当一个线程访问某公共资源时,其它线程不得访问该资源,它们只能等待此线程访问完成后,再逐个访问该资源。

Linux线程同步的解决方案

Linux 环境中,实现线程同步的常用方法有 4 种,分别称为互斥锁信号量条件变量读写锁

互斥锁(Mutex)又称互斥量或者互斥体,是最简单也最有效地一种线程同步机制。互斥锁的用法和实际生活中的锁非常类似,当一个线程访问公共资源时,会及时地“锁上”该资源,阻止其它线程访问;访问结束后再进行“解锁”操作,将该资源让给其它线程访问。

信号量又称“信号灯”,主要用于控制同时访问公共资源的线程数量,当线程数量控制在 ≤1 时,该信号量又称二元信号量,功能和互斥锁非常类似;当线程数量控制在 N(≥2)个时,该信号量又称多元信号量,指的是同一时刻最多只能有 N 个线程访问该资源。

条件变量的功能类似于实际生活中的门,门有“打开”和“关闭”两种状态,分别对应条件变量的“成立”状态和“不成立”状态。当条件变量“不成立”时,任何线程都无法访问资源,只能等待条件变量成立;一旦条件变量成立,所有等待的线程都会恢复执行,访问目标资源。为了防止各个线程竞争资源,条件变量总是和互斥锁搭配使用。

多线程程序中,如果大多数线程都是对公共资源执行读取操作,仅有少量的线程对公共资源进行修改,这种情况下可以使用读写锁解决线程同步问题。

推荐阅读