Linux-笔记 线程同步机制

2024-06-24 08:04

本文主要是介绍Linux-笔记 线程同步机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

前言

实现

信号量(Semaphore)

计数型信号量

二值信号量 

信号量的原语操作

无名信号量的操作函数

例子

互斥锁(mutex)

互斥锁的操作函数

例子

自旋锁 (Spinlock)

自旋锁与互斥锁的区别

自旋锁的操作函数

例子


前言

        线程同步是为了对共享资源的访问进行保护,确保数据的一致性,由于进程中会有多个线程的存在,每个线程对共享资源的并发访问就会出现数据的一致性问题。

实现

        实现线程同步的机制有很多,比较常用的有:信号量、互斥锁、自旋锁、条件变量、读写锁等等,这里仅探讨linux下的线程同步机制。

信号量(Semaphore)

        信号量是一种非常常见的进程同步机制,主要用于多线程或多进程环境中,以控制对共享资源的访问。如果是按照命名区分可以分为有名信号量和无名信号量,本章主要使用无名信号量,因为无名信号量常用于线程同步,它们没有全局名称,只在进程内部有效,使用起来简单且高效。

如果按照信号量值分类可以分成计数型信号量与二值信号量。

计数型信号量
  • 计数信号量的值可以是一个非负整数。
  • 它用于控制多个相同资源的访问数量。
  • 当一个进程(或线程)试图获取资源时,它会检查信号量的值是否大于零。如果大于零,则信号量减一,进程继续执行;否则,进程会进入等待状态,直到信号量的值大于零。
  • 当一个进程释放资源时,它会将信号量的值加一,并唤醒等待队列中的一个进程(如果有的话)。
二值信号量 
  • 二元信号量的值只有0和1两种状态,类似于互斥锁(Mutex)。
  • 当信号量为1时,表示资源是可用的;当信号量为0时,表示资源不可用。
  • 一个进程获取资源时,将信号量设置为0;当进程释放资源时,将信号量设置为1。
  • 二元信号量主要用于确保一次只有一个进程访问某个资源。
信号量的原语操作
  • P操作(Proberen):也叫wait或down操作,表示请求资源。

    • 对于计数信号量,P操作会将信号量的值减一,如果结果为负,进程将进入等待状态。
    • 对于二元信号量,如果信号量值为0,进程将进入等待状态;如果为1,则设置为0,继续执行。
  • V操作(Verhogen):也称为signalup操作,表示释放资源。

    • 对于计数信号量,V操作会将信号量的值加一,如果有进程在等待,将唤醒一个等待的进程。
    • 对于二元信号量,将信号量设置为1,并唤醒一个等待的进程(如果有的话)。
无名信号量的操作函数

1、sem_init:初始化一个无名信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);sem_t sem;
if (sem_init(&sem, 0, 1) != 0) {perror("sem_init");exit(EXIT_FAILURE);
}
  • 参数

    • sem:指向信号量对象的指针。
    • pshared:指定信号量是否在进程间共享。0 表示用于线程间同步,非 0 表示用于进程间同步。
    • value:信号量的初始值。
  • 返回值

    • 成功返回 0,失败返回 -1 并设置 errno

2、sem_destroy:销毁一个无名信号量。

int sem_destroy(sem_t *sem);if (sem_destroy(&sem) != 0) {perror("sem_destroy");exit(EXIT_FAILURE);
}
  • 参数

    • sem:指向要销毁的信号量对象的指针。
  • 返回值

    • 成功返回 0,失败返回 -1 并设置 errno

3、sem_wait:等待信号量,将信号量的值减1。如果信号量的值为0,则阻塞直到信号量的值大于0。

int sem_wait(sem_t *sem);if (sem_wait(&sem) != 0) {perror("sem_wait");exit(EXIT_FAILURE);
}
  • 参数

    • sem:指向信号量对象的指针。
  • 返回值

    • 成功返回 0,失败返回 -1 并设置 errno

4、sem_post:释放信号量,将信号量的值加1。如果有其他线程正在阻塞等待该信号量,则唤醒其中一个线程。

int sem_post(sem_t *sem);if (sem_post(&sem) != 0) {perror("sem_post");exit(EXIT_FAILURE);
}
  • 参数

    • sem:指向信号量对象的指针。
  • 返回值

    • 成功返回 0,失败返回 -1 并设置 errno

5、sem_trywait:尝试等待信号量。如果信号量的值大于0,将信号量的值减1并立即返回。如果信号量的值为0,则立即返回错误而不阻塞。

