Linux系统编程三(设置线程属性、互斥锁、读写锁、条件变量、信号量、生产者和消费者模型)

本文介绍了多线程编程中的同步技术,包括设置线程属性、线程同步的概念,详细讲解了互斥锁的使用、读写锁的优缺点以及在数据同步中的应用,自旋锁的特点和适用场景,还对比了互斥锁和自旋锁的区别。此外,文章通过条件变量和信号量展示了如何实现生产者消费者模型,深入探讨了这两种同步原语在并发控制中的作用。
摘要由CSDN通过智能技术生成

1、设置线程属性

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

void sys_err(char* str)
{
	perror(str);
	exit(-1);
}

void* tfn(void* arg)
{
	printf("thread;pid = %d, tid = %lu\n", getpid(), pthread_self());
	return NULL;
}

int main(int argc, char* argv[])
{
	pthread_t tid;
	pthread_attr_t attr;
	int ret = pthread_attr_init(&attr);
	if (ret != 0) {
		perror("pthread_attr_init error");
		exit(1);
	}

	ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);		//设置线程属性为分离态
	if (ret != 0) {
		perror("pthread_attr_setdetachstate error");
		exit(1);
	}

	ret = pthread_create(&tid, &attr, tfn, NULL);			//创建线程
	if (ret != 0) {
		perror("pthread_create error");
		exit(1);
	}

	ret = pthread_attr_destroy(&attr);						//销毁线程属性
	if (ret != 0) {
		perror("pthread_attr_destroy error");
		exit(1);
	}

	ret = pthread_join(tid, NULL);							//回收线程 等待阻塞
	if (ret != 0) {											//设置分离后,无需回收,故会出错
		perror("pthread_join error");
		exit(1);
	}

	printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
	pthread_exit((void*)0);

	return 0;
}

输出结果
在这里插入图片描述

2、线程同步

概念:协同步调,对公共区域数据按先后顺序访问。 防止数据混乱,产生与时间有关的错误。多个控制访问同一共享资源,必须同步。
一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回,且同时其他线程为 保证数据一致性,不能调用该功能。例如:银行取钱。

3、互斥锁

锁mutex的使用:建议锁,锁不会限制资源访问。
对公共数据进行保护。 所有线程应该在访问公共数据前先拿锁再访问,但锁本身不具备强制性。
使用mutex(互斥量、互斥锁)一般步骤:
1、pthread_mutex_t lock
创建一个pthread_mutex_t类型的名为locks的变量,本质是结构体类型。
2、pthread_mutex_init:初始化,相当于初值为1
3、pthread_mutex_lock:加锁 1减减 -> 0
4、访问共享数据(stdout)
5、pthread_mutex_unlock 解锁 0加加 ->1,同时将阻塞在该锁上的所有线程全部唤醒
6、pthread_mutex_destroy 销毁锁
初始化互斥量:
pthread_mutex_t mutex
1、pthread_mutex_init(&mutex, NULL):动态初始化
参数
互斥锁mutex(传出)
互斥锁属性attr(传入)
restrict关键字,用来限定指针变量,被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
2、pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER:静态初始化
注意事项:
尽量保证锁的粒度,越小越好,访问共享数据前。加锁。访问结束,立即解锁。
互斥锁,本质为结构体,可以看作整数。初始化后为1。 pthread_mutex_init()函数调用成功。
加锁 --操作 阻塞线程 锁只有两种状态
解锁 ++操作 唤醒阻塞在锁上的线程
try锁 尝试加锁:
成功 减减(- -)
失败 返回错误号(EBUSY) 不阻塞

4、借助互斥锁管理共享数据实现同步

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

pthread_mutex_t mutex;			//定义一把互斥锁

void* tfn(void* arg)
{
	srand(time(NULL));
	while (1) {
		pthread_mutex_lock(&mutex);			//加锁
		printf("hello ");
		sleep(rand() % 3);		//模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
		printf("world\n");
		pthread_mutex_unlock(&mutex);		//解锁
		sleep(rand() % 3);
	}
	return NULL;
}

int main(int argc, char* argv[])
{
	pthread_t tid;
	srand(time(NULL));

	int ret = pthread_mutex_init(&mutex, NULL);			//初始化互斥锁
	if (ret != 0) {
		fprintf(stderr, "mutex init error:%s\n", strerror(ret));
		exit(1);
	}

	pthread_create(&tid, NULL, tfn, NULL);
	while (1) {
		pthread_mutex_lock(&mutex);			//加锁
		printf("HELLO");
		sleep(rand() % 3);
		printf("WORLD\n");
		pthread_mutex_unlock(&mutex);		//解锁

		sleep(rand() % 3);
	}
	pthread_join(tid, NULL);
	pthread_mutex_destroy(&mutex);			//销毁互斥锁
	
	return 0;
}

