linux用户空间下的锁介绍

在Linux用户空间中,有多种锁机制可用于控制多线程或多进程之间的并发访问和资源共享。以下是一些常见的锁类型和它们的简要介绍:

一、互斥锁(Mutex):
(1)特性
互斥锁是最常见的锁类型之一,用于保护临界区(Critical Section),确保同时只有一个线程可以访问共享资源。如果一个线程已经持有互斥锁,其他线程尝试获取该锁时会被阻塞,直到锁被释放。Linux用户空间的互斥锁通常基于系统调用或库函数来实现,最常见的是使用线程库提供的互斥锁。
Linux用户空间的互斥锁是一种强大的同步工具,用于确保多线程或多进程之间的互斥访问共享资源。正确使用互斥锁可以避免竞争条件和数据损坏,并帮助实现多线程应用程序的正确同步。

互斥锁的创建和初始化:

在Linux用户空间,通常使用线程库(如pthread库)来创建和初始化互斥锁。互斥锁初始化后,可以用于多个线程之间的同步。
互斥锁的锁定和解锁:

互斥锁通过两个主要操作来实现同步:
pthread_mutex_lock():用于获取互斥锁。如果锁已经被其他线程持有,调用线程将被阻塞,直到锁可用。
pthread_mutex_unlock():用于释放互斥锁,允许其他线程获取锁并访问临界区。
互斥锁的状态:

互斥锁有两种状态:锁定(locked)和未锁定(unlocked)。一旦一个线程成功地获取了互斥锁,其他线程就必须等待锁被释放才能继续执行。
互斥锁的类型:

在Linux中,互斥锁可以有不同的类型,主要包括:
PTHREAD_MUTEX_NORMAL:不提供死锁检测或错误检测。
PTHREAD_MUTEX_ERRORCHECK:提供错误检测,允许同一线程多次获取锁,但会导致死锁。
PTHREAD_MUTEX_RECURSIVE:提供递归锁定,允许同一线程多次获取锁,但必须相同数量的解锁操作才能释放锁。
PTHREAD_MUTEX_DEFAULT:系统默认类型,通常与PTHREAD_MUTEX_NORMAL相同。
互斥锁的销毁:

当不再需要互斥锁时,应使用pthread_mutex_destroy()函数将其销毁,以释放相关资源。
互斥锁的性能:

互斥锁的性能取决于实现和配置。在高度竞争的情况下,频繁的锁定和解锁操作可能会导致性能瓶颈,因此需要谨慎设计并发算法以减小锁的粒度或考虑其他同步机制。
注意事项:

使用互斥锁时,要小心处理死锁情况,即多个线程相互等待对方释放锁的情况。可以使用良好的设计模式来避免死锁。
在使用互斥锁时要避免长时间持有锁,以允许其他线程有机会访问临界区,以提高并发性。
(2)使用场景
Linux用户空间互斥锁适用于多种使用场景,其中包括但不限于以下几种:

共享资源的访问控制:互斥锁用于确保多个线程可以安全地访问共享资源,如全局变量、数据结构、文件等。在任何时刻,只有一个线程能够持有互斥锁,其他线程需要等待锁的释放。

避免竞态条件:竞态条件是一种可能导致不确定行为或数据破坏的情况,通常发生在多个线程尝试同时访问共享资源时。互斥锁可以用来消除竞态条件,确保只有一个线程能够修改或访问关键资源。

线程安全的数据结构:互斥锁可以用于保护自定义数据结构,如队列、堆栈、哈希表等,以确保多线程并发地操作这些数据结构时不会出现问题。这种情况下,只有执行特定操作的线程能够获得锁。

控制线程间的顺序:互斥锁可以用于控制多个线程之间的执行顺序,例如,一个线程需要等待另一个线程完成某项任务后才能继续执行。通过在关键部分使用互斥锁,可以实现线程之间的协作。

临界区保护:互斥锁通常用于保护临界区,即一段代码或操作,在任何时刻只有一个线程可以执行。这确保了在多线程环境中的可靠性和一致性。

资源管理:互斥锁可以用于管理有限资源的访问,例如数据库连接池、线程池等。只有在资源可用时才能获取锁,否则线程需要等待。

避免死锁:通过仔细设计锁的获取顺序,互斥锁可以用于避免死锁情况,即多个线程相互等待对方释放的锁而无法继续执行的情况。

总之,Linux用户空间互斥锁在多线程编程中广泛应用,用于确保共享资源的安全访问、避免竞态条件、保护数据结构、控制线程的执行顺序等多种场景。使用互斥锁可以提高多线程程序的稳定性和可靠性,但需要小心处理锁的获取和释放,以避免死锁和性能问题。

(3)使用例子
如下用了pthread_mutex_t数据类型来定义互斥锁,并使用pthread_mutex_init函数进行初始化。然后,创建了两个线程,每个线程都会执行thread_function函数,该函数会使用互斥锁来保护对shared_resource的访问。每个线程在访问共享资源之前会先上锁,然后在访问完毕后解锁,以确保线程之间的互斥访问。
最后,在主线程中等待两个工作线程执行完毕,并销毁互斥锁以释放资源。

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

// 共享资源
int shared_resource = 0;

// 定义互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        // 上锁
        pthread_mutex_lock(&mutex);

        // 访问共享资源
        shared_resource++;
        printf("Thread %d: Shared Resource = %d\n", thread_id, shared_resource);

        // 解锁
        pthread_mutex_unlock(&mutex);

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    // 创建两个线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}


二、读写锁(Read-Write Lock):
(1)特性
Linux用户空间的读写锁(Read-Write Lock)是一种同步机制,用于管理多线程或多进程对共享资源的并发访问。与互斥锁不同,读写锁允许多个线程同时读取共享资源(只要它们是读操作),但只允许一个线程写入共享资源,一旦有一个线程试图执行写操作,其他所有线程都将被阻塞,直到写操作完成。这种锁的使用可以提高并发性,特别是在读操作频繁而写操作较少的情况下。

读写锁的创建和初始化:

读写锁通常是通过线程库(如pthread库)提供的函数来创建和初始化的。初始化后,读写锁可以用于多个线程之间的同步。
读写锁的锁定和解锁:

