跟我学Linux编程-12-多线程编程-同步
- 格式:doc
- 大小:38.00 KB
- 文档页数:3
多线程编程-同步
在上一章节中,我们通过程序示例,见证了单线程世界中不可能发生的事件(一个数既是奇数又是偶数)在多线程环境中是怎样分分钟发生的,我通过细分程序执行步骤,分析了奇异事件发生的过程,并探明了其原因:一个线程在对全局变量gcnt进行两次判读的过程中,另一个线刚好改变了这个变量的值。
在多线程编程术语中,称这两个线程同时进入了临界区域。
所谓临界区域,是指多线程环境下两个及以上线程同时执行可能会导致冲突的一段代码。
在上一章节的示例中,这几行代码就是一个临界区域:
gcnt++;
if (gcnt % 2)
{
if (!(gcnt % 2)) printf("[%d] : %d\n", id, gcnt);
}
冲突之所以会发生,是因为临界区域的代码,通常需要很多个CPU指令周期才能完成,其运行过程随时可能被打断(进行了线程调试),CPU去运行另外的线程,如果这个线程刚好也进入了临界区域,则异常的程序状态极可能会发生。
如果当某个线程进入临界区域,在其退出区域之前,其他的线程无论如何也不能进入该区域,那么冲突就不会发生。
Linux提供了这种保证多线程进入临界区域互斥的机制,这正是本章节所要介绍的内容:线程锁。
我们今天的示例程序还是在上一章节的示例上改进而来的,我们的任务就是使用线程锁,保证“一个数既是奇数又是偶数”的奇异事件在多线程环境下也不发生,代码如下:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int gcnt = 0;
pthread_mutex_t g_mutex;
void *thread_task(void *arg)
{
int id = (int)arg;
while (1)
{
pthread_mutex_lock(&g_mutex);
gcnt++;
if (gcnt % 2)
if (!(gcnt % 2)) printf("[%d] : %d\n", id, gcnt);
}
pthread_mutex_unlock(&g_mutex);
usleep(1);
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t thr;
pthread_mutex_init(&g_mutex, NULL);
pthread_create(&thr, NULL, thread_task, (void *)1);
pthread_create(&thr, NULL, thread_task, (void *)2);
thread_task((void *)0);
return 0;
}
今天的程序相对于上章的代码,改动非常小,只添加了四行,已使用红色加粗标注。
我们一行行来分析:
pthread_mutex_t g_mutex;
这一行我们在程序中定义了一个线程锁对像。
线程锁的类型为pthread_mutex_t,像普通的数据类型一个,它可以表态定义,也可以动态申请。
pthread_mutex_init(&g_mutex, NULL);
我们在main函数的一开始,调用pthread_mutex_init对线程锁进行了初始化。
pthread_mutex_init有两个参数,前者是线程锁对像,以指针的形式传递,第二个参数是线程锁的初始化参数,用于定义线程锁的类型,通常传NULL。
线程锁也可以通过赋初值的形式初始化,上面两行代码等价于如下的一行:
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
我们通过上述方法定义并初始化了一把互斥锁。
除了互斥锁,线程锁还有其它的类型,通过初始化过程确定,如果读者有兴趣,可以查阅相关资料补充学习。
需要提出的是,如果没有特殊需要,我们线程过程中使用这种类型的锁就行了,使用其它类型锁前,请一定一定三思
pthread_mutex_lock(&g_mutex);
pthread_mutex_unlock(&g_mutex);
加锁与解锁,这两行代码通常要成对出现,lock在临界区域的第一行代码之前,unlock紧跟在临界区域的最后一行代码之后,保证了临界区域同一时刻,只有一个线程能够进入。
共原理如下:
1 当线程调用pthread_mutex_lock时,会对线程锁对像进行检测,如果线程锁已被其他线程lock了,则线程一直等待,直至其它线程都调用了unlock释放了该锁。
2 当锁未被其他线程锁定时,pthread_mutex_lock函数标识锁被当前线程锁定,并返回,程序继续向下运行。
我们称之为该线程获得了锁,或者占用了锁。
3 当线程调用pthread_mutex_unlock时,解除本线程对锁的锁定状态,并返回,程序继续向下运行。
我们称之为该线程释入了锁。
当锁被占用线程释放后,其它线程(包括本线程)可以获得该锁。
我们今天的示例程序保存为thread3.c,编译执行:
gcc thread3.c -o thread3 –lpthread
./thread3
大家会发现,程序什么也不输出,这就对了,一个数既是奇数又是偶数,怎么可能!!!!
通过对临界区进行加锁解锁,使得并发运行的线程只能够串行进出临界区,有效避免的多线程对同一资源的访问冲突。
这种临界区串行运行的方式,我们通常称之为线程同步。
需要指出的是,在程序中,针对某一资源的临界区域有可能不是同一断代码,如在上面的例子中,我们可以加一个线程thread_task2,thread_task2对gcnt进行递减,其它代码与thread_task一样。
显然,thread_task2与thread_task中的代码也存在访问gcnt冲突的可能,因此我们的代码中有两处临界区域。
这两处临界区域都使用同一个线程锁通过加锁解决锁的方式保护起来。
线程同步解决了线程竞争导致有程序异常问题,但由于在临界区域的串行运行,一定程度上减弱了线程的并发运行特性,会导致程序性能的下降。
我们依据加锁解锁中间的代码量来度量锁保护的颗粒大小,加锁解码间的代码起多,则锁的颗粒越大,反方,则越小。
显然,颗粒越大的锁,程序中需要串行运行的代码比例高,因此其导致程序性能下降的可能性越高。
所以,我们对程序临界区域的圈定,要尽量小于充分,尽量不要多圈代码,但一定不能少圈一行代码,否则线程锁会生去功效,使得线程异步问题不仅没被彻底解决,反而更不好跟踪了。