int sem_trywait(sem_t *sem);if (sem_trywait(&sem) != 0) {if (errno == EAGAIN) {printf("信号量当前不可用\n");} else {perror("sem_trywait");exit(EXIT_FAILURE);}
}
  • 参数

    • sem:指向信号量对象的指针。
  • 返回值

    • 成功返回 0,失败返回 -1 并设置 errno

6、sem_timedwait:在指定的时间内等待信号量。如果在指定时间内信号量的值变为正数,则将其减1并返回。如果超过指定时间,返回错误。

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5; // 等待5秒if (sem_timedwait(&sem, &ts) != 0) {if (errno == ETIMEDOUT) {printf("等待超时\n");} else {perror("sem_timedwait");exit(EXIT_FAILURE);}
}
  • 参数

    • sem:指向信号量对象的指针。
    • abs_timeout:指定的绝对超时时间。
  • 返回值

    • 成功返回 0,失败返回 -1 并设置 errno
例子
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>sem_t sem_g, sem_p;  // 定义两个信号量
char ch = 'a';void *pthread_g(void *arg)  // 改变字符 ch 的值
{while (1){sem_wait(&sem_g);  //第一次为0阻塞ch++;sleep(1);sem_post(&sem_p);  //加1}
}void *pthread_p(void *arg)  // 打印字符 ch 的值
{while (1){sem_wait(&sem_p); //非0,减一执行printf("%c", ch);fflush(stdout);sem_post(&sem_g); //为0,加一}
}int main()
{pthread_t tid1, tid2;sem_init(&sem_g, 0, 0);  // 初始化信号量sem_init(&sem_p, 0, 1);  //sem_p先获得1,先执行pthread_create(&tid1, NULL, pthread_g, NULL);pthread_create(&tid2, NULL, pthread_p, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}

互斥锁(mutex)

        互斥锁又叫互斥量,在对共享资源进行访问的时候可以进行上锁,访问结束后可以解锁。当有一个线程对互斥锁进行上锁后,其他线程也想对互斥锁进行上锁就会被阻塞,直到对互斥锁上锁的线程释放互斥锁为止。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行上锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。

        简单说,互斥量就是为了确保同一时间只有一个线程能够访问一个共享资源。

互斥锁的操作函数

1、pthread_mutex_init:初始化一个互斥锁。

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);pthread_mutex_t mutex;
if (pthread_mutex_init(&mutex, NULL) != 0) {perror("pthread_mutex_init");exit(EXIT_FAILURE);
}
  • 参数

    • mutex:指向互斥锁对象的指针。
    • attr:互斥锁属性。可以是 NULL,表示使用默认属性。
  • 返回值

    • 成功返回 0,失败返回错误代码。

2、pthread_mutex_destroy:销毁一个互斥锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);if (pthread_mutex_destroy(&mutex) != 0) {perror("pthread_mutex_destroy");exit(EXIT_FAILURE);
}
  • 参数

    • mutex:指向要销毁的互斥锁对象的指针。
  • 返回值

    • 成功返回 0,失败返回错误代码。

3、pthread_mutex_lock:锁定一个互斥锁。如果互斥锁已经被锁定,则阻塞直到互斥锁可用。

int pthread_mutex_lock(pthread_mutex_t *mutex);if (pthread_mutex_lock(&mutex) != 0) {perror("pthread_mutex_lock");exit(EXIT_FAILURE);
}
  • 参数

    • mutex:指向要锁定的互斥锁对象的指针。
  • 返回值

    • 成功返回 0,失败返回错误代码。

4、pthread_mutex_trylock:尝试锁定一个互斥锁。如果互斥锁已经被锁定,则立即返回错误而不阻塞。

int pthread_mutex_trylock(pthread_mutex_t *mutex);if (pthread_mutex_trylock(&mutex) != 0) {if (errno == EBUSY) {printf("互斥锁当前已被锁定\n");} else {perror("pthread_mutex_trylock");exit(EXIT_FAILURE);}
}
  • 参数

    • mutex:指向要锁定的互斥锁对象的指针。
  • 返回值

    • 成功返回 0,失败返回错误代码。

5、pthread_mutex_unlock:解锁一个互斥锁。

int pthread_mutex_unlock(pthread_mutex_t *mutex);if (pthread_mutex_unlock(&mutex) != 0) {perror("pthread_mutex_unlock");exit(EXIT_FAILURE);
}
  • 参数

    • mutex:指向要解锁的互斥锁对象的指针。
  • 返回值

    • 成功返回 0,失败返回错误代码。
例子
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>#define NUM_ITERATIONS 1000000int counter = 0;
pthread_mutex_t mutex;void* increment(void* arg) {for (int i = 0; i < NUM_ITERATIONS; i++) {pthread_mutex_lock(&mutex);   // 加锁counter++;pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}void* decrement(void* arg) {for (int i = 0; i < NUM_ITERATIONS; i++) {pthread_mutex_lock(&mutex);   // 加锁counter--;pthread_mutex_unlock(&mutex); // 解锁}return NULL;
}int main() {pthread_t thread1, thread2;// 初始化互斥锁if (pthread_mutex_init(&mutex, NULL) != 0) {perror("pthread_mutex_init");exit(EXIT_FAILURE);}// 创建线程if (pthread_create(&thread1, NULL, increment, NULL) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}if (pthread_create(&thread2, NULL, decrement, NULL) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}// 等待线程完成if (pthread_join(thread1, NULL) != 0) {perror("pthread_join");exit(EXIT_FAILURE);}if (pthread_join(thread2, NULL) != 0) {perror("pthread_join");exit(EXIT_FAILURE);}// 销毁互斥锁if (pthread_mutex_destroy(&mutex) != 0) {perror("pthread_mutex_destroy");exit(EXIT_FAILURE);}// 输出结果printf("Final counter value: %d\n", counter);return 0;
}

自旋锁 (Spinlock)

        自旋锁(spinlock)是一种用于多线程编程的同步机制,用于保护共享资源,使同一时间只有一个线程可以访问该资源。与互斥锁不同,自旋锁在等待锁时不会使线程休眠,而是不断地检查锁的状态,直到获得锁。这种机制在短时间内需要高频率访问共享资源的情况下比较有效。

        自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋), 所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。 试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁。

自旋锁与互斥锁的区别
特性自旋锁(Spinlock)互斥锁(Mutex)
等待机制忙等待,循环检查锁状态睡眠等待,线程被挂起
CPU开销高(忙等待消耗CPU)低(等待时线程睡眠)
上下文切换不会发生可能发生
适用场景锁持有时间短,多处理器系统锁持有时间长,各类系统
复杂度简单复杂(需要操作系统调度支持)
死锁检测一般没有自动检测机制可以有自动检测机制
自旋锁的操作函数

1、spin_lock_init:初始化一个自旋锁。

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);static pthread_spinlock_t spin;//定义自旋锁
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
  • 参数
    • lock:指向 pthread_spinlock_t 类型的指针,用于存储初始化后的自旋锁。
    • pshared:指定锁的共享性质,可以是 PTHREAD_PROCESS_PRIVATEPTHREAD_PROCESS_SHARED
  • 返回值
    • 成功初始化返回 0,失败返回错误码。