读写锁具有三种主要操作:
pthread_rwlock_rdlock():用于获取读锁,允许多个线程同时获取读锁,但阻止写锁的获取。
pthread_rwlock_wrlock():用于获取写锁,只有一个线程可以获取写锁,且在写锁被释放之前不允许其他线程获取读锁或写锁。
pthread_rwlock_unlock():用于释放读锁或写锁,允许其他线程获取锁。
读写锁的状态:

读写锁有两种状态:读锁状态和写锁状态。多个线程可以同时持有读锁,但只有一个线程可以持有写锁。当写锁被持有时,任何线程尝试获取写锁或读锁都会被阻塞。
读写锁的性能:

读写锁适用于读操作频繁而写操作较少的情况,因为它允许多个线程同时读取共享资源,提高了并发性。
写操作会导致读写锁的升级,即从读锁升级为写锁,这可能会引入一些性能开销。因此,需要根据应用程序的访问模式来选择使用读写锁。
注意事项:

使用读写锁时,需要小心处理竞争条件,确保写操作不会破坏数据的一致性。
读写锁不应该被滥用,如果写操作非常频繁,可能会降低性能,因为写锁的获取会阻塞其他线程的读取操作。
对于读写锁的正确使用,需要深入理解应用程序的访问模式,以确定何时使用读锁和写锁。
总之,Linux用户空间的读写锁是一种强大的同步工具,用于管理共享资源的并发访问。通过允许多个线程同时读取共享资源,它提高了多线程应用程序的并发性能,但需要小心处理写操作以确保数据的一致性。选择使用读写锁还要根据应用程序的特点和访问模式来进行权衡和设计。

(2)使用场景
Linux用户空间的读写锁的主要目的是提高读操作的并发性,因为在多线程环境中,读操作通常是线程安全的,而写操作需要互斥访问以确保一致性。以下是Linux用户空间读写锁的一些常见使用场景:

读多写少的共享资源:

读写锁适用于共享资源被频繁读取而写入较少的情况。多个线程可以同时获取读锁,从而提高了读操作的并发性,但只有在没有读锁或写锁被持有时,才能获取写锁。
数据缓存:

在具有数据缓存的应用程序中,读写锁常用于保护缓存。多个线程可以同时读取缓存数据,而只有一个线程能够更新缓存。这提高了数据的读取效率,同时确保数据的一致性。
数据库连接池:

当多个线程需要从数据库连接池中获取连接时,读写锁可用于确保多个线程可以同时获取连接(读操作),但只有一个线程能够释放或回收连接(写操作)。
配置管理:

在应用程序中,多个线程可能需要读取配置信息,而只有一个线程可以修改配置。读写锁可以用于保护配置信息,确保读取线程之间不会相互干扰。
文件访问:

在多线程文件访问的情况下,读写锁可以用于允许多个线程并发读取文件内容,但只有一个线程能够写入文件。
统计信息更新:

当多个线程需要更新统计信息或日志时,读写锁可用于确保多线程可以并发地读取统计信息,但只有一个线程能够更新它们。
资源管理:

读写锁可以用于管理有限资源的访问,例如线程池、内存池等。多个线程可以同时请求资源的访问权限,但只有一个线程能够分配或释放资源。
需要注意的是,读写锁在某些情况下可能会引入写操作一直等待的问题,而读操作频繁进行。因此,在设计多线程应用程序时,需要根据具体的使用场景和性能需求来选择合适的同步机制。读写锁适用于特定情况下,可以提高程序的性能和并发性。

(3)使用例子
如下例子中,定义了一个共享的整数 shared_data,并使用 pthread_rwlock_t 类型的读写锁 rwlock 进行保护。创建了两个读线程和两个写线程,每个线程都会分别执行 read_thread 和 write_thread 函数。
读线程在读取 shared_data 之前会上读锁,以确保多个读线程可以同时访问。写线程在修改 shared_data 之前会上写锁,以确保在写操作期间不允许其他线程访问。
最后,在主线程中等待所有线程执行完毕,并销毁读写锁以释放资源。

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

// 共享资源
int shared_data = 0;

// 定义读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

// 读线程函数
void *read_thread(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        // 上读锁
        pthread_rwlock_rdlock(&rwlock);

        // 读取共享资源
        printf("Read Thread %d: Shared Data = %d\n", thread_id, shared_data);

        // 解读锁
        pthread_rwlock_unlock(&rwlock);

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

// 写线程函数
void *write_thread(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        // 上写锁
        pthread_rwlock_wrlock(&rwlock);

        // 修改共享资源
        shared_data++;
        printf("Write Thread %d: Incremented Shared Data to %d\n", thread_id, shared_data);

        // 解写锁
        pthread_rwlock_unlock(&rwlock);

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t read_threads[2];
    pthread_t write_threads[2];
    int thread_ids[2] = {1, 2};

    // 创建两个读线程和两个写线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&read_threads[i], NULL, read_thread, &thread_ids[i]) != 0) {
            perror("pthread_create (read_thread)");
            exit(EXIT_FAILURE);
        }

        if (pthread_create(&write_threads[i], NULL, write_thread, &thread_ids[i]) != 0) {
            perror("pthread_create (write_thread)");
            exit(EXIT_FAILURE);
        }
    }

    // 等待读线程和写线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(read_threads[i], NULL) != 0) {
            perror("pthread_join (read_thread)");
            exit(EXIT_FAILURE);
        }

        if (pthread_join(write_threads[i], NULL) != 0) {
            perror("pthread_join (write_thread)");
            exit(EXIT_FAILURE);
        }
    }

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}


三、条件变量(Condition Variable):
(1)特性
Linux用户空间的条件变量(Condition Variable)是一种同步机制,通常与互斥锁一起使用,用于在多线程或多进程应用程序中实现线程之间的通信和同步。条件变量用于等待某个特定条件的发生,并在条件满足时通知等待的线程继续执行。

条件变量的创建和初始化:

条件变量通常是通过线程库(如pthread库)提供的函数来创建和初始化的。初始化后,条件变量可以用于多个线程之间的同步。
条件变量的等待和通知:

条件变量提供了以下两个主要操作:
pthread_cond_wait():用于使线程等待某个条件的发生。当调用此函数时,线程会释放它持有的互斥锁,然后阻塞等待条件变量的通知。
pthread_cond_signal() 或 pthread_cond_broadcast():用于通知一个或多个等待在条件变量上的线程,条件已经满足,可以继续执行。pthread_cond_signal() 通知等待队列中的一个线程,而 pthread_cond_broadcast() 通知等待队列上的所有线程。
条件变量和互斥锁的关系:

