Linux系统编程(线程同步 互斥锁)

简介: Linux系统编程(线程同步 互斥锁)

前言

本篇文章带大家学习线程的同步。

一、什么是线程同步

线程同步是指协调多个线程之间的执行顺序,以确保共享资源的正确访问和数据的一致性。当多个线程同时操作共享数据时,如果没有适当的同步机制,就会出现数据竞争和不一致的情况。

线程同步的目的是为了保证共享资源在多线程环境下的安全访问,避免数据冲突和并发缺陷。通过使用同步机制,可以使得多个线程按照一定的顺序来访问共享资源,避免出现竞态条件(Race Condition)和不确定性的结果。

常用的线程同步机制包括:

1.互斥锁(Mutex):互斥锁是一种常见的同步机制,用于保护共享资源,一次只允许一个线程访问共享资源。当一个线程获取到互斥锁之后,其他线程必须等待该线程释放锁之后才能继续访问。

2.信号量(Semaphore):信号量是一种用于控制并发访问数量的同步机制。通过设置一个计数器,限制同时访问某个资源的线程数量。当计数器大于0时,线程可以获取许可进行访问,访问后计数器减少;当计数器为0时,线程等待其他线程释放许可。

3.条件变量(Condition Variable):条件变量用于线程之间的条件等待与信号通知。线程可以在某个条件成立时等待条件变量,而其他线程在满足条件时发出信号通知等待线程继续执行。

4.栅栏(Barrier):栅栏用于线程的同步和屏障等待。多个线程在某个位置等待,直到所有线程都到达屏障位置,然后才能继续执行后面的操作。