2、pthread_spin_destroy:用于销毁自旋锁。

int pthread_spin_destroy(pthread_spinlock_t *lock);static pthread_spinlock_t spin;
/* 销毁自旋锁 */pthread_spin_destroy(&spin);
  • 参数
    • lock:指向 pthread_spinlock_t 类型的指针,指定要销毁的自旋锁。
  • 返回值
    • 成功销毁返回 0,失败返回错误码。

3、pthread_spin_lock:用于获取自旋锁,如果自旋锁已经被其他线程持有,则当前线程会一直自旋等待直到获取锁为止。

int pthread_spin_lock(pthread_spinlock_t *lock);static pthread_spinlock_t spin;//定义自旋锁
pthread_spin_lock(&spin); //自旋锁上锁
  • 参数
    • lock:指向 pthread_spinlock_t 类型的指针,指定要获取的自旋锁。
  • 返回值
    • 成功获取锁返回 0,失败返回错误码。

4、pthread_spin_trylock:用于尝试获取自旋锁,如果自旋锁已经被其他线程持有,则立即返回失败,不会进入自旋等待。

int pthread_spin_trylock(pthread_spinlock_t *lock);
  • 参数
    • lock:指向 pthread_spinlock_t 类型的指针,指定要尝试获取的自旋锁。
  • 返回值
    • 成功获取锁返回 0,失败返回 EBUSY(锁已经被持有)或其他错误码。

5、pthread_spin_unlock:用于释放自旋锁,允许其他线程继续获取该自旋锁。

int pthread_spin_unlock(pthread_spinlock_t *lock);pthread_spin_unlock(&spin);//自旋锁解锁
  • 参数
    • lock:指向 pthread_spinlock_t 类型的指针,指定要释放的自旋锁。
  • 返回值
    • 成功释放锁返回 0,失败返回错误码。