条件变量通常与互斥锁一起使用,以确保线程在等待条件变量时能够安全地释放并重新获取互斥锁,避免竞争条件。典型的用法如下:
线程在获取互斥锁后,检查条件是否满足。
如果条件未满足,线程调用 pthread_cond_wait() 释放互斥锁并等待条件满足的通知。
另一个线程在条件满足时,通过 pthread_cond_signal() 或 pthread_cond_broadcast() 来通知等待的线程。
等待的线程被唤醒后,会重新获取互斥锁,并检查条件,如果条件仍然不满足,它可能继续等待或返回。
条件变量的性能:

条件变量的性能较高,因为它们允许线程在等待条件时休眠,不浪费CPU资源。唤醒线程时,只有真正需要被唤醒的线程会被唤醒。
注意事项:

使用条件变量时,需要小心处理竞争条件,确保线程在等待条件满足时和通知其他线程时互斥地持有互斥锁。
在某些情况下,条件变量可能会产生虚假唤醒,即线程在没有条件满足的情况下被唤醒。因此,等待线程应该在唤醒后重新检查条件。
总之,Linux用户空间的条件变量是一个有力的工具,用于实现多线程或多进程之间的通信和同步。它们通常与互斥锁结合使用,可以帮助线程等待某些条件的发生,而不浪费CPU资源。但要小心处理竞争条件和虚假唤醒,以确保正确的同步和通信。

(2)使用场景
Linux用户空间条件变量允许一个线程在满足特定条件之前等待,而其他线程可以在条件满足时通知等待线程继续执行。以下是一些常见的Linux用户空间条件变量的使用场景:

生产者-消费者问题:

条件变量常用于解决生产者-消费者问题。生产者线程负责生产数据,消费者线程负责消费数据。使用条件变量,当缓冲区为空时,消费者线程可以等待条件变量,直到生产者线程放入数据并通知条件变量。反之,当缓冲区满时,生产者线程可以等待条件变量,直到消费者线程消费数据并通知条件变量。
多线程任务协作:

在多线程应用程序中,多个线程可能需要协同工作以完成复杂的任务。条件变量可以用于等待其他线程的信号或通知以执行下一步操作。例如,一个线程可能等待其他线程完成初始化工作,然后再继续执行。
线程池管理:

线程池通常用于并发处理多个任务。条件变量可以用于等待任务队列中有新任务可用时通知工作者线程开始执行任务。工作者线程可以等待条件变量以等待任务的到来。
任务完成通知:

在一些情况下,一个线程可能需要等待其他线程完成特定任务,然后再继续执行。条件变量可用于实现等待其他线程完成并通知的机制。
资源分配和释放:

在多线程环境中,资源的分配和释放可能需要协同工作,以确保资源的正确使用。条件变量可以用于等待某个资源可用并通知资源释放的情况。
定时等待:

条件变量通常可以与超时机制结合使用,以实现定时等待。线程可以等待条件变量,但在一定时间内如果条件未满足,则可以超时返回。
线程同步:

条件变量还可用于多线程之间的同步,以确保线程按照特定顺序执行或等待其他线程完成某些操作。
需要注意的是,使用条件变量时,通常需要与互斥锁一起使用,以确保线程之间的安全同步。条件变量的使用需要小心,以避免死锁和竞态条件。正确的条件变量使用可以提高多线程程序的性能和可靠性,确保线程之间协作良好。

(3)使用例子
在下面例子中使用了一个互斥锁 mutex 来保护共享资源 shared_resource,并使用条件变量 cond 来进行线程之间的协作。有两个线程,一个等待条件满足,另一个改变条件。
等待条件的线程首先上锁,然后在一个循环中等待条件满足。条件不满足时,它调用 pthread_cond_wait 来释放互斥锁并阻塞自己,直到条件满足并收到通知才继续执行。
改变条件的线程模拟了一些工作,然后上锁,改变了条件 shared_resource 的值,并通过 pthread_cond_signal 发出通知,告诉等待线程条件已经满足。
最后,主线程等待两个线程结束,并销毁互斥锁和条件变量。这个例子演示了如何使用条件变量来实现线程之间的协作和同步,确保线程在特定条件下等待和通知。

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

// 共享资源
int shared_resource = 0;

// 互斥锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 线程函数 - 等待条件
void *wait_thread(void *arg) {
    // 上锁
    pthread_mutex_lock(&mutex);

    // 等待条件满足
    printf("Waiting for the condition to be met...\n");
    while (shared_resource < 5) {
        pthread_cond_wait(&cond, &mutex);
    }

    // 条件满足后继续执行
    printf("Condition met! Shared Resource = %d\n", shared_resource);

    // 解锁
    pthread_mutex_unlock(&mutex);

    return NULL;
}

// 线程函数 - 改变条件
void *change_thread(void *arg) {
    // 模拟一些工作
    sleep(2);

    // 上锁
    pthread_mutex_lock(&mutex);

    // 改变条件
    shared_resource = 5;
    printf("Changed the condition. Shared Resource = %d\n", shared_resource);

    // 通知等待的线程条件已满足
    pthread_cond_signal(&cond);

    // 解锁
    pthread_mutex_unlock(&mutex);

    return NULL;
}

int main() {
    pthread_t wait_thread_id, change_thread_id;

    // 创建等待条件的线程
    if (pthread_create(&wait_thread_id, NULL, wait_thread, NULL) != 0) {
        perror("pthread_create (wait_thread)");
        exit(EXIT_FAILURE);
    }

    // 创建改变条件的线程
    if (pthread_create(&change_thread_id, NULL, change_thread, NULL) != 0) {
        perror("pthread_create (change_thread)");
        exit(EXIT_FAILURE);
    }

    // 等待线程结束
    if (pthread_join(wait_thread_id, NULL) != 0) {
        perror("pthread_join (wait_thread)");
        exit(EXIT_FAILURE);
    }

    if (pthread_join(change_thread_id, NULL) != 0) {
        perror("pthread_join (change_thread)");
        exit(EXIT_FAILURE);
    }

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}


