Linux_线程互斥与同步

1、linux线程互斥

临界资源:多线程执行流共享的资源
临界区:每个线程内部访问临界资源的代码
互斥:任何时刻,互斥保证有且只有一个线程执行流进入临界区访问临界资源
原子性:不会被任何调度机制打断,要么完成,要么未完成

互斥量mutex

大部分情况,线程使用数据为局部变量,变量地址空间在线程栈空间内,归属单线程,其他线程无法获取。有时候,很多变量需要在线程间共享,这样的变量称为共享变量,但是多线程并发操作会带来一些问题,如以下程序,while判断条件为真后,代码可以并发的切换到其他线程,也就是说,有很多线程会进入该代码段:

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

int shared_val = 10;

void * thread_func(void * arg)
{
	while(shared_val)
	{
		sleep(1);
		--shared_val;
		cout<<static_cast<char *>(arg)<<shared_val<<endl;
	}
	return nullptr;
}

int main()
{
	char * msg;
	pthread_t t1, t2, t3, t4;

	msg = "thread 1 shared_val = ";
	pthread_create(&t1, nullptr, thread_func, static_cast<void *>(msg));
	msg = "thread 2 shared_val = ";
	pthread_create(&t2, nullptr, thread_func, static_cast<void *>(msg));
	msg = "thread 3 shared_val = ";
	pthread_create(&t3, nullptr, thread_func, static_cast<void *>(msg));
	msg = "thread 4 shared_val = ";
	pthread_create(&t4, nullptr, thread_func, static_cast<void *>(msg));

	pthread_join(t1, nullptr);
	pthread_join(t2, nullptr);
	pthread_join(t3, nullptr);
	pthread_join(t4, nullptr);
	return 0;
}

在这里插入图片描述
要解决以上问题:首先,代码要有互斥行为,代码进入临界区时不允许其他线程进入;第二,如果多个线程同时想执行临界区代码,且临界区没有线程在执行时,只允许一个线程进入;第三,如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区。

互斥量接口

  • 初始化互斥量
//静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
	const pthread_mutexattr_t *restrict attr);
//mutex:要初始化的互斥量;attr:NULL
  • 销毁互斥量

注:静态分配初始化的互斥量不需要销毁;不要销毁一个加锁的互斥量;已销毁的互斥量要确保后面不会有线程再尝试加锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 互斥量加锁解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

注:调用pthread_mutex_lock时,如果互斥量处于未锁状态,该函数会将互斥量锁定并返回成功;发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

int shared_val = 10;
pthread_mutex_t mutex;

void * thread_func(void * arg)
{
	while(true)
	{
		pthread_mutex_lock(&mutex);
		if(shared_val)
		{
			sleep(1);
			cout<<static_cast<char *>(arg)<<shared_val<<endl;
			--shared_val;
			pthread_mutex_unlock(&mutex);
		}
		else
		{
			pthread_mutex_unlock(&mutex);
			break;
		}
	}
	return nullptr;
}

int main()
{
	char * msg;
	pthread_t t1, t2, t3, t4;
	
	pthread_mutex_init(&mutex, nullptr);

	msg = "thread 1 shared_val = ";
	pthread_create(&t1, nullptr, thread_func, static_cast<void *>(msg));
	msg = "thread 2 shared_val = ";
	pthread_create(&t2, nullptr, thread_func, static_cast<void *>(msg));
	msg = "thread 3 shared_val = ";
	pthread_create(&t3, nullptr, thread_func, static_cast<void *>(msg));
	msg = "thread 4 shared_val = ";
	pthread_create(&t4, nullptr, thread_func, static_cast<void *>(msg));

	pthread_join(t1, nullptr);
	pthread_join(t2, nullptr);
	pthread_join(t3, nullptr);
	pthread_join(t4, nullptr);

	pthread_mutex_destroy(&mutex);
	return 0;
}

可重入与线程安全

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

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

常见的线程不安全的情况

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

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

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

常见可重入的情况

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

可重入与线程安全的联系与区别

联系

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

区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
  • 如果将对临界资源的访问加锁,则这个函数就是线程安全的,但是如果对可重入函数的锁未释放,则会产生死锁

2、死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁算法:死锁检测算法、银行家算法

3、linux线程同步

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
竞态条件:因时序问题而导致程序异常,称为竞态条件。

条件变量函数

  • 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
	const pthread_condattr_t *restrict attr);
  • 销毁
int pthread_cond_destroy(pthread_cond_t *cond);
  • 等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
	pthread_mutex_t *restrict mutex);
  • 唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

pthread_mutex_t mutex;
pthread_cond_t cond;

void * thread_func1(void * arg)
{
	while(true)
	{
		pthread_cond_wait(&cond, &mutex);
		cout<<"activity..."<<endl;
	}
	return nullptr;
}

void * thread_func2(void * arg)
{
	while(true)
	{
		pthread_cond_signal(&cond);
		sleep(1);
	}
	return nullptr;
}

int main()
{
	pthread_t t1, t2;

	pthread_mutex_init(&mutex, nullptr);
	pthread_cond_init(&cond, nullptr);
	
	pthread_create(&t1, nullptr, thread_func1, nullptr);
	pthread_create(&t2, nullptr, thread_func2, nullptr);

	pthread_join(t1, nullptr);
	pthread_join(t2, nullptr);

	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
	return 0;
}

条件变量使用规范

  • 等待条件代码
pthread_mutex_lock(&mutex);
while(条件)
	pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_lock(&mutex);
  • 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
发布了72 篇原创文章 · 获赞 25 · 访问量 7314

猜你喜欢

转载自blog.csdn.net/qq_41245381/article/details/104195227
今日推荐