例子
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> // 提供 sleep 函数pthread_spinlock_t lock;
int shared_variable = 0;void* thread_func(void* arg) {for (int i = 0; i < 5; ++i) {pthread_spin_lock(&lock);shared_variable++;printf("Thread %ld: shared_variable = %d\n", (long)pthread_self(), shared_variable);pthread_spin_unlock(&lock);sleep(1); // 模拟耗时操作}return NULL;
}int main() {pthread_t thread1, thread2;// 初始化自旋锁pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);// 创建线程pthread_create(&thread1, NULL, thread_func, NULL);pthread_create(&thread2, NULL, thread_func, NULL);// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);// 销毁自旋锁pthread_spin_destroy(&lock);return 0;
}

这篇关于Linux-笔记 线程同步机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089579

相关文章

linux生产者,消费者问题

pthread_cond_wait() :用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread

Linux 安装、配置Tomcat 的HTTPS

Linux 安装 、配置Tomcat的HTTPS 安装Tomcat 这里选择的是 tomcat 10.X ,需要Java 11及更高版本 Binary Distributions ->Core->选择 tar.gz包 下载、上传到内网服务器 /opt 目录tar -xzf 解压将解压的根目录改名为 tomat-10 并移动到 /opt 下, 形成个人习惯的路径 /opt/tomcat-10

Tolua使用笔记(上)

目录   1.准备工作 2.运行例子 01.HelloWorld:在C#中,创建和销毁Lua虚拟机 和 简单调用。 02.ScriptsFromFile:在C#中,对一个lua文件的执行调用 03.CallLuaFunction:在C#中,对lua函数的操作 04.AccessingLuaVariables:在C#中,对lua变量的操作 05.LuaCoroutine:在Lua中,

AssetBundle学习笔记

AssetBundle是unity自定义的资源格式,通过调用引擎的资源打包接口对资源进行打包成.assetbundle格式的资源包。本文介绍了AssetBundle的生成,使用,加载,卸载以及Unity资源更新的一个基本步骤。 目录 1.定义: 2.AssetBundle的生成: 1)设置AssetBundle包的属性——通过编辑器界面 补充:分组策略 2)调用引擎接口API

RedHat运维-Linux文本操作基础-AWK进阶

你不用整理,跟着敲一遍,有个印象,然后把它保存到本地,以后要用再去看,如果有了新东西,你自个再添加。这是我参考牛客上的shell编程专项题,只不过换成了问答的方式而已。不用背,就算是我自己亲自敲,我现在好多也记不住。 1. 输出nowcoder.txt文件第5行的内容 2. 输出nowcoder.txt文件第6行的内容 3. 输出nowcoder.txt文件第7行的内容 4. 输出nowcode

【Linux进阶】UNIX体系结构分解——操作系统,内核,shell

1.什么是操作系统? 从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kerel),因为它相对较小,而且位于环境的核心。  从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特生。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等

《offer来了》第二章学习笔记

1.集合 Java四种集合:List、Queue、Set和Map 1.1.List:可重复 有序的Collection ArrayList: 基于数组实现,增删慢,查询快,线程不安全 Vector: 基于数组实现,增删慢,查询快,线程安全 LinkedList: 基于双向链实现,增删快,查询慢,线程不安全 1.2.Queue:队列 ArrayBlockingQueue:

Windows/macOS/Linux 安装 Redis 和 Redis Desktop Manager 可视化工具

本文所有安装都在macOS High Sierra 10.13.4进行,Windows安装相对容易些,Linux安装与macOS类似,文中会做区分讲解 1. Redis安装 1.下载Redis https://redis.io/download 把下载的源码更名为redis-4.0.9-source,我喜欢跟maven、Tomcat放在一起,就放到/Users/zhan/Documents

操作系统实训复习笔记(1)

目录 Linux vi/vim编辑器(简单) (1)vi/vim基本用法。 (2)vi/vim基础操作。 进程基础操作(简单) (1)fork()函数。 写文件系统函数(中等) ​编辑 (1)C语言读取文件。 (2)C语言写入文件。 1、write()函数。  读文件系统函数(简单) (1)read()函数。 作者本人的操作系统实训复习笔记 Linux

Linux系统稳定性的奥秘:探究其背后的机制与哲学

在计算机操作系统的世界里,Linux以其卓越的稳定性和可靠性著称,成为服务器、嵌入式系统乃至个人电脑用户的首选。那么,是什么造就了Linux如此之高的稳定性呢?本文将深入解析Linux系统稳定性的几个关键因素,揭示其背后的技术哲学与实践。 1. 开源协作的力量Linux是一个开源项目,意味着任何人都可以查看、修改和贡献其源代码。这种开放性吸引了全球成千上万的开发者参与到内核的维护与优化中,形成了