赞
踩
**线程同步是为了对共享资源的访问进行保护。**这里说的共享资源指的是多个线程都会进行访问的资源,譬如定义了一个全局变量 a,线程 1 访问了变量 a、同样在线程 2 中也访问了变量 a,那么此时变量 a 就是多个线程间的共享资源,大家都要访问它。
**保护的目的是为了解决数据一致性的问题。**这也是为什么我们学习完单片机的裸机基础,需要进一步学习RTOS的原因,除了有多个线程的优点,还有数据的一致性,那我们在Linux学习中再次学习这部分的内容,也充分说明了这部分内容的重要!
当然什么情况下才会出现数据一致性的问题,根据不同的情况进行区分;如果每个线程访问的变量都是其它线程不会读取和修改的(譬如线程函数内定义的局部变量或者只有一个线程访问的全局变量),那么就不存在数据一致性的问题;同样,如果变量是只读的,多个线程同时读取该变量也不会有数据一致性的问题;但是,当一个线程可以修改的变量,其它的线程也可以读取或者修改的时候,这个时候就存在数据一致性的问题,需要对这些线程进行同步操作,确保它们在访问变量的存储内容时不会访问到无效的值。
**出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。**前面给大家介绍了,进程中的多个线程间是并发执行的,每个线程都是系统调用的基本单元,参与到系统调度队列中;对于多个线程间的共享资源,并发执行会导致对共享资源的并发访问,并发访问所带来的问题就是竞争(如果多个线程同时对共享资源进行访问就表示存在竞争,跟现实生活当中的竞争有一定的相似之处,譬如一个队伍当中需要选出一名队长,现在有两个人在候选名单中,那么意味着这两个人就存在竞争关系),并发访问就可能会出现数据一致性问题,所以就需要解决这个问题;要防止并发访问共享资源,那么就需要对共享资源的访问进行保护,防止出现并发访问共享资源。
我们可以编写一个简单地代码对此文件进行测试,示例代码: 2 个线程在常规方式下访问共享资源,这里的共享资源指的就是静态全局变量 g_count。该程序创建两个线程,且均执行同一个函数,该函数执行一个循环,重复以下步骤:将全局变量 g_count 复制到本地变量 l_count 变量中,然后递增l_count,再把 l_count 复制回 g_count,以此不断增加全局变量 g_count 的值。因为 l_count 是分配于线程栈中的自动变量(函数内定义的局部变量),所以每个线程都有一份。循环重复的次数要么由命令行参数指定,要么去默认值 1000 万次,循环结束之后线程终止,主线程回收两个线程之后,再将全局变量 g_count 的值打印出来。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> static int g_count = 0; static void *new_thread_start(void *arg) { int loops = *((int *)arg); int l_count, j; for (j = 0; j < loops; j++) { l_count = g_count; l_count++; g_count = l_count; } return (void *)0; } static int loops; int main(int argc, char *argv[]) { pthread_t tid1, tid2; int ret; /* 获取用户传递的参数 */ if (2 > argc) loops = 10000000; //没有传递参数默认为 1000 万次 else loops = atoi(argv[1]); /* 创建 2 个新线程 */ ret = pthread_create(&tid1, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } ret = pthread_create(&tid2, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } /* 等待线程结束 */ ret = pthread_join(tid1, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } ret = pthread_join(tid2, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } /* 打印结果 */ printf("g_count = %d\n", g_count); exit(0); }
}
编译代码,进行测试,首先执行代码,传入参数 1000,也就是让每个线程对全局变量 g_count 递增 1000次,如下所示:
从打印结果看,得到了我们想象中的结果,每个线程递增 1000 次,最后的数值就是 2000;接着我们把递增次数加大,采用默认值 1000 万次,如下所示:
可以发现,结果竟然不是我们想看到的样子,执行到最后,应该是 2000 万才对,这里其实就出现本结的问题,数据不一致。
现在我们已经有了需要保护线程公共资源的意识,那我们应该如何保护呢?线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享。不过这种便捷的共享是有代价的,必须确保多个线程不会同时修改同一变量、或者某一线程不会读取正由其它线程修改的变量,也就是必须确保不会出现对共享资源的并发访问。Linux 系统提供了多种用于实现线程同步的机制,常见的方法有:互斥锁、条件变量、自旋锁以及读写锁等,下面将向大家一一进行介绍:
互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);
对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。
举一个非常简单容易理解的例子,就拿卫生间(共享资源)来说,当来了一个人(线程)看到卫生间没人,然后它进去了、并且从里边把门锁住(互斥锁上锁)了;此时又来了两个人(线程),它们也想进卫生间方便,发生此时门打不开(互斥锁上锁失败),因为里边有人,所以此时它们只能等待(陷入阻塞);当里边的人方便完了之后(访问共享资源完成),把锁(互斥锁解锁)打开从里边出来,此时外边有两个人在等,当然它们都迫不及待想要进去(尝试对互斥锁进行上锁),自然两个人只能进去一个,进去的人再次把门锁住,另外一个人只能继续等待它出来。
# define PTHREAD_MUTEX_INITIALIZER { { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
所以我们在使用的时候,我们初始化这个宏:已经携带了互斥锁的默认属性
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向需要进行初始化操作的互斥锁对象;
attr:参数 attr 是一个 pthread_mutexattr_t 类型指针,指向一个 pthread_mutexattr_t 类型对象,该对象用于定义互斥锁的属性,若将参数 attr 设置为 NULL,则表示将互斥锁的属性设置为默认值,在这种情况下其实就等价于PTHREAD_MUTEX_INITIALIZER 这种方式初始化,而不同之处在于,使用宏不进行错误检查。
使用 pthread_mutex_init()函数对互斥锁进行初始化示例:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
或者:
pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);
互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock()可以对互斥锁加锁、获取互斥锁,而调用函数 pthread_mutex_unlock()可以对互斥锁解锁、释放互斥锁。其函数原型如下所示:
#include <pthread.h>
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数 mutex :指向互斥锁对象;
调用 pthread_mutex_lock()函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock()会一直阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。
调用 pthread_mutex_unlock()函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:
tips: 如 果 有 多 个 线 程 处 于 阻 塞 状 态 等 待 互 斥 锁 被 解 锁 , 当 互 斥 锁 被 当 前 锁 定 它 的 线 程 调 用pthread_mutex_unlock()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会如愿以偿!
我们已经知道了互斥锁运用的方法,通过上面对数据一致性的问题进行解决:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> //1 static pthread_mutex_t mutex; static int g_count = 0; static void *new_thread_start(void *arg) { int loops = *((int *)arg); int l_count, j; for (j = 0; j < loops; j++) { // 2 pthread_mutex_lock(&mutex); //互斥锁上锁 l_count = g_count; l_count++; g_count = l_count; // 3 pthread_mutex_unlock(&mutex);//互斥锁解锁 } return (void *)0; } static int loops; int main(int argc, char *argv[]) { pthread_t tid1, tid2; int ret; /* 获取用户传递的参数 */ if (2 > argc) loops = 10000000; //没有传递参数默认为 1000 万次 else loops = atoi(argv[1]); /* 初始化互斥锁 */ pthread_mutex_init(&mutex, NULL); /* 创建 2 个新线程 */ ret = pthread_create(&tid1, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } ret = pthread_create(&tid2, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } /* 等待线程结束 */ ret = pthread_join(tid1, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } ret = pthread_join(tid2, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } /* 打印结果 */ printf("g_count = %d\n", g_count); exit(0); }
在测试运行,使用默认值 1000 万次,如下所示:
其实也没有很难嘛,论述了这么多,一共才加了三行代码!!!但我们能看到确实得到了我们想看到的正确结果,每次对 g_count 的累加总是能够保持正确。
当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;调用 pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
我们任然是从上方的代码做更改:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> //1 static pthread_mutex_t mutex; static int g_count = 0; static void *new_thread_start(void *arg) { int loops = *((int *)arg); int l_count, j; for (j = 0; j < loops; j++) { // 2 pthread_mutex_lock(&mutex); //互斥锁上锁 l_count = g_count; l_count++; g_count = l_count; // 3 pthread_mutex_unlock(&mutex);//互斥锁解锁 } return (void *)0; } static int loops; int main(int argc, char *argv[]) { pthread_t tid1, tid2; int ret; /* 获取用户传递的参数 */ if (2 > argc) loops = 10000000; //没有传递参数默认为 1000 万次 else loops = atoi(argv[1]); /* 初始化互斥锁 */ pthread_mutex_init(&mutex, NULL); /* 创建 2 个新线程 */ ret = pthread_create(&tid1, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } ret = pthread_create(&tid2, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } /* 等待线程结束 */ ret = pthread_join(tid1, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } ret = pthread_join(tid2, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } /* 打印结果 */ printf("g_count = %d\n", g_count); exit(0); }
我在标注2的位置,把目标函数替换了,但是我们得到的结果是一样的!
当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy()函数来销毁互斥锁,其函数原型如下所示:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
关于这个函数,特别需要注意的是:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> //1 static pthread_mutex_t mutex; static int g_count = 0; static void *new_thread_start(void *arg) { int loops = *((int *)arg); int l_count, j; for (j = 0; j < loops; j++) { // 2 pthread_mutex_lock(&mutex); //互斥锁上锁 l_count = g_count; l_count++; g_count = l_count; // 3 pthread_mutex_unlock(&mutex);//互斥锁解锁 } return (void *)0; } static int loops; int main(int argc, char *argv[]) { pthread_t tid1, tid2; int ret; /* 获取用户传递的参数 */ if (2 > argc) loops = 10000000; //没有传递参数默认为 1000 万次 else loops = atoi(argv[1]); /* 初始化互斥锁 */ pthread_mutex_init(&mutex, NULL); /* 创建 2 个新线程 */ ret = pthread_create(&tid1, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } ret = pthread_create(&tid2, NULL, new_thread_start, &loops); if (ret) { fprintf(stderr, "pthread_create error: %s\n", strerror(ret)); exit(-1); } /* 等待线程结束 */ ret = pthread_join(tid1, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } ret = pthread_join(tid2, NULL); if (ret) { fprintf(stderr, "pthread_join error: %s\n", strerror(ret)); exit(-1); } /* 打印结果 */ printf("g_count = %d\n", g_count); /* 销毁互斥锁 */ pthread_mutex_destroy(&mutex); exit(0); }
试想一下,如果一个线程试图对同一个互斥锁加锁两次,会出现什么情况?情况就是该线程会陷入死锁状态,一直被阻塞永远出不来;这就是出现死锁的一种情况,除此之外,使用互斥锁还有其它很多种方式也能产生死锁。
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。如下示例代码中所示:
// 线程 A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
// 线程 B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);
至于怎么解决死锁,大家可以在B站上了解一下,后面我有时间也会去了解的,到时候补充回来。。。
如前所述,调用 pthread_mutex_init()函数初始化互斥锁时可以设置互斥锁的属性,通过参数 attr 指定。参数 attr 指向一个 pthread_mutexattr_t 类型对象,该对象对互斥锁的属性进行定义,当然,如果将参数 attr设置为 NULL,则表示将互斥锁属性设置为默认值。
如果不使用默认属性,在调用 pthread_mutex_init()函数时,参数 attr 必须要指向一个 pthread_mutexattr_t对象,而不能使用 NULL。当定义 pthread_mutexattr_t 对象之后,需要使用 pthread_mutexattr_init()函数对该对象进行初始化操作,当对象不再使用时,需要使用 pthread_mutexattr_destroy()将其销毁,函数原型如下所示:
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
互斥锁的类型属性控制着互斥锁的锁定特性,一共有 4 中类型:
PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。
PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次),返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
PTHREAD_MUTEX_DEFAULT : 此 类 互 斥 锁 提 供 默 认 的 行 为 和 特 性 。 使 用 宏PTHREAD_MUTEX_INITIALIZER 初 始 化 的 互 斥 锁 , 或 者 调 用 参 数 arg 为 NULL 的pthread_mutexattr_init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大灵活性, Linux 上 , PTHREAD_MUTEX_DEFAULT 类 型 互 斥 锁 的 行 为 与PTHREAD_MUTEX_NORMAL 类型相仿。
可以使用 pthread_mutexattr_gettype()函数得到互斥锁的类型属性,使用 pthread_mutexattr_settype()修改/设置互斥锁类型属性,其函数原型如下所示:
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
对于 pthread_mutexattr_settype()函数,会将参数 attr 指向的 pthread_mutexattr_t 对象的类型属性设置为参数 type 指定的类型。使用方式如下:
pthread_mutex_t mutex; pthread_mutexattr_t attr; /* 初始化互斥锁属性对象 */ pthread_mutexattr_init(&attr); /* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */ pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); /* 初始化互斥锁 */ pthread_mutex_init(&mutex, &attr); ...... /* 使用完之后 */ pthread_mutexattr_destroy(&attr); pthread_mutex_destroy(&mutex);
本文参考正点原子的嵌入式LinuxC应用编程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。