输出结果如下
在这里插入图片描述

5、读写锁

读写锁:锁只有一把
以读方式给数据加锁–读锁
以写方式给数据加锁–写锁。解锁前,所有对该加锁的线程都会被阻塞。
读共享,写独占,多个线程去读都可以加锁成功。
写锁优先级高,当读和写一起来的时候,优先写锁操作。
相较于互斥量而言,当读线程多的时候,提高访问效率。
使用场景:适合于对数据结构读的次数远大于写。
读写锁的操作:
1、pthread_rwlock_t rwlock
2、pthread_rwlock_init(&rwlock, NULL)
3、pthread_rwlock_rdlock(&rwlock)
4、pthread_rwlock_wrlock(&rwlock)
5、pthread_rwlock_unlock(&rwlock)
6、pthread_rwlock_destroy(&rwlock)

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

int counter;
pthread_rwlock_t rwlock;		//定义一个全局的读写锁

//3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
void* th_write(void* arg)
{
	int t;
	long i = (long)arg;

	while (1) {
		pthread_rwlock_wrlock(&rwlock);		//以写模式加锁,写独占
		t = counter;
		usleep(1000);
		printf("---------write %ld: %lu: counter = %d ++counter = %d\n", i, pthread_self(), t, ++counter);
		pthread_rwlock_unlock(&rwlock);
		usleep(10000);
	}
	return NULL;
}

void* th_read(void* arg)
{
	long i = (long)arg;
	while (1) {
		pthread_rwlock_rdlock(&rwlock);		//读线程间,读锁共享
		printf("-----------read %ld: %lu :%d\n", i, pthread_self(), counter);
		pthread_rwlock_unlock(&rwlock);
		usleep(2000);
	}
	return NULL;
}