四、自旋锁(Spin Lock):
(1)特性
Linux用户空间的自旋锁(Spin Lock)是一种同步机制,用于在多线程或多进程应用程序中实现临界区的保护和同步。与互斥锁不同,自旋锁不会将线程阻塞,而是在锁被其他线程占用时,通过忙等待方式不断尝试获取锁,直到成功获取为止。自旋锁适用于对临界区共享资源的竞争非常短暂的情况,因为它会持续占用CPU资源。自旋锁通常用于内核开发。

自旋锁的创建和初始化:

自旋锁通常是通过线程库(如pthread库)提供的函数来创建和初始化的。初始化后,自旋锁可以用于多个线程之间的同步。
自旋锁的锁定和解锁:

自旋锁主要提供以下两个操作:
pthread_spin_lock():用于获取自旋锁。如果锁已经被其他线程持有,调用线程将进入忙等待状态,不断尝试获取锁。
pthread_spin_unlock():用于释放自旋锁,允许其他线程获取锁并访问临界区。
自旋锁的状态:

自旋锁有两种状态:锁定(locked)和未锁定(unlocked)。一旦一个线程成功地获取了自旋锁,其他线程将不断尝试获取锁,直到锁被释放。
自旋锁的性能:

自旋锁适用于对临界区的竞争非常短暂的情况,因为它会持续占用CPU资源。在竞争激烈的情况下,自旋锁可能会导致CPU资源的浪费,因此需要谨慎使用。
自旋锁通常比互斥锁的开销更小,因为它避免了线程的阻塞和唤醒,但只适用于特定的应用场景。
注意事项:

使用自旋锁时需要小心处理竞争条件,确保临界区被正确保护,以避免数据损坏。
自旋锁不适用于等待长时间的情况,因为它会持续占用CPU资源,可能导致性能下降。
总之,Linux用户空间的自旋锁是一种用于保护临界区的同步机制,通过忙等待方式尝试获取锁,适用于对临界区的竞争非常短暂的情况。虽然自旋锁的开销较小,但需要谨慎使用,以避免浪费CPU资源和处理竞争条件。选择使用自旋锁还需要深入理解应用程序的访问模式和性能需求。

(2)使用场景
Linux用户空间自旋锁与互斥锁不同,自旋锁在尝试获取锁时不会将线程挂起,而是通过不断尝试获取锁的方式来自旋等待,直到锁可用。自旋锁适用于特定的使用场景,通常在以下情况下使用:

短期等待:

自旋锁适用于短期等待的情况,即线程需要等待锁的时间非常短。在这种情况下,将线程挂起和恢复的开销可能比自旋等待的开销更大,因此自旋锁可以减少上下文切换的开销。
多核处理器:

自旋锁在多核处理器上的性能更好,因为在一个核上自旋等待时,其他核上的线程可以继续执行。在单核处理器上,自旋等待可能会导致资源争用,影响性能。
低竞争情况:

自旋锁适用于低竞争情况,即对锁的争用很少。在高竞争情况下,自旋锁可能会导致大量的自旋等待,浪费CPU资源。
实时系统:

实时系统通常要求线程的响应时间非常稳定,而自旋锁可以确保线程在获取锁之前不会被挂起,因此适用于某些实时系统中的关键路径操作。
锁的期望保持时间短:

自旋锁适用于锁的期望保持时间非常短的情况。如果锁的保持时间较长,自旋锁可能会导致CPU资源的浪费,因为线程会在自旋等待期间持续占用CPU。
防止睡眠:

有些情况下,睡眠会导致问题,例如在中断处理程序中不能睡眠。自旋锁可以用于在这些情况下防止睡眠。
需要注意的是,自旋锁应该谨慎使用,因为它们可能导致CPU资源的浪费,并且在高竞争情况下可能会导致性能问题。在选择锁的类型时,需要考虑线程的等待时间、竞争程度、系统架构等因素。在大多数情况下,互斥锁更适合一般的多线程同步需求,而自旋锁适用于上述特定场景。

(3)使用例子
在下面例子中使用 pthread_spinlock_t 数据类型来定义自旋锁 spinlock 并使用 pthread_spin_init 进行初始化。然后,创建了两个线程,每个线程都会执行 thread_function 函数。
在 thread_function 函数中,线程会不断尝试获取自旋锁 spinlock,并在成功获取锁后访问共享资源 shared_data,然后释放锁。自旋锁的关键特点是,如果锁已经被其他线程持有,那么尝试获取锁的线程会一直自旋等待,直到锁被释放。
最后,在主线程中等待两个工作线程执行完毕,并销毁自旋锁以释放资源。需要注意的是,自旋锁适用于短期等待的情况,如果等待时间较长,自旋锁可能会导致CPU资源的浪费。因此,选择是否使用自旋锁需要根据具体的应用场景和性能要求来决定。

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

// 共享资源
int shared_data = 0;

// 定义自旋锁
pthread_spinlock_t spinlock;

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        // 尝试获取自旋锁
        pthread_spin_lock(&spinlock);

        // 访问共享资源
        shared_data++;
        printf("Thread %d: Shared Data = %d\n", thread_id, shared_data);

        // 释放自旋锁
        pthread_spin_unlock(&spinlock);

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    // 初始化自旋锁
    if (pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED) != 0) {
        perror("pthread_spin_init");
        exit(EXIT_FAILURE);
    }

    // 创建两个线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    // 销毁自旋锁
    pthread_spin_destroy(&spinlock);

    return 0;
}
五、信号量(Semaphore):
(1)特性
在Linux用户空间,信号量是一种常见的同步机制,是一种计数器,用于控制多个线程或多个进程之间的并发访问共享资源。信号量通常用于线程之间的通信和同步,允许一个线程在等待某个特定条件满足时被阻塞,而另一个线程可以在条件满足时通知等待的线程。信号量可以是二进制信号量(只有0和1值)或计数信号量(可以是任意非负整数)。线程可以通过等待和释放信号量来同步对共享资源的访问。

信号量的创建和初始化:

信号量通常是通过系统调用或库函数来创建和初始化的。初始化后,信号量可以用于多个线程或进程之间的同步。
信号量的类型:

信号量有两种类型:
二进制信号量:也称为互斥信号量,只有两个值,通常用于实现互斥锁的功能。它的值通常为0表示锁定,为1表示解锁。
计数信号量:可以拥有任意非负整数值,用于控制资源的可用数量。
信号量的操作:

