一 why
先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题。
在博客《Linux线程同步(一)—“初识篇”》中,介绍了为什么需要线程同步,从本篇博客开始介绍如何实现线程同步,本篇主要介绍的是互斥锁实现线程同步。
二 how
Linux提供一把互斥锁mutex(也称之为互斥量)。
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束后解锁。
资源还是共享的,线程间也还是竞争的,但通过锁将资源的访问变为互斥操作,而后与时间有关的错误也不会在产生了。
同一个时刻,只能有一个线程持有该锁,比如线程A拥有了这把锁,其他线程都无法拥有这把锁。只有获得这把锁的线程才可以访问临界区,其他没有获得这把锁的线程都无法访问临界区。
互斥锁实质上是是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制,但是,并没有强制限定。
2.1 相关函数
互斥锁相关的函数主要有:初始化函数,销毁函数,加锁函数,以及解锁函数
初始化
pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutex_attr *restrict attr)
参数1:传出参数,调用时应传&mutex
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
参数2:互斥属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享).
静态初始化:如果互斥锁mutex是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:局部变量应采用动态初始化。pthread_mutex_init(&mutex, NULL);
销毁
pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁
pthread_mutex_lock(pthread_mutex_t *mutex);
分析:
没有被上锁,当前线程会将这把锁锁上;
被锁上了,当前线程阻塞,锁被打开之后,线程解除阻塞。
pthread_mutex_trylock(pthread_mutex_t *mutex);
分析:
没有锁上:当前线程会给这把锁加锁
如果锁上了:不会阻塞,返回
解锁
pthread_mutex_unlock(pthread_mutex_t *mutex)
注意:
同时将阻塞在该锁上的所有线程全部唤醒
补充知识:
这里介绍一下restrict关键字的用法
restrict是C99新引入的关键字,只用于限定指针,表明本指针是访问一个数据对象的惟一且初始的方式。
三 demo
那么,如何使用上面的几个函数来解决《Linux线程同步(一)—“初识篇”》中的那个问题呢?我们可以在临界区的开始和结束的地方分别加锁,以及解锁即可。这样当两个线程分别访问这段临界区的时候,只有先获得锁的线程才能访问,没有获得锁的线程,会在"加锁"函数位置处等待,以便获得锁,这样线程A和线程B就是以顺序形式来访问临界区
void *pthfun(void *arg)
{
int i = 0;
pthread_mutex_lock(&test_mutex);
for (i = 0; i < MAX_CNT; i++) {
cnt++;
}
pthread_mutex_unlock(&test_mutex);
return NULL;
}
完整函数如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#define MAX_CNT 10000
pthread_mutex_t test_mutex;
int cnt = 0;
void *pthfun(void *arg)
{
int i = 0;
pthread_mutex_lock(&test_mutex);
for (i = 0; i < MAX_CNT; i++) {
cnt++;
}
pthread_mutex_unlock(&test_mutex);
}
int main(int argc, char *argv[])
{
pthread_t pth_id1, pth_id2;
pthread_mutex_init(&test_mutex, NULL);
pthread_create(&pth_id1, NULL, pthfun, NULL);
pthread_create(&pth_id2, NULL, pthfun, NULL);
pthread_join(pth_id1, NULL);
pthread_join(pth_id2, NULL);
if (cnt == 2 * MAX_CNT) {
printf("It is the result as we think, cnt = %d!\n", cnt);
} else {
printf("Error, it is not the result as we think, cnt = %d\n!", cnt);
}
pthread_mutex_destroy(&test_mutex);
return 0;
}
四 测试
编译运行加锁后的函数,发现每次运行结果都是20000
五 源码
只是掌握互斥锁如何使用还不足够,我希望我们作为程序员要对自己要求严格一点,做到知其然并且知其所以然,那么互斥锁的源代码是如何实现的呢?
pthread是POSIX thread,这些函数都是封装在了libpthread库中,使用时,我们只需include对应的头文件“pthread.h”即可调用这些函数。
如果查看这些函数的声明,我们需要安装POSIX接口man手册(sudo apt-get install manpages-posix-dev),安装完成之后,我们即可用“man pthread_mutex_init”来查看此函数的声明部分。
pthread的源代码是在GLIBC中的NPTL包中,NPTL,或称为 NativePOSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。
大部分现代 Linux 发行版都预装了 LinuxThreads 和 NPTL,因此它们提供了一种机制来在二者之间进行切换。要查看您的系统上正在使用的是哪个线程库,请运行下面的命令:
getconf GNU_LIBPTHREAD_VERSION
如下表列出了一些流行的 Linux 发行版,以及它们所采用的线程实现的类型、glibc 库和内核版本。
知道了我们的虚拟机使用了是NPTL后,我们去glibc的官方网站下载glibc的源码:http://ftp.gnu.org/gnu/glibc/,下载合适的版本,比如我们下载的是glibc-2.34.tar.bz2,将这个压缩包传到虚拟机上,并解压,然后我们就可以到glibc-2.34/nptl路径下阅读NPTL的源码了
本篇先介绍到如何使用互斥锁,以及如何下载NPTL包,下一篇我们会阅读NPTL中的实现,来理解互斥锁的底层原理。