`
810364804
  • 浏览: 773361 次
文章分类
社区版块
存档分类
最新评论

Linux多线程——使用信号量同步线程

 
阅读更多
信号量、同步这些名词在进程间通信时就已经说过,在这里它们的意思是相同的,只不过是同步的对象不同而已。但是下面介绍的信号量的接口是用于线程的信号量,注意不要跟用于进程间通信的信号量混淆,关于用于进程间通信的信号量的详细介绍可以参阅我的另一篇博文:Linux进程间通信——使用信号量。相似地,线程同步是控制线程执行和访问临界区域的方法

一、什么是信号量
线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。

而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。

二、信号量的接口和使用

信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。

1、sem_init函数
该函数用于创建信号量,其原型如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.

2、sem_wait函数
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:
int sem_wait(sem_t *sem);
sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

3、sem_post函数
该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
int sem_post(sem_t *sem);
与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

4、sem_destroy函数
该函数用于对用完的信号量的清理。它的原型如下:
int sem_destroy(sem_t *sem);
成功时返回0,失败时返回-1.

三、使用信号量同步线程

下面以一个简单的多线程程序来说明如何使用信号量进行线程同步。在主线程中,我们创建子线程,并把数组msg作为参数传递给子线程,然后主线程等待直到有文本输入,然后调用sem_post来增加信号量的值,这样就会立刻使子线程从sem_wait的等待中返回并开始执行。线程函数在把字符串的小写字母变成大写并统计输入的字符数量之后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值。

#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

//线程函数
void *thread_func(void *msg);
sem_t sem;//信号量

#define MSG_SIZE 512

int main()
{
	int res = -1;
	pthread_t thread;
	void *thread_result = NULL;
	char msg[MSG_SIZE];
	//初始化信号量,其初值为0
	res = sem_init(&sem, 0, 0);
	if(res == -1)
	{
		perror("semaphore intitialization failed\n");
		exit(EXIT_FAILURE);
	}
	//创建线程,并把msg作为线程函数的参数
	res = pthread_create(&thread, NULL, thread_func, msg);
	if(res != 0)
	{
		perror("pthread_create failed\n");
		exit(EXIT_FAILURE);
	}
	//输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
	printf("Input some text. Enter 'end'to finish...\n");
	while(strcmp("end\n", msg) != 0)
	{
		fgets(msg, MSG_SIZE, stdin);
		//把信号量加1
		sem_post(&sem);
	}

	printf("Waiting for thread to finish...\n");
	//等待子线程结束
	res = pthread_join(thread, &thread_result);
	if(res != 0)
	{
		perror("pthread_join failed\n");
		exit(EXIT_FAILURE);
	}
	printf("Thread joined\n");
	//清理信号量
	sem_destroy(&sem);
	exit(EXIT_SUCCESS);
}

void* thread_func(void *msg)
{
	//把信号量减1
	sem_wait(&sem);
	char *ptr = msg;
	while(strcmp("end\n", msg) != 0)
	{
		int i = 0;
		//把小写字母变成大写
		for(; ptr[i] != '\0'; ++i)
		{
			if(ptr[i] >= 'a' && ptr[i] <= 'z')
			{
				ptr[i] -= 'a' - 'A';
			}
		}
		printf("You input %d characters\n", i-1);
		printf("To Uppercase: %s\n", ptr);
		//把信号量减1
		sem_wait(&sem);
	}
	//退出线程
	pthread_exit(NULL);
}
运行结果如下:



从运行的结果来看,这个程序的确是同时在运行两个线程,一个控制输入,另一个控制处理统计和输出。

四、分析此信号量同步程序的缺陷
但是这个程序有一点点的小问题,就是这个程序依赖接收文本输入的时间足够长,这样子线程才有足够的时间在主线程还未准备好给它更多的单词去处理和统计之前处理和统计出工作区中字符的个数。所以当我们连续快速地给它两组不同的单词去统计时,子线程就没有足够的时间支执行,但是信号量已被增加不止一次,所以字符统计线程(子线程)就会反复处理和统计字符数目,并减少信号量的值,直到它再次变成0为止。

为了更加清楚地说明上面所说的情况,修改主线程的while循环中的代码,如下:
	printf("Input some text. Enter 'end'to finish...\n");
	while(strcmp("end\n", msg) != 0)
	{
		if(strncmp("TEST", msg, 4) == 0)
		{
			strcpy(msg, "copy_data\n");
			sem_post(&sem);
		}
		fgets(msg, MSG_SIZE, stdin);
		//把信号量加1
		sem_post(&sem);
	}
重新编译程序,此时运行结果如下:



当我们输入TEST时,主线程向子线程提供了两个输入,一个是来自键盘的输入,一个来自主线程复数据到msg中,然后从运行结果可以看出,运行出现了异常,没有处理和统计从键盘输入TEST的字符串而却对复制的数据作了两次处理。原因如上面所述。

五、解决此缺陷的方法

解决方法有两个,一个就是再增加一个信号量,让主线程等到子线程处理统计完成之后再继续执行;另一个方法就是使用互斥量。

下面给出用增加一个信号量的方法来解决该问题的代码,源文件名为semthread2.c,源代码如下:
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

//线程函数
void *thread_func(void *msg);
sem_t sem;//信号量
sem_t sem_add;//增加的信号量

#define MSG_SIZE 512

int main()
{
	int res = -1;
	pthread_t thread;
	void *thread_result = NULL;
	char msg[MSG_SIZE];
	//初始化信号量,初始值为0
	res = sem_init(&sem, 0, 0);
	if(res == -1)
	{
		perror("semaphore intitialization failed\n");
		exit(EXIT_FAILURE);
	}
	//初始化信号量,初始值为1
	res = sem_init(&sem_add, 0, 1);
	if(res == -1)
	{
		perror("semaphore intitialization failed\n");
		exit(EXIT_FAILURE);
	}
	//创建线程,并把msg作为线程函数的参数
	res = pthread_create(&thread, NULL, thread_func, msg);
	if(res != 0)
	{
		perror("pthread_create failed\n");
		exit(EXIT_FAILURE);
	}
	//输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”
	printf("Input some text. Enter 'end'to finish...\n");
	while(strcmp("end\n", msg) != 0)
	{
		//把sem_add的值减1,即等待子线程处理完成
		sem_wait(&sem_add);
		if(strncmp("TEST", msg, 4) == 0)
		{
			strcpy(msg, "copy_data\n");
			sem_post(&sem);
		}
		fgets(msg, MSG_SIZE, stdin);
		//把信号量加1
		sem_post(&sem);
	}

	printf("Waiting for thread to finish...\n");
	//等待子线程结束
	res = pthread_join(thread, &thread_result);
	if(res != 0)
	{
		perror("pthread_join failed\n");
		exit(EXIT_FAILURE);
	}
	printf("Thread joined\n");
	//清理信号量
	sem_destroy(&sem);
	sem_destroy(&sem_add);
	exit(EXIT_SUCCESS);
}

void* thread_func(void *msg)
{
	char *ptr = msg;
	//把信号量减1
	sem_wait(&sem);
	while(strcmp("end\n", msg) != 0)
	{
		int i = 0;
		//把小写字母变成大写
		for(; ptr[i] != '\0'; ++i)
		{
			if(ptr[i] >= 'a' && ptr[i] <= 'z')
			{
				ptr[i] -= 'a' - 'A';
			}
		}
		printf("You input %d characters\n", i-1);
		printf("To Uppercase: %s\n", ptr);
		//把信号量加1,表明子线程处理完成
		sem_post(&sem_add);
		//把信号量减1
		sem_wait(&sem);
	}
	sem_post(&sem_add);
	//退出线程
	pthread_exit(NULL);
}
此时运行结果如下:



分析:这里我们多使用了一个信号量sem_add,并把它的初值赋为1,在主线程在使用sem_wait来等待子线程处理完全,由于它的初值为1,所以主线程第一次调用sem_wait总是立即返回,而第二次调用则需要等待子线程处理完成之后。而在子线程中,若处理完成就会马上使用sem_post来增加信号量的值,使主线程中的sem_wait马上返回并执行紧接下面的代码。从运行结果来看,运行终于正常了。注意,在线程函数中,信号量sem和sem_add使用sem_wait和sem_post函数的次序,它们的次序不能错乱,否则在输入end时,可能运行不正常,子线程不能正常退出,从而导致程序不能退出。

至于使用互斥量的方法,将会在下篇文章:Linux多线程——使用互斥量同步线程中详细介绍。

分享到:
评论

相关推荐

    疯狂内核之——进程管理子系统

    1.2 Linux的线程——轻量级进程 15 1.3 进程的创建——do_fork()函数详解 19 1.4 执行进程间切换 33 1.4.1 进程切换之前的工作 33 1.4.2 进程切换实务 —— switch_to宏 37 1.4.3 __switch_to函数 39 1.5 fork与...

    嵌入式Linux应用程序开发标准教程(第2版全)

    第9章 多线程编程 9.1 Linux线程概述 9.1.1 线程概述 9.1.2 线程机制的分类和特性 9.2 Linux线程编程 9.2.1 线程基本编程 9.2.2 线程之间的同步与互斥 9.2.3 线程属性 9.3 实验内容——“生产者消费者”实验 9.4 本...

    Linux高性能服务器编程

    多线程编程 14.1 Linux线程概述 14.1.1 线程模型 14.1.2 Linux线程库 14.2 创建线程和结束线程 14.3 线程属性 14.4 POSIX信号量 14.5 互斥锁 14.5.1 互斥锁基础API 14.5.2 互斥锁属性 14.5.3 死锁举例 ...

    清华大学Linux操作系统原理与应用

    7.2.3 信号量 156 7.3 并发控制实例 157 7.3.1 内核任务及其并发关系 158 7.3.2 实现机制 158 7.3.3 关键代码解释 162 7.3.4 实现步骤 163 习题7 164 第8章 文件系统 165 8.1 Linux文件系统基础 165 8.1.1 Linux文件...

    Linux程序设计 第4版.haozip01

    12.8 多线程 438 12.9 小结 442 第13章 进程间通信:管道 443 13.1 什么是管道 443 13.2 进程管道 444 13.3 将输出送往popen 445 13.3.1 传递更多的数据 446 13.3.2 如何实现popen 447 13.4 pipe调用 449 ...

    Linux程序设计 第4版.haozip02

    12.8 多线程 438 12.9 小结 442 第13章 进程间通信:管道 443 13.1 什么是管道 443 13.2 进程管道 444 13.3 将输出送往popen 445 13.3.1 传递更多的数据 446 13.3.2 如何实现popen 447 13.4 pipe调用 449 ...

    《操作系统原理与设计》全本

    5.3.4 记录型信号量解决生产者-消费者问题 119 5.3.5 记录型信号量解决读者-写者问题 121 5.3.6 记录型信号量解决理发师问题 123 5.3.7 AND型信号量机制 123 5.3.8 一般型信号量机制 125 5.4 管程 127 5.4.1 管程和...

    一个进程池的服务器程序

    if (write_pid() ) //避免同时有多个该程序在运行 return -1; if (pipe(fd1) ) { perror("pipe failed"); exit(-1); } if (s_pipe(fd2) ) { perror("pipe failed"); exit(-1); } int port = atoi(argv...

    计算机操作系统课程设计报告《生产者---消费者问题》.doc

    年 级: 2010级 小组成员: A B 指导教师: 时 间: 地 点: 2012年 5 月 摘要 生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的...

    C++标准库介绍.pdf

    可移植C多线程库 Python 把C类和映射到Python的中 Pool 内存池管理 smart_ptr 5个智能指针学习智能指针必读份不错参考是来自CUJ文章: Smart Poers in Boost,哦这篇文章可以查到CUJ是提供在线浏览中文版见笔者在Dr....

    操作系统原理 计算机

    1.5.4 自由软件和Linux 操作系统............................................................................................45 1.5.5 IBM系列操作系统.........................................................

Global site tag (gtag.js) - Google Analytics