信号量提供以下基本操作:
sem_wait():用于等待信号量,如果信号量的值大于0,则将其减1并继续执行;如果值为0,则阻塞等待。
sem_post():用于释放信号量,将其值加1,如果有其他线程或进程正在等待这个信号量,它会唤醒其中一个。
sem_getvalue():用于获取信号量的当前值,可以用于检查信号量的状态。
信号量的等待和通知:

信号量通常与条件变量一起使用,以实现线程之间的等待和通知机制。一个线程可以等待某个条件,而另一个线程可以通过增加信号量的值来通知等待线程。
信号量的性能:

信号量提供了较高的灵活性,但相对于互斥锁和自旋锁来说,其开销较大。每次等待和通知都涉及到系统调用,因此在高度竞争的情况下,性能可能较差。
注意事项:

使用信号量时,需要确保正确处理竞争条件和资源的管理,以避免死锁或数据损坏。
计数信号量可以用于控制资源的可用数量,而不仅仅是锁定和解锁。这对于线程池等应用非常有用。
总之,Linux用户空间的信号量是一种强大的同步工具,用于控制多个线程或多个进程之间的并发访问共享资源。虽然信号量提供了高度的灵活性,但它也有较高的开销,特别是在高度竞争的情况下。选择使用信号量还需要深入理解应用程序的访问模式和性能需求。

(2)使用场景
Linux用户空间信号量可以用于协调不同线程或进程之间的操作,确保资源的安全访问和避免竞态条件。以下是一些常见的Linux用户空间信号量的使用场景:

资源限制和控制:

信号量可用于控制对有限资源的访问,例如线程池或连接池。它可以用于限制同时访问资源的线程或进程数量,确保资源不会被过度占用。
生产者-消费者问题:

信号量常用于解决生产者-消费者问题。生产者线程负责生产数据,而消费者线程负责消费数据。信号量用于通知消费者何时可以消费生产者生产的数据,以及何时可以生产更多的数据。
进程间通信:

信号量可以用于实现进程间通信(IPC),允许不同进程之间同步操作。例如,一个进程可以等待另一个进程完成某项任务,然后再继续执行。
线程同步:

在多线程应用程序中,信号量可用于同步线程的执行。它可以用于等待其他线程完成某些操作,然后再继续执行。
资源池管理:

当多个线程或进程需要共享资源池(如内存池)时,信号量可以用于管理资源的分配和释放。线程或进程可以等待信号量以获取资源,并在使用后释放资源。
互斥访问共享资源:

信号量通常与互斥锁结合使用,以确保对共享资源的互斥访问。线程或进程在访问共享资源之前必须获取互斥锁,然后在使用完资源后释放互斥锁。
顺序控制:

信号量可以用于控制多个线程或进程的执行顺序。例如,一个线程可以等待另一个线程执行完特定操作后才能继续执行。
定时等待:

信号量通常可以与超时机制结合使用,以实现定时等待。线程或进程可以等待信号量,但在一定时间内如果信号量未被释放,则可以超时返回。
需要注意的是,使用信号量时,程序员需要小心处理信号量的等待和通知,以确保正确的同步。信号量的使用场景通常涉及多个线程或进程之间的协作和同步,确保资源的安全访问和避免竞态条件。根据具体的应用需求和线程模型,选择适当的信号量操作是很重要的。

(3)使用例子
1、二进制信号量:
在下面例子中使用 sem_t 数据类型来定义信号量 semaphore,并使用 sem_init 进行初始化。然后,创建了两个线程,每个线程都会执行 thread_function 函数。
在 thread_function 函数中,线程首先等待信号量 semaphore。如果信号量的值大于零,线程会减少信号量的值并继续执行,否则线程将被阻塞,直到信号量的值变为非零。这种机制允许多个线程同时进入临界区,但需要确保同时访问共享资源的线程数量受到信号量的控制。
在访问共享资源后,线程会增加信号量的值,以允许其他线程进入临界区。
最后,在主线程中等待两个工作线程执行完毕,并销毁信号量以释放资源。这个示例演示了如何使用信号量来控制线程之间的并发访问,确保线程按照一定的顺序访问共享资源。

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

// 共享资源
int shared_resource = 0;

// 定义信号量
sem_t semaphore;

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        // 等待信号量
        sem_wait(&semaphore);

        // 访问共享资源
        shared_resource++;
        printf("Thread %d: Shared Resource = %d\n", thread_id, shared_resource);

        // 释放信号量
        sem_post(&semaphore);

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    // 初始化信号量
    if (sem_init(&semaphore, 0, 1) != 0) {
        perror("sem_init");
        exit(EXIT_FAILURE);
    }

    // 创建两个线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    // 销毁信号量
    sem_destroy(&semaphore);

    return 0;
}
2、计数信号量
计数信号量(Counting Semaphore)是一种信号量,允许多个线程或进程同时访问共享资源,而不仅限于二进制信号量的两种状态。
如下例子使用 sem_t 数据类型来定义计数信号量 semaphore,并使用 sem_init 进行初始化。计数信号量的初始值为2,这表示允许同时有两个线程访问共享资源。
然后,创建了两个线程,每个线程都会执行 thread_function 函数。在 thread_function 函数中,线程首先等待信号量 semaphore。如果信号量的值大于零,线程会减少信号量的值并继续执行,允许多个线程同时进入临界区。每次线程访问共享资源后,都会释放信号量,增加信号量的值。
最后,在主线程中等待两个工作线程执行完毕,并销毁计数信号量以释放资源。这个示例演示了如何使用计数信号量来控制多个线程的并发访问,允许指定数量的线程同时访问共享资源。

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

// 共享资源
int shared_resource = 0;

// 定义计数信号量
sem_t semaphore;

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        // 等待信号量
        sem_wait(&semaphore);

        // 访问共享资源
        shared_resource++;
        printf("Thread %d: Shared Resource = %d\n", thread_id, shared_resource);

        // 释放信号量
        sem_post(&semaphore);

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    // 初始化计数信号量,初始值为2
    if (sem_init(&semaphore, 0, 2) != 0) {
        perror("sem_init");
        exit(EXIT_FAILURE);
    }

    // 创建两个线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    // 销毁计数信号量
    sem_destroy(&semaphore);

    return 0;
}
六、屏障(Barrier):
(1)特性
Linux用户空间的屏障(Barrier)是一种同步机制,用于协调多个线程或多进程应用在执行过程中的同步点,同步一组线程,确保它们在达到某个点之前都不会继续执行。屏障允许一组线程等待彼此,直到所有线程都到达屏障之后,才允许它们继续执行。这种同步机制通常用于分阶段的任务并行化,确保在下一个阶段开始之前,所有线程都已经完成了当前阶段的工作。

