Linux线程同步

<span style="font-family: 'PingFang SC'; font-size: 16px; background-color: rgb(255, 255, 255);">在</span><span style="font-size: 16px; line-height: normal; font-family: Arial;">Linux</span><span style="font-family: 'PingFang SC'; font-size: 16px; background-color: rgb(255, 255, 255);">中每一条线程在内存中对应了一个堆栈,我们没调用一个函数的时候,其实就是在内存中执行压入堆栈的操作。</span>

而多线程就是在内存中有多个堆栈。每个堆栈的大小都是固定的,事先在内存中分配好的。当数据压入堆栈的时候,堆栈的顶部就会延伸,直到延伸到边缘。

Linux多线程程序编写的时候,很多时候会遇到这样的报错:stack overflow。它指的就是当前线程分配的堆栈空间已经用完了,内存中堆栈的顶部已经超过了边缘。

解决这种问题的方法有很多,这里就不在赘述。这篇文章主要对Linux的线程同步进行讲解。

Linux多线程一个很头疼的问题就是资源竞争的问题。因为线程和进程不同,一般通过fork产生的子进程会将程序数据进行拷贝,而多个线程之间会共享主线程的资源。所以在Linux多线程编程中就会产生这样的资源竞争的问题。最典型的资源竞争问题就是火车售票:

</pre><pre name="code" class="cpp">while(1){
<span>	</span>if(count>0){
<span>		</span>count--;
<span>	</span>}else{
<span>		</span>break;
<span>	</span>}
}



上面这段伪代码就表示了一个售票窗口做的事情,首先是一个循环,重复执行售票的操作。售票之前需要先判断剩余票数是否大于0,如果还有,就需要售出一张,如果没有了,那么必须退出循环,不能再进行卖票了。

如果只有一个窗口执行这样的卖票操作工作就会很顺利,但是那么多的顾客来买票,只有一个窗口是不够的。为了缓解压力,需要开很多个窗口,也就是需要多个线程执行这段程序。这个时候多线程的问题就凸现出来了。当一个窗口进行余票检测完毕,另一个窗口刚好售出一张票,这个时候前一个窗口进行售票,是在它第一次检测到票数的基础上进行减,那么就会多卖出一张票。最后导致的问题就是火车发车的时候很多人拿着相同ID号的票上了火车。

要想解决这个问题,做法就是将票数的改变这一操作变成原子操作,也就是在售票的过程中,每个线程不能互相干扰。这样进行的动作就是多线程的同步。

在Linux中,实现多线程同步的方法有多种,这里主要介绍三种:

1、互斥锁

2、条件变量

3、信号量


1、互斥锁

互斥锁提供了对共享资源的保护访问。在使用的时候一般有三个步骤,以及它们对应的操作函数:

1)初始化互斥锁 pthread_mutex_init

2)获取/释放互斥锁 pthread_mutex_lock / pthread_mutex_unlock

3)销毁互斥锁 pthread_mutex_destroy

互斥锁的使用方法很简单,使用之前需要初始化。使用的是时候将获取/释放函数写在共享资源前后,最后进行锁的销毁。下面就选择这个过程中的一些细节进行讲解:

初始化:

互斥锁的初始化分为静态初始化和动态初始化。

我们通常使用的pthread_mutex_init()方法就是动态初始化,其实互斥锁在Linux内核中就是一个结构体的数据类型。动态初始化的做法就是malloc一段栈内存来保存数据,因为是手动开辟的,所以最后需要调用pthread_mutex_destroy方法来销毁这段栈内存。

静态分配的写法是pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER,PTHREAD_MUTEX_INITIALIZER是一个宏定义,在一般的Linux系统中他的值是一个结构体:{{0. 0. 0. 0. 0. 0, {0, 0}}},使用这种方式初始化的互斥锁最后并不需要调用destroy方法来释放。

静态初始化和动态初始化在使用上并没有什么区别,唯一的区别就是在使用完毕后的释放上,动态初始化的互斥锁需要调用pthread_mutex_destroy方法来释放。


获取/释放互斥锁:

这个过程中提一下容易混淆的两个函数pthread_mutex_lock / pthread_mutex_trylock。顾名思义,前者是锁、后者是尝试锁。在线程没有获取到锁的情况下,这两个函数的操作结果是一样的,就是获取一把锁;而如果线程先前已经获取了锁,那么lock函数就会阻塞住线程,而trylock会直接返回错误,是非阻塞的,不会阻塞住当前线程。和非阻塞的read系统调用类似,一般我们将这类非阻塞的调用和while连用。


我们用互斥锁最常用的就是下面这种情况

pthread_mutex_lock(&lock);

count++;

pthread_mutex_unlock(&lock);

我们总是理解为互斥锁是对一些变量进行加锁,其实并不是这样的,我们看下面这段代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t lock;

void test(){
	pthread_mutex_lock(&lock);
	printf("thread test\n");
	pthread_mutex_unlock(&lock);
}

int main(void){
	pthread_t t;
	pthread_mutex_init(&lock, NULL);	/** 初始化互斥锁 **/

	pthread_mutex_lock(&lock);		/** 获取锁 **/
	printf("Main lock\n");
	pthread_create(&t, NULL, test, NULL);	/** 新建线程执行test() **/
	sleep(2);
	pthread_mutex_unlock(&lock);		/** 释放锁 **/
	printf("Main unlock\n");

	pthread_mutex_destroy(&lock);		/** 销毁锁 **/
	return 0;
}


在主函数首先动态初始化互斥锁,然后主线程立即获取lock锁,接着开启一个新的线程打印“pthread_test”,睡眠2秒之后进行释放锁。

打印:

Main lock

Main unlock

pthread_test

