举个例子,编写一个多线程程序模拟“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 个线程访问该资源。
条件变量的功能类似于实际生活中的门,门有“打开”和“关闭”两种状态,分别对应条件变量的“成立”状态和“不成立”状态。当条件变量“不成立”时,任何线程都无法访问资源,只能等待条件变量成立;一旦条件变量成立,所有等待的线程都会恢复执行,访问目标资源。为了防止各个线程竞争资源,条件变量总是和互斥锁搭配使用。
多线程程序中,如果大多数线程都是对公共资源执行读取操作,仅有少量的线程对公共资源进行修改,这种情况下可以使用读写锁解决线程同步问题。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/21421.html