屏障的创建和初始化:

屏障通常是通过系统调用或库函数来创建和初始化的。初始化后,它可以用于多个线程或进程之间的同步。
屏障的使用场景:

屏障通常用于多个线程或进程需要在执行过程中等待其他线程或进程达到某个同步点的情况。例如,在一个计算任务中,多个线程可以并行计算不同部分的结果,但在计算完成之前需要等待其他线程完成以进行下一步的数据整合。
屏障的等待和解除阻塞:

屏障提供以下主要操作:
pthread_barrier_wait():用于线程等待在屏障上。当一个线程调用pthread_barrier_wait()时,它会阻塞,直到足够多的线程都到达屏障。
当足够多的线程到达屏障后,所有等待的线程会同时解除阻塞,然后继续执行。
屏障的计数:

屏障通常需要指定需要等待的线程数量,即需要达到屏障的线程数目。一旦足够多的线程都到达屏障,屏障将被打破,所有线程可以继续执行。
屏障的性能:

屏障的性能取决于等待在屏障上的线程数量。如果线程数量非常大,可能会导致性能问题,因为所有线程都需要等待,直到满足屏障的条件。
注意事项:

使用屏障时,需要确保正确处理线程数量和屏障的等待和通知,以确保正确的同步。
如果线程的数量非常大或等待时间不确定,可能需要考虑其他同步机制,以避免性能问题。
总之,Linux用户空间的屏障是一种同步机制,用于协调多个线程或进程在执行过程中的同步点。它允许一组线程等待彼此,然后同时解除阻塞,继续执行,通常用于分阶段的任务并行化。使用屏障需要根据具体的应用需求和线程数量来进行权衡和设计。

(2)使用场景
Linux用户空间的屏障用于协调多个线程或进程在执行过程中的同步点。屏障允许一组线程等待彼此,直到所有线程都到达屏障之后,才允许它们继续执行。以下是一些常见的Linux用户空间屏障的使用场景:

任务并行化:

在多线程或多进程的任务并行化中,屏障可以用于划分不同阶段的任务。每个线程在完成当前阶段的工作后等待屏障,确保所有线程都完成了当前阶段后再进入下一阶段。这有助于确保各个线程之间的同步和协作。
迭代计算:

屏障可用于迭代计算的同步。在并行迭代中,每个线程可以在每轮迭代后等待屏障,以确保所有线程都完成了当前轮的计算,然后再进行下一轮。
数据收集和合并:

在分布式系统中,多个节点可能同时收集数据并将其合并到一个共享数据结构中。屏障可以用于确保所有节点都完成数据收集后再进行数据合并操作。
线程池任务协作:

在线程池中,屏障可以用于协调工作者线程的执行。当线程池中的任务被分发给工作者线程执行时,每个线程可以等待屏障,以确保所有任务都已分发并完成后再继续执行其他操作。
多阶段计算:

在某些计算任务中,需要进行多个阶段的计算,每个阶段可能需要不同的线程协作。屏障可用于确保每个阶段都完成后,线程可以进入下一个阶段。
模拟:

在模拟应用程序中,多个线程可能模拟不同的系统组件,它们需要同步以模拟系统的行为。屏障可以用于确保所有线程在进行下一步模拟之前都已准备好。
线程间数据传递:

在一些多线程应用中,线程之间需要传递数据,但需要确保所有线程都准备好接收数据。屏障可以用于确保所有线程都已就绪,然后进行数据传递。
需要注意的是,使用屏障时,需要确保正确处理线程数量和屏障的等待和通知,以确保正确的同步。屏障通常在多线程或多进程之间建立同步点,确保它们在某个共同的阶段或事件之后继续执行。选择使用屏障需要根据具体的应用需求和线程模型来进行权衡和设计。

(3)使用例子
如下例子中使用 pthread_barrier_t 数据类型来定义屏障 barrier,并使用 pthread_barrier_init 进行初始化。创建了四个线程,每个线程都会执行 thread_function 函数。
在 thread_function 函数中,线程首先执行一些模拟工作(这里用 sleep(2) 模拟)。然后,它会在到达同步点之前打印一条消息,表示准备好了。接下来,线程调用 pthread_barrier_wait 等待其他线程到达屏障。
当所有线程都到达屏障时,它们都会被释放,并继续执行后续代码。在本示例中,每个线程在通过屏障后打印一条消息。
最后,主线程等待所有工作线程执行完毕,并销毁屏障以释放资源。这个示例演示了如何使用屏障来确保多个线程在特定的同步点同时到达,以便进行协同工作。

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

#define NUM_THREADS 4

// 定义屏障
pthread_barrier_t barrier;

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    printf("Thread %d: Start\n", thread_id);

    // 模拟一些工作
    sleep(2);

    printf("Thread %d: Ready to reach the barrier\n", thread_id);

    // 等待所有线程到达屏障
    pthread_barrier_wait(&barrier);

    printf("Thread %d: Passed the barrier\n", thread_id);

    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];

    // 初始化屏障,设置等待线程数为NUM_THREADS
    if (pthread_barrier_init(&barrier, NULL, NUM_THREADS) != 0) {
        perror("pthread_barrier_init");
        exit(EXIT_FAILURE);
    }

    // 创建多个线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        thread_ids[i] = i;
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < NUM_THREADS; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    // 销毁屏障
    pthread_barrier_destroy(&barrier);

    return 0;
}


七、读-改-写锁(Compare-and-Swap,CAS):
(1)特性
Linux用户空间的CAS(Compare-And-Swap)操作是一种原子操作,用于实现多线程或多进程之间的同步和共享数据的安全更新。CAS操作允许程序员在没有锁的情况下进行原子性的读取、修改和写入操作,通常用于实现无锁数据结构和一些同步原语。
CAS操作用于实现无锁算法,通常用于实现自定义锁或数据结构。它比传统锁的开销更小,但也更复杂。