可以看到打印结果并不是pthread_test穿插在中间。因为pthread_test需要等待lock这把锁释放了之后才能重新获取。这个例子就证明了其实活吃锁保护的并不是变量,而是一段程序。


2、条件变量

条件变量能够实现互斥锁保护共享资源的功能,不过他还是常用在 条件阻塞/条件唤醒这种场合下。意思就是一个线程不满足一定的条件就会阻塞不运行下去,当某个条件满足的时候,当前线程活着其他线程能够去唤醒被阻塞的线程继续运作。

条件变量的使用主要借住四个函数:

pthread_cond_init  初始化

pthread_cond_signal / pthread_cond_wait  唤醒/阻塞

pthread_cond_destroy 销毁

单单看着几个函数肯定能发现条件变量的使用非常简单,不过难点在于条件变量需要搭配互斥锁使用才能实现功能。一般使用格式为:

线程1:

pthread_mutex_lock(&lock);

pthread_cond_wait(&cond, &lock);

pthread_mutex_unlock(&lock);

pthread_mutex_unlock(&lock);

线程2:

if( xxx ){

pthread_cond_signal(&cond);

}

在线程1中阻塞住,当线程2满足了一定条件时,再唤醒线程1。

这里要注意,当调用pthread_cond_wait的时候需要配合互斥锁一起使用。条件变量cond是一个结构体数据,在调用wait函数的时候,回去更改这个数据结构的内容,而这个wait函数本身并不是一个原子操作。当多个线程使用同一个条件变量,需要阻塞的时候,都去调用这个wait函数,那么很可能同时去改变这个条件变量结构体数据内容,如果不加入同步机制很可能就会引发问题。所以在调用这个函数之前需要获取一把互斥锁。这个pthread_cond_wait函数其实内部做了三个操作:

1、释放互斥锁

2、等待唤醒

3、加上互斥锁

我们在函数之前获取了互斥锁,其他线程就不会同时去更改条件变量,直到一个线程阻塞成功,这个时候需要释放掉这个互斥锁,供其他线程可以阻塞。然后阻塞的线程就等待signal函数来唤醒它。等待唤醒过后再将互斥锁加上,执行一段被保护的代码后外面再释放互斥锁。


另外值得提的一点就是pthread_mutex_signal函数只可以唤醒条件变量所在的一个线程,至于是哪条线程是由系统调度决定的。如果有的情况需要唤醒这个条件变量对应的所有线程,就可以调用pthread_cond_broadcast函数。


这里给出一个信号量使用的例子:

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;	/** 初始化互斥锁 **/
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;		/** 初始化条件变量 **/

void test(){
	pthread_mutex_lock(&mut);
	pthread_cond_wait(&cond, &mut);			/** 线程阻塞,等待唤醒 **/

	printf("thread test\n");	
	
	pthread_mutex_unlock(&mut);
	pthread_exit(NULL);
}

int main(void){
	
	pthread_t t;
	int i;

	pthread_create(&t, NULL, test, NULL);		/** 新建线程,执行test() **/
	
	/** 间隔1秒 打印i **/
	for(i=0; i<10; i++){
		printf("%d\n", i);
		if(i == 4){				/** i==4 唤醒线程 **/
			pthread_cond_signal(&cond);
		}
		sleep(1);			
	}
	
	return 0;
}



函数功能就是主函数打印1-10,并且开启一个线程打印”pthread test”。线程是阻塞的,当主线程打印到4的时候就唤醒线程。


3、信号量

信号量和条件变量的功能相似,既可以用来保护一段共享资源,也可以用来 条件阻塞/条件唤醒。这里的信号量和Linux常说的信号不同,信号量是Linux线程同步的机制,而信号则是另一个概念。

在Linux中,信号量有两种:POSIX信号量和SystemV信号量。

在使用上,POSIX信号量比较方便,理解起来也很简单,他常用在Linux线程同步。而SystemV信号量较为复杂,提供了一个信号量集合,常用在进程/线程间的同步。这里就简单介绍一下POSIX信号量:

sem_init 初始化

sem_post / sem_wait 唤醒/阻塞

sem_destroy 销毁

可以通过上面的函数看得出来,POSIX信号量和条件变量非常像。它们唯一的区别就是以上四个函数都是原子操作的函数,也就是说他们在使用的时候 不需要配合互斥锁来使用。

这里给出一个POSIX信号量使用的简单例子:


#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/types.h>

sem_t sem_id;

void test(){

	sem_wait(&sem_id);
	printf("thread test\n");	
	
	pthread_exit(NULL);
}

int main(void){
	
	pthread_t t;
	int i;
	sem_init(&sem_id, 0, 0);			/** 初始化信号量 **/

	pthread_create(&t, NULL, test, NULL);		/** 新建线程,执行test() **/
	
	/** 间隔1秒 打印i **/
	for(i=0; i<10; i++){
		printf("%d\n", i);
		if(i == 4){				/** i==4 唤醒线程 **/
			sem_post(&sem_id);
		}
		sleep(1);			
	}

	pthread_join(&t, NULL);
	sem_destroy(&sem_id);
	return 0;
}



这个例子实现了上面条件变量的功能。


在Linux的POSIX信号量中,还分无名信号量和有名信号量。信号量是一个结构体数据类型,把这个数据存储在内存中的时候,就是无名信号量;当把这个数据存储在文件中的时候,就是有名信号量。以上例子中和我们提到的函数都是无名信号量,也比较常用。无名信号量和有名信号量使用上的区别只是初始化和销毁函数不同,sem_init/sem_destroy是无名信号量的,sem_open/sem_close是有名信号量的。有名信号量因为是保存文件的原因,一般可以用它来进行进程同步。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值