int main(void)
{
	int i;
	pthread_t tid[8];
	pthread_rwlock_init(&rwlock, NULL);			//初始化读写锁

	for (i = 0; i < 3; i++)						//写线程 进行写操作
		pthread_create(&tid[i], NULL, th_write, (void*)i);
	for (i = 0; i < 5; i++)						//读线程 进行读操作
		pthread_create(&tid[i], NULL, th_read, (void*)i);
	for (i = 0; i < 8; i++)
		pthread_join(tid[i], NULL);
		
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

输出结果
在这里插入图片描述

6、自旋锁

struct list_head		done_list;			//此链表中的缓冲区已填充数据,可以出队被用户空间取走使用 
spinlock_t			done_lock;				//保护done_list链表的自旋锁

自旋锁 done_lock,用于保护 done_list 链表。自旋锁是一种常见的同步机制,用于在多线程环境中保护共享资源免受并发访问的影响。

自旋锁的特点是当某个线程尝试获取锁时,如果锁已经被其他线程占用,该线程不会进入睡眠状态,而是会一直循环等待,直到锁可用为止。因此,自旋锁适用于保护临界区较小且锁被占用时间较短的情况。

在这里,done_lock 用于保护 done_list 链表,以确保在多线程环境下对该链表的访问是线程安全的。这意味着任何试图对 done_list 进行修改或访问的线程都必须先获取 done_lock 锁,操作完成后再释放锁,以避免竞争条件和数据不一致性问题。

补充:自旋锁是一种多线程同步的机制,用于保护临界区资源的访问。与互斥锁不同,自旋锁并不会将线程阻塞在等待资源释放的地方,而是采用循环的方式一直尝试获取锁,直到成功为止。这种方式称为"自旋",因为线程在获取锁之前会一直循环忙等(自旋)。
自旋锁有两种状态:被锁定(Locked)和未锁定(Unlocked)。当一个线程尝试获取自旋锁时,如果锁处于未锁定状态,那么线程将将其状态设置为锁定,并继续执行临界区代码。如果自旋锁已经被其他线程锁定,那么线程将继续在循环中自旋,直到锁被释放为止。

7、互斥锁和自旋锁的区别

互斥锁(Mutex

  1. 阻塞行为:当一个线程试图获取一个已经被其他线程持有的互斥锁时,它会被阻塞,直到锁被释放。这种阻塞行为可以减少CPU的空转时间。
  2. 适用范围:互斥锁适用于锁持有时间较长的情况下,因为线程被阻塞,不会消耗CPU资源。
  3. 上下文切换:由于线程会被阻塞,需要切换上下文,这会有一些开销。
  4. 优点:适用于锁持有时间较长的情况,可以让线程释放CPU,以便执行其他任务。
  5. 缺点:在高性能需求的情况下,锁竞争导致的阻塞可能引起性能下降。

自旋锁(Spinlock)

  1. 循环等待:当一个线程试图获取一个已经被其他线程持有的自旋锁时,它会一直循环等待(空转)直到锁被释放。这种等待消耗CPU资源。
  2. 适用范围:自旋锁适用于锁持有时间很短的情况,因为它避免了上下文切换的开销。
  3. 上下文切换: 自旋锁不阻塞线程,避免了上下文切换,但可能会消耗大量的CPU时间。
  4. 优点:在锁持有时间短的情况下,避免了线程被阻塞和上下文切换,减少了锁竞争对性能的影响。
  5. 缺点:如果锁持有时间较长,自旋锁会消耗大量的CPU资源,导致性能下降。

总体来说,选择使用互斥锁还是自旋锁取决于锁持有时间的长短。如果锁持有时间较长,建议使用互斥锁以避免CPU资源的浪费;如果锁持有时间较短,建议使用自旋锁以避免上下文切换的开销。
在多核处理器上,自旋锁的效率通常比较高,因为在自旋等待期间,线程可以利用CPU时间片继续执行其他任务,而不是被操作系统挂起和重新调度。
应用
互斥锁:vb2_queue的互斥锁,使缓冲队列的操作串行化 因为会进行数据的copy,时间长
自旋锁:
1、队列缓冲区链表的增删改查 链表操作时间较短
2、在网卡发送网络包时,要求较高的实时性,网卡是PCIE,并行网络包发送速度会很快

8、条件变量

条件变量:本身不是锁,但是通常结合mutex锁来使用。
初始化条件变量:首先定义一个条件变量 pthread_cond_t cond;
方式1:动态初始化 pthread_cond_init (&cond, NULL);
方式2:静态初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER
pthread_cond_wait函数:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
阻塞等待一个条件变量
作用
1、阻塞等待条件变量cond满足。
2、释放已经掌握的互斥锁mutex。相当于pthread_mutex_unlock(&mutex)
3、被唤醒时,函数返回时解除阻塞,重新申请获取互斥锁。2和3有一个历时的时长。
1、2步为原子操作。同时完成,不可分。
pthread_cond_timedwait 超时等待
pthread_cond_signal 唤醒(至少)一个阻塞在条件变量上的线程
pthread_cond_broadcast 唤醒全部阻塞在条件变量上的 所有线程
pthread_cond_destroy 销毁条件变量

9、条件变量实现生产者消费者模型

//借助条件变量模拟 生产者-消费者 问题
#include <stdio.h>
#include <unistd.h> 
#include <pthread.h>
#include <stdlib.h>

//链表作为共享数据,需被互斥量保护
struct msg {
	struct msg* next;
	int num;
};

struct msg* head;

//静态初始化 一个条件变量 和 一个互斥锁
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* consumer(void* p)			//消费者线程
{
	struct msg* mp;

	for (;;) {
		pthread_mutex_lock(&lock);
		while (head == NULL) {
			pthread_cond_wait(&has_product, &lock);	
		}
		mp = head;
		head = mp->next;
		pthread_mutex_unlock(&lock);

		printf("-Consume %lu---%d\n", pthread_self(), mp->num);
		free(mp);				//共享堆区
		sleep(rand() % 5);
	}
}

void* producer(void* p)			//生产者线程
{
	struct msg* mp;

	for (;;) {
		mp = malloc(sizeof(struct msg));
		mp->num = rand() % 1000 + 1;			//模拟生产一个产品
		printf("-Produce --------------%d\n", mp->num);

		pthread_mutex_lock(&lock);
		mp->next = head;
		head = mp;
		pthread_mutex_unlock(&lock);

		pthread_cond_signal(&has_product);		//将等待在该条件变量上的一个线程唤醒
		sleep(rand() % 5);
	}
}

int main(int argc, char* argv[])
{
	pthread_t pid, cid;
	srand(time(NULL));
	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid, NULL, consumer, NULL);

	pthread_join(pid, NULL);
	pthread_join(cid, NULL);
	return 0;
}

在这里插入图片描述

10、信号量

信号量:应用于线程、进程同步。支持多个线程访问共享区域
相当于初始化值为 N的互斥量,N值表示可以同时访问共享数据区的线程数。保证同步的同时,提高并发。信号量的初值,决定了占用信号量的线程的个数。
sem_t sem :定义结构体类型,N代表线程数量,N不能<0,头文件#include <semaphore.h>。
sem_init(sem_t *sem , int pshared, unsigned int value)
参数:
sem:信号量
pshared: 0:用于线程间同步, 1:用于进程间同步
value:N值(指定同时访问的进程数)
sem_destroy()
sem_wait() 类比于pthread_mutex_lock。 一次调用,做一次–减减操作。 当信号量的值为0时,再–减减就会阻塞。
sem_post() 类比于pthread_mutex_unlock。一次调用,做一次++操作。 当信号量的值为N时,再加加就会阻塞。

11、信号量实现生产者消费者模型

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

#define NUM 5

int queue[NUM];			//全局数组实现环形队列
sem_t blank_number, product_number;			//空格子信号量 5,产品信号量 0

void* producer(void* arg)
{
	int i = 0;

	while (1) {
		sem_wait(&blank_number);			//生产者将空格子数--,为0则阻塞等待				
		queue[i] = rand() % 1000 + 1;		//生产一个产品
		printf("---Produce---%d\n", queue[i]);
		sem_post(&product_number);			//将产品数++

		i = (i + 1) % NUM;					//借助下标实现环形
		sleep(rand() % 1);					
	}
}


void* consumer(void* arg)
{
	int i = 0;

	while (1) {
		sem_wait(&product_number);				//消费者将产品数--, 为0则阻塞等待
		printf("-Consume---%d\n", queue[i]);
		queue[i] = 0;							//消费一个产品
		sem_post(&blank_number);				//消费掉以后,格子数++

		i = (i + 1) % NUM;
		sleep(rand() % 3);
	}
}

int main(int argc, char* argv[])
{
	pthread_t pid, cid;

	sem_init(&blank_number, 0, NUM);			//初始化空格子信号为5,线程间共享--0
	sem_init(&product_number, 0, 0);			//产品数为0

	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid, NULL, consumer, NULL);

	pthread_join(pid, NULL);
	pthread_join(cid, NULL);

	sem_destroy(&blank_number);
	sem_destroy(&product_number);

	return 0;
}

