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天前
|
存储 缓存 监控
Linux缓存管理:如何安全地清理系统缓存
在Linux系统中,内存管理至关重要。本文详细介绍了如何安全地清理系统缓存,特别是通过使用`/proc/sys/vm/drop_caches`接口。内容包括清理缓存的原因、步骤、注意事项和最佳实践,帮助你在必要时优化系统性能。
124 78
|
12天前
|
Linux Shell 网络安全
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
本指南介绍如何利用 HTA 文件和 Metasploit 框架进行渗透测试。通过创建反向 shell、生成 HTA 文件、设置 HTTP 服务器和发送文件,最终实现对目标系统的控制。适用于教育目的,需合法授权。
47 9
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
|
8天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
61 13
|
9天前
|
Ubuntu Linux C++
Win10系统上直接使用linux子系统教程(仅需五步!超简单,快速上手)
本文介绍了如何在Windows 10上安装并使用Linux子系统。首先,通过应用商店安装Windows Terminal和Linux系统(如Ubuntu)。接着,在控制面板中启用“适用于Linux的Windows子系统”并重启电脑。最后,在Windows Terminal中选择安装的Linux系统即可开始使用。文中还提供了注意事项和进一步配置的链接。
28 0
|
3天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
14 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
32 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
25 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
47 1