同步与互斥

背景介绍

临界资源:执行流的运行所共享的资源叫做临界资源。
临界区:每个执行流内部访问临界区的代码称为临界区。
互斥操作:在任意一个时刻下,只允许一个执行流进入临界区,访问临界资源,通常对临界资源起到保护的作用。
原子性:不会被任何调度机制打断的操作,该操作要么执行,要么不执行。
(说明:这里的执行流可以理解成线程或者进程,一个进程里面可以含有多个线程,以下全部都用线程来代表执行流)

互斥量:在大部分的情况下,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,在这种的情况下,该变量属于单个线程。但有时候很多变量都需要在线程之间共享,这样的变量称为共享变量。这里注意一下,全局变量≠共享变量)

下面具体来分析下面的代码:

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

int ticket = 30; //定义一个共享变量

void *sell(void *arg){
	char *id = (char*)arg;
	for( ; ;){
		if(ticket > 0){
			usleep(1000);
			printf("%s sell ticket: %d\n", id, ticket);
			ticket--;
		} else{
			break;
		}
	}
}

int main() {
	pthread_t t1, t2, t3, t4; //定义四个线程
	pthred_create(&t1, NULL, sell, "thread 1");
	pthred_create(&t2, NULL, sell, "thread 2");
	pthred_create(&t3, NULL, sell, "thread 3");
	pthred_create(&t4, NULL, sell, "thread 4");
	
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
	return 0;
}

结果如下:

thread 1 sell ticket: 26
thread 1 sell ticket: 22
thread 4 sell ticket: 22
thread 2 sell ticket: 22
thread 3 sell ticket: 22
thread 3 sell ticket: 18
thread 1 sell ticket: 18
thread 2 sell ticket: 18
thread 4 sell ticket: 18
thread 3 sell ticket: 14
thread 2 sell ticket: 14
thread 1 sell ticket: 14
thread 4 sell ticket: 14
thread 3 sell ticket: 10
thread 2 sell ticket: 10
thread 1 sell ticket: 10
thread 4 sell ticket: 10
thread 3 sell ticket: 6
thread 4 sell ticket: 6
thread 2 sell ticket: 6
thread 1 sell ticket: 6
thread 3 sell ticket: 2
thread 2 sell ticket: 2
thread 1 sell ticket: 2
thread 4 sell ticket: 2
thread 3 sell ticket: -2

--------------------------------
Process exited after 0.4664 seconds with return value 0
请按任意键继续. . .

这里在运行的时候有个小bug,当你要运行上面代码的时候会出现 error: invalid conversion from ‘void*’ to 'void* ()(void)'不厌其烦地出现在了我的xshell中,弄得我是苦不堪言。于是,我查看了Posix中建立线程函数的定义:extern int pthread_create (pthread_t *__restrict __threadp, __const pthread_attr_t __restrict __attr, void(__start_routine) (void *), void __restrict __arg) __THROW;这个pthread_create()中的第三个参数是载入一个函数,这个函数有一个参数可以传入,返回一个 通用指针。因此,出现上述错误的解决方法:
(1)线程函数定义为void sell(void
arg),而调用处写为:int ret = pthread_create(&id, NULL, (viod)&thread, NULL);
(2)线程函数定义为void *sell(void *arg),调用处为:int ret = pthead_create(&id, NULL, thread, NULL)。
然后进行编译: gcc main.c -o -lpthread debug,搞定!小伙伴,注意,注意,要运行这段代码,这里只能写gcc,深究其原因就是C编译器允许隐含性的将一个通用指针转换为任意类型的指针(坑爹,只能通过反编译去一步一步调试),而C++不允许的。

小伙伴们现在回到正题
从以上的结果可以看出,出现了ticket为负数的情况。这是什么原因呢?
代码中if语句判断条件为真以后,代码并发的切换到其他线程。usleep模拟漫长的业务过程,在这个过程中,可能有很多线程会进入该代码段。并且ticket操作本身就不是一个原子操作,而是对应三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update:更新寄存器里面的值,执行-1操作
store:将新值从寄存器写回到共享变量ticket的内存地址中
在这里插入图片描述
代码必须要有互斥行为:当代码进入临界区执行时不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
其实这三点要求本质上就是需要一把锁,Linux中把这把锁叫互斥量。锁资源也是临界资源,锁资源保证它的原子性。

{
	非临界区,可以并发的执行代码 
	lock
	临界区,只允许一个线程执行,不允许多个线程同时执行|
	unlock
	非临界区,可以并发的执行代码
}

互斥量的接口
初始化互斥量
初始化互斥量的两种方法

方法1,静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配
int pthread_mutex_init(pthread_mutex *restrict mutex,const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

销毁互斥量

使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量要确保后面不会有线程尝试加锁
销毁函数:
int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量的加锁和解锁函数

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);
返回值:成功返回0,失败返回错误号

在调用pthread_mutex_lock时,可能遇到下面的问题
互斥量属于未上锁的状态,该函数会将互斥量锁定,同时返回成功。
发起函数调用后,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么调用此函数会被阻塞挂起,等待互斥量解锁。因此可以在上面加上互斥锁。代码如下:

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

int ticket = 30; //定义一个共享变量
pthread_mutex_t mutex;

void *sell(void *arg){
	char *id = (char*)arg;
	for( ; ;){
		pthread_mutex_lock(&mutex);
		if(ticket > 0){
			usleep(1000);
			printf("%s sell ticket: %d\n", id, ticket);
			ticket--;
			pthread_mutex_unlock(&mutex);
		} else{
			pthread_mutex_unlock(&mutex);
			break;
		}
	}
}

int main() {

	pthread_t t1, t2, t3, t4; //定义四个线程
	pthread_create(&t1, NULL, sell, "thread 1");
	pthread_create(&t2, NULL, sell, "thread 2");
	pthread_create(&t3, NULL, sell, "thread 3");
	pthread_create(&t4, NULL, sell, "thread 4");
	
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
	return 0;
}

lock和unlock的伪代码如下:

lock:
    movb $0,%a1
    xchgb %a1,mutex
    if(a1寄存器的内容>0)
    {
        return 0;
    }
    else
    {
        挂起等待;
    }
    goto lock;
 
unlock:
    movb $1,mutex
    唤醒等待mutex的线程;
    return 0;

可重入与线程安全

线程安全:多个线程并发同一段代码时,不会出现不同的结果。出现线程安全问题的情况一般是:对全局变量或者静态变量进行操作,并且没有锁保护。

重入:同一函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,称为不可重入函数。

线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

常见的线程安全的情况:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。

线程安全情况:
类或者接口对于线程来说都是原子操作。
多个线程之间的切换不会导致该接口的执行结果存在语义上的二义性。

常见不可重入的情况:
调用malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

常见的可重入函数:
不使用全局变量或静态变量
不使用malloc或new开辟的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入函数与线程安全的联系

函数是可重入的,那就是线程安全的。函数是不可重入的,就不能由多个线程使用,不然就可能引发线程安全问题。如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入函数与线程安全的区别:
可重入函数是线程安全的一种。线程安全不一定是可重入的,我们可以使用一些手段将不可重入的函数变成线程安全,可重入函数一定是线程安全的。
如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个函数是线程安全的,但这个重入的函数若锁还未释放则会产生死锁,因此是不可重入的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值