CAS操作的原理:

CAS操作是一种原子性的操作,它会比较一个内存位置的当前值与预期值。如果它们相等,就将新值写入该内存位置;否则,不做任何操作。整个操作是原子的,不会被其他线程或进程中断。
CAS操作的函数:

在Linux用户空间,CAS操作通常使用一些原子操作的库函数或内联汇编指令来实现,具体取决于编程语言和体系结构。例如,C/C++中可以使用GCC内置的__sync_bool_compare_and_swap函数或C11标准中的atomic_compare_exchange_strong函数。
CAS操作的用途:

CAS操作可以用于实现多线程之间的同步机制,例如实现自旋锁、无锁队列、无锁堆栈等数据结构。
CAS操作还可以用于实现一些原子操作,如计数器的原子递增和递减。
它还可用于实现乐观锁,允许多个线程同时读取和修改数据,但只有在写入数据时检测到冲突时才会进行回滚操作。
CAS操作的注意事项:

CAS操作通常需要提供预期值和新值,以确保只有在预期值与当前内存位置的值匹配时才会执行更新。这可以防止竞态条件和数据不一致。
CAS操作需要小心处理循环,以防止无限自旋。通常,程序员需要在CAS操作失败时进行重试,但需要限制重试次数。
在多核系统上,CAS操作的性能通常较好,但在高度竞争的情况下可能会导致性能下降,因为它会引入内存总线的竞争。
总之,Linux用户空间的CAS操作是一种强大的原子操作,用于实现多线程或多进程之间的同步和共享数据的安全更新。它可以用于各种用途,包括实现无锁数据结构和一些原子操作。但需要小心处理预期值和循环,以确保正确性和性能。CAS操作通常是多核系统上实现高并发的重要工具。

(2)使用场景
Linux用户空间CAS操作用于实现多线程或多进程之间的同步和共享数据的安全更新。CAS操作允许程序员在没有锁的情况下进行原子性的读取、修改和写入操作,通常用于以下一些使用场景:

实现无锁数据结构:

CAS操作常用于实现无锁数据结构,如无锁队列、无锁堆栈、无锁散列表等。这些数据结构允许多个线程在不使用锁的情况下并发地访问和修改数据。
并发计数器:

CAS操作可用于实现并发计数器,允许多个线程并发地递增或递减计数器的值,而不需要锁定整个计数器。
实现自旋锁:

CAS操作可以用于实现自旋锁,通过CAS操作来竞争锁的所有权,从而避免了线程挂起和恢复的开销。这在一些情况下可以提高性能,特别是对于短期竞争的情况。
标记与操作:

CAS操作可以用于标记数据结构中的节点或对象,并在多线程环境中进行操作。例如,标记节点作为已删除的标志,并使用CAS来进行节点删除操作。
避免ABA问题:

CAS操作在某些情况下可以用于解决ABA问题,即在多线程环境中,一个值从A变成B,然后再次变回A。通过CAS操作,可以检查值是否与预期值匹配,以避免误认为数据未变化。
实现乐观锁:

CAS操作通常与乐观锁机制一起使用,允许多个线程同时读取数据,但只有在写入数据时检测到冲突时才会进行回滚操作。
内存管理:

在某些情况下,CAS操作可以用于线程间的内存管理,例如分配和释放内存块,以避免锁的使用。
需要注意的是,CAS操作通常需要提供预期值和新值,以确保只有在预期值与当前内存位置的值匹配时才会执行更新。这可以防止竞态条件和数据不一致。此外,CAS操作需要小心处理循环,以防止无限自旋。在使用CAS操作时,需要根据具体的应用需求和性能需求来选择使用的场景。

(3)使用例子
CAS操作通常在原子性地读取、修改和写入共享数据时使用,用于实现无锁数据结构。虽然CAS操作本身是一个底层的原子操作,但是它可以用来实现各种高级的同步机制。
如下例子中使用了C标准库中的stdatomic.h头文件,它提供了原子操作的支持。定义了一个共享的原子整数shared_data,并使用atomic_cas函数来执行CAS操作。
在thread_function函数中,线程首先读取shared_data的当前值,然后尝试使用CAS操作将其增加1。如果CAS操作失败,线程将重试直到成功为止。这确保了多个线程在没有锁的情况下安全地修改共享数据。
最后,在主线程中等待两个工作线程执行完毕。这个示例演示了如何使用CAS操作来实现无锁的数据修改,确保多个线程可以原子性地更新共享资源。需要特别注意的是,实际应用中CAS操作通常用于实现更复杂的同步机制和数据结构。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>

// 共享资源
_Atomic int shared_data = 0;

// CAS操作函数
int atomic_cas(_Atomic int *target, int expected, int new_value) {
    return __atomic_compare_exchange_n(target, &expected, new_value, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST);
}

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        int current_value = atomic_load(&shared_data);
        int new_value = current_value + 1;

        // 使用CAS操作来修改共享数据
        while (!atomic_cas(&shared_data, current_value, new_value)) {
            current_value = atomic_load(&shared_data);
            new_value = current_value + 1;
        }

        printf("Thread %d: Shared Data = %d\n", thread_id, shared_data);

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    // 创建两个线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}
八、自定义锁:
除了上述常见的锁类型之外,用户空间还可以实现自定义锁,以满足特定需求。这些锁可以基于原子操作、文件锁、共享内存等构建。
(1)基于原子操作的例子
在Linux用户空间,可以基于原子操作来实现一个自定义的简单自旋锁。这个示例将使用C语言和原子操作库来创建一个自旋锁,确保多线程之间的互斥访问。
在这个示例中,使用了C标准库中的stdatomic.h头文件,它提供了原子操作的支持。定义了一个原子整数atomic_lock作为自定义锁。custom_lock函数使用原子操作atomic_exchange来不断尝试获取锁,直到成功为止。custom_unlock函数使用原子操作atomic_store来释放锁。
在thread_function函数中,线程使用custom_lock来获取锁,然后访问共享资源。之后,使用custom_unlock来释放锁。这确保了多个线程之间的互斥访问。
最后,在主线程中等待两个工作线程执行完毕。这个示例演示了如何使用原子操作来实现一个简单的自旋锁,确保多个线程可以互斥地访问共享资源。但是,这只是一个简单的自旋锁示例,实际中可能需要更复杂的自旋锁或使用其他线程同步机制,具体取决于应用需求。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>