`线程同步的正确使用可以避免数据竞争和不一致性问题,提高并发程序的正确性和可靠性。然而,不正确或过度的同步机制也可能导致性能问题,因此需要根据具体的应用场景进行合理的同步策略和设计。``

二、不使用线程同步访问共享资源可能出现的问题

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int i = 0;
void* Thread1(void *arg)
{
    while (1)
    {
        i++;
        printf("Thread1 i = %d\n", i);
        sleep(1);//休眠1s
    }
}
void* Thread2(void *arg)
{
    while (1)
    {
        i++;
        printf("Thread2 i = %d\n", i);
        sleep(1);//休眠1s
    }
}
int main(void)
{
    pthread_t tid1;//线程ID
    pthread_t tid2;//线程ID
    int err = -1;
    err = pthread_create(&tid1, NULL, Thread1, NULL);
    if(err < 0)
    {
        printf("create thread1 is err\n");
    }
    err = pthread_create(&tid2, NULL, Thread2, NULL);
    if(err < 0)
    {
        printf("create thread2 is err\n");
    }
    while (1)
    {
        sleep(1);//休眠1s
    }
    return 0;
}

运行结果:

由运行结果可以看出不使用线程同步访问共享资源的话可能导致数据的不一致性。

三、互斥锁概念

互斥锁(Mutex Lock)是一种并发编程中的同步机制,用于保护共享资源的访问,确保在任何给定时间只有一个线程可以执行受保护的代码段。

互斥锁是二进制锁,它有两个状态:锁定(locked)和解锁(unlocked)。当一个线程获取到互斥锁时,它将锁定状态设置为锁定,其他线程尝试获取锁时将被阻塞,直到锁被释放。

互斥锁主要用于解决并发环境下的竞争条件,例如多个线程试图同时访问和修改同一共享资源的情况。通过使用互斥锁,我们可以确保同一时间只有一个线程能够进入临界区(Critical Section),执行对共享资源的访问和操作,避免数据竞争和不一致的结果。

使用互斥锁的基本流程如下:

1.初始化互斥锁:在使用互斥锁前,需要对其进行初始化。

2.获取互斥锁:线程通过调用互斥锁的"锁定"操作,尝试获取互斥锁。如果互斥锁当前处于解锁状态,线程将获取到锁,并将其状态设置为锁定;否则,线程将被阻塞,直到锁被释放。

3.执行临界区代码:一旦线程成功获取到互斥锁,它就可以进入临界区,执行需要保护的代码,访问共享资源。

4.释放互斥锁:线程执行完临界区代码后,应该调用互斥锁的"解锁"操作来释放锁,将互斥锁的状态设置为解锁,以允许其他线程获取锁并访问共享资源。

互斥锁是一种常见的线程同步机制,用于确保共享资源在并发访问时的正确性和一致性。然而,不正确地使用互斥锁可能导致死锁和性能问题,因此,合理的设计和使用是很重要的。

四、互斥锁使用

1.初始化线程锁的方式

静态方法:

静态创建互斥锁意味着在编译时为互斥锁分配静态的内存,并在定义时进行初始化。

在C语言中,可以使用静态初始化宏PTHREAD_MUTEX_INITIALIZER来静态创建并初始化互斥锁。

pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;

动态方法:

动态创建互斥锁意味着在运行时动态分配互斥锁的内存,并使用相关函数进行初始化。

在C语言中,可以使用pthread_mutex_init函数进行动态创建和初始化互斥锁。

示例代码如下:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 销毁互斥锁
pthread_mutex_destroy(&mutex);

2.使用代码

示例代码:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
/*共享变量*/
int val = 0;
pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;
void* Thread1(void* arg)
{
    while (1)
    {
        /*加锁*/
        pthread_mutex_lock(&m_mutex);
        val++;
        /*解锁*/
        pthread_mutex_unlock(&m_mutex);
        printf("thread1 val : %d\n", val);
        sleep(1);
    }
}
void* Thread2(void* arg)
{
    while (1)
    {
        /*加锁*/
        pthread_mutex_lock(&m_mutex);
        val++;
        /*解锁*/
        pthread_mutex_unlock(&m_mutex);
        printf("thread2 val : %d\n", val);
        sleep(1);
    }
}
int main(void)
{
    pthread_t tid1;
    pthread_t tid2;   
    pthread_create(&tid1, NULL, Thread1, NULL);
    pthread_create(&tid2, NULL, Thread1, NULL);
    while(1)
    {
        sleep(1);
    }
    return 0;
}

五、死锁的产生和解决方法

1.什么是死锁

死锁是指在并发程序中,两个或多个线程或进程因为互相等待对方释放资源而无法继续执行的情况。在死锁中,每个线程都在等待其他线程释放资源,导致所有线程都被阻塞,无法进行下一步操作。

死锁通常发生在多线程或多进程环境中,涉及共享资源的竞争。当多个线程需要访问共享资源时,如果每个线程都持有一个资源并且等待其他线程释放它所需要的资源,就可能发生死锁。

2.为什么会产生死锁

死锁的产生需要满足以下四个必要条件,也被称为死锁的条件:

1.互斥条件(Mutual Exclusion):一个资源一次只能被一个线程或进程占用,即当一个线程或进程访问资源时,其他线程或进程必须等待。

2.请求与保持条件(Hold and Wait):一个线程或进程可以在保持已经获得的资源的同时请求新的资源。

3.不可抢占条件(No Preemption):已经分配给一个线程或进程的资源不能被强制性地抢占,只能在使用完之后自愿释放。

4.循环等待条件(Circular Wait):存在一种等待资源的循环链,即若干个线程或进程之间形成一种头尾相接的循环等待资源的关系。

要解决死锁问题,可以采用死锁预防、死锁避免、死锁检测和死锁恢复等策略。这些策略的目标是打破死锁产生的必要条件,从而避免或解决死锁的发生。

3.怎么解决死锁问题

1.资源有序分配:为了避免循环等待条件,可以对资源进行排序,并确保线程按照相同的顺序请求资源,从而避免产生循环等待。下面是一个示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread1: Holding mutex1...\n");
    // 线程1等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread1: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread1: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    pthread_exit(NULL);
}
void *thread2(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread2: Holding mutex1...\n");
    // 线程2等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread2: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread2: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    pthread_exit(NULL);
}
int main() {
    pthread_t tid1, tid2;
    // 创建两个线程
    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);
    // 等待线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("Program completed.\n");
    return 0;
}

在这个示例中,我们确保线程1和线程2都按照相同的顺序访问互斥锁mutex1和mutex2,从而避免了循环等待的情况。

2.避免持有并等待:一种解决死锁问题的方法是要求线程在申请资源时,释放已持有的资源。这样,当线程请求新的资源时,它没有任何资源保持,从而避免了持有并等待的情况。以下是示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread1: Holding mutex1...\n");
    pthread_mutex_unlock(&mutex1);
    // 线程1等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread1: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread1: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_exit(NULL);
}
void *thread2(void *arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread2: Holding mutex1...\n");
    pthread_mutex_unlock(&mutex1);
    // 线程2等待一段时间,模拟资源竞争
    sleep(1);
    printf("Thread2: Trying to acquire mutex2...\n");
    pthread_mutex_lock(&mutex2);
    printf("Thread2: Holding mutex2...\n");
    pthread_mutex_unlock(&mutex2);
    pthread_exit(NULL);
}
int main() {
    pthread_t tid1, tid2;
    // 创建两个线程
    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);
    // 等待线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    printf("Program completed.\n");
    return 0;
}

在这个示例中,线程1和线程2在获取并释放mutex1之后立即释放它,然后才尝试获取mutex2。这种方法确保了线程在请求新资源之前不保持任何资源,从而避免了持有并等待的情况。

总结

本篇文章主要讲解了线程同步的概念和互斥锁的使用,以及死锁的产生和解决方法。


相关文章
|
9月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
427 83
|
6月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
280 6
|
11月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
415 0
|
6月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
420 1
|
7月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
506 16
|
6月前
|
Java 调度 数据库
Python threading模块:多线程编程的实战指南
本文深入讲解Python多线程编程,涵盖threading模块的核心用法:线程创建、生命周期、同步机制(锁、信号量、条件变量)、线程通信(队列)、守护线程与线程池应用。结合实战案例,如多线程下载器,帮助开发者提升程序并发性能,适用于I/O密集型任务处理。
682 0
|
11月前
|
安全 算法 Ubuntu
Linux(openssl)环境:编程控制让证书自签的技巧。
总结:在Linux环境中,OpenSSL是一个非常实用的工具,可以帮助我们轻松地生成自签名证书。通过上述三个简单步骤,即可为内部网络、测试环境或开发环境创建自签名证书。但在公共访问场景下,建议购买经过权威认证机构签发的证书,以避免安全警告。
583 13
|
Linux C语言
Linux读写锁源码分析
本文分析了读写锁的实现原理与应用场景,基于glibc 2.17源码。读写锁通过读引用计数、写线程ID、条件变量等实现,支持读优先(默认)和写优先模式。读优先时,写锁可能饥饿;写优先时,读线程需等待写锁释放。详细解析了`pthread_rwlock_t`数据结构及加解锁流程,并通过实验验证:2000个读线程与1个写线程测试下,读优先导致写锁饥饿,写优先则正常抢占锁。
389 19
|
JavaScript Ubuntu Linux
如何在阿里云的linux上搭建Node.js编程环境?
本指南介绍如何在阿里云Linux服务器(Ubuntu/CentOS)上搭建Node.js环境,包含两种安装方式:包管理器快速安装和NVM多版本管理。同时覆盖全局npm工具配置、应用部署示例(如Express服务)、PM2持久化运行、阿里云安全组设置及外部访问验证等步骤,助你完成开发与生产环境的搭建。
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
304 17

热门文章

最新文章