输出结果如下
在这里插入图片描述

12、RCU

RCU(Read-Copy-Update)是一种高效的同步机制,主要用于优化多线程或多核环境中的读取操作,减少写操作对读取操作的影响。它特别适用于读操作频繁而写操作相对少的场景。

RCU 的工作原理
1、读取:
读取操作通常不需要加锁,读操作只需访问数据的快照。
在 RCU 中,读取操作可以并发执行,因为它们读取的数据版本是稳定的,不会被写操作修改。
2、更新:
写操作不会直接修改正在被读取的数据。相反,它会创建数据的一个新的版本。
更新操作通常涉及三个步骤:
复制:创建数据的新版本。
更新:将指向数据的引用更新为新版本。
回收:等待所有之前的读取操作完成,然后回收旧版本的数据。
3、延迟回收:
RCU 使用延迟回收机制,即使数据的旧版本被更新,旧版本的数据在新版本被完全替换后,仍然会保留一段时间,直到确保所有读取操作完成后再回收。

内核源码:kernel-4.9\net\bluetooth\6lowpan.c

static void disconnect_devices(void)
{
	struct lowpan_btle_dev *entry, *tmp, *new_dev;
	struct list_head devices;

	INIT_LIST_HEAD(&devices);

	/* We make a separate list of devices because the unregister_netdev()
	 * will call device_event() which will also want to modify the same
	 * devices list.
	 */

	rcu_read_lock();

	list_for_each_entry_rcu(entry, &bt_6lowpan_devices, list) {
		new_dev = kmalloc(sizeof(*new_dev), GFP_ATOMIC);
		if (!new_dev)
			break;

		new_dev->netdev = entry->netdev;
		INIT_LIST_HEAD(&new_dev->list);

		list_add_rcu(&new_dev->list, &devices);
	}

	rcu_read_unlock();
}

它首先通过 RCU 读锁创建一个副本列表,防止在遍历 bt_6lowpan_devices 列表时发生竞争条件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值