// 定义原子锁
_Atomic int atomic_lock = 0;

// 自定义锁的加锁操作
void custom_lock() {
    while (atomic_exchange(&atomic_lock, 1)) {
        // 自旋等待锁的释放
        // 可以在这里添加一些睡眠时间以减少CPU负载
    }
}

// 自定义锁的解锁操作
void custom_unlock() {
    atomic_store(&atomic_lock, 0);
}

// 共享资源
int shared_resource = 0;

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    for (int i = 0; i < 5; ++i) {
        custom_lock();

        // 访问共享资源
        shared_resource++;
        printf("Thread %d: Shared Resource = %d\n", thread_id, shared_resource);

        custom_unlock();

        // 模拟一些工作
        sleep(1);
    }

    return NULL;
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    // 创建两个线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}
(2)基于文件锁的例子
在Linux用户空间,可以使用文件锁(File Lock)作为自定义锁的一种实现方式。文件锁是一种基于文件的锁定机制,多个进程可以通过操作文件来实现同步。
在这个示例中,使用了文件锁来实现自定义锁。定义了一个文件路径lock_file_path,并使用open函数打开或创建一个文件。每个线程都会尝试获取该文件的写锁(F_WRLCK),以确保在访问共享资源时只有一个线程可以持有锁。
custom_lock函数使用fcntl系统调用来获取文件锁,它会一直阻塞等待直到锁被获取为止。custom_unlock函数用于释放文件锁。
在thread_function函数中,每个线程在访问共享资源前调用custom_lock以获取锁,然后在访问完成后调用custom_unlock以释放锁。这确保了多个线程之间的互斥访问。
最后,在主线程中等待两个工作线程执行完毕。这个示例演示了如何使用文件锁来实现一个自定义锁,确保多个线程可以互斥地访问共享资源。在实际应用中,可能需要更复杂的同步机制,具体取决于应用需求。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

// 定义文件锁的文件路径
const char *lock_file_path = "/tmp/custom_lock_file";

// 自定义锁的加锁操作
int custom_lock(int fd) {
    struct flock fl;

    fl.l_type = F_WRLCK; // 写锁
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0; // 锁定整个文件
    return fcntl(fd, F_SETLKW, &fl);
}

// 自定义锁的解锁操作
int custom_unlock(int fd) {
    struct flock fl;

    fl.l_type = F_UNLCK; // 解锁
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0; // 解锁整个文件
    return fcntl(fd, F_SETLK, &fl);
}

// 共享资源
int shared_resource = 0;

// 线程函数
void *thread_function(void *arg) {
    int thread_id = *((int *)arg);

    int lock_fd;
    if ((lock_fd = open(lock_file_path, O_RDWR | O_CREAT, 0666)) == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < 5; ++i) {
        custom_lock(lock_fd);

        // 访问共享资源
        shared_resource++;
        printf("Thread %d: Shared Resource = %d\n", thread_id, shared_resource);

        custom_unlock(lock_fd);

        // 模拟一些工作
        sleep(1);
    }

    close(lock_fd);
    return NULL;
}

int main() {
    pthread_t threads[2];
    int thread_ids[2] = {1, 2};

    // 创建两个线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("pthread_join");
            exit(EXIT_FAILURE);
        }
    }

    return 0;
}


(3)基于共享内存的例子
在Linux用户空间,可以使用共享内存(Shared Memory)来实现自定义锁。
在这个示例中,使用共享内存来存储共享资源,然后使用信号量来实现自定义锁。首先,创建了一个共享内存段和一个信号量集。
在custom_lock函数中,使用semop系统调用来减小信号量的值以获取锁,如果信号量的值为0,进程将会被阻塞,直到获得锁。在custom_unlock函数中,增加信号量的值以释放锁。
在main函数中,使用自定义锁来保护共享资源的访问,确保多个进程之间的互斥访问。最后,断开共享内存连接并删除共享内存和信号量集以释放资源。
这个示例演示了如何在多个进程之间使用共享内存和信号量来实现自定义锁,确保多个进程可以互斥地访问共享资源。在实际应用中,可能需要更复杂的同步机制,具体取决于应用需求。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>

// 定义共享内存的键值和大小
#define SHM_KEY 1234
#define SHM_SIZE sizeof(int)

// 定义信号量的键值
#define SEM_KEY 5678

// 自定义锁的加锁操作
void custom_lock(int sem_id) {
    struct sembuf sops;

    sops.sem_num = 0; // 信号量集中的第一个信号量
    sops.sem_op = -1; // 减小信号量的值,即获取锁
    sops.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sops, 1) == -1) {
        perror("semop");
        exit(EXIT_FAILURE);
    }
}

// 自定义锁的解锁操作
void custom_unlock(int sem_id) {
    struct sembuf sops;

    sops.sem_num = 0;
    sops.sem_op = 1; // 增加信号量的值,即释放锁
    sops.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sops, 1) == -1) {
        perror("semop");
        exit(EXIT_FAILURE);
    }
}

// 共享资源
int *shared_resource;

int main() {
    int shm_id;
    int sem_id;

    // 创建共享内存段
    if ((shm_id = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    // 连接共享内存
    if ((shared_resource = shmat(shm_id, NULL, 0)) == (int *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    // 创建信号量集合
    if ((sem_id = semget(SEM_KEY, 1, IPC_CREAT | 0666)) == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    // 初始化信号量值为1,表示锁是未锁定状态
    semctl(sem_id, 0, SETVAL, 1);

    // 使用自定义锁
    for (int i = 0; i < 5; ++i) {
        custom_lock(sem_id);

        // 访问共享资源
        (*shared_resource)++;
        printf("Shared Resource = %d\n", *shared_resource);

        custom_unlock(sem_id);

        // 模拟一些工作
        sleep(1);
    }

    // 断开共享内存连接
    shmdt(shared_resource);

    // 删除共享内存和信号量集
    shmctl(shm_id, IPC_RMID, NULL);
    semctl(sem_id, 0, IPC_RMID);

    return 0;
}





每种锁类型都有其适用的场景和性能特征,选择合适的锁取决于具体的应用需求和并发访问模式。在使用锁时,必须小心处理死锁、优化性能以及确保正确的同步和互斥操作。
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/szqy00/article/details/132641803

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值