Linux-笔记 线程同步机制

目录

前言

实现

信号量(Semaphore)

计数型信号量

二值信号量 

信号量的原语操作

无名信号量的操作函数

例子

互斥锁(mutex)

互斥锁的操作函数

例子

条件变量 (Condition Variables)

条件变量的操作函数

例子

自旋锁 (Spinlock)

自旋锁与互斥锁的区别

自旋锁的操作函数

例子

读写锁 (Read-Write Lock)

读写锁的操作函数

例子

屏障(Barrier)

屏障的操作函数

例子


前言

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

实现

        实现线程同步的机制有很多,比较常用的有:信号量、互斥锁、自旋锁、条件变量、读写锁等等,这里仅探讨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 1000000

int 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;
}

条件变量 (Condition Variables)

        条件变量是用于线程同步的一种机制,通常与互斥锁(mutex)配合使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全 的问题。

        条件变量允许线程在某些条件不满足的情况下进行等待,并在条件满足时被唤醒。也就是说条件变量允许一个线程休眠(阻塞等待)直至获取到另 一个线程的通知(收到信号)再去执行自己的操作。

条件变量的操作函数

1、pthread_cond_init:初始化条件变量。

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • 参数
    • cond: 指向条件变量的指针。
    • attr: 条件变量属性。通常为NULL,使用默认属性。
  • 返回值:成功返回0,失败返回错误码。

2、pthread_cond_destroy:摧毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);
  • 参数
    • cond: 指向条件变量的指针。
  • 返回值:成功返回0,失败返回错误码。

对于初始化与销毁操作,有以下问题需要注意:

1)在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或 者函数 pthread_cond_init()都行;

2)对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;

3)对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;

4)对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;

5)经 pthread_cond_destroy()销毁的条件变量,可以再次调用 pthread_cond_init()对其进行重新初始化。

        条件变量的主要操作便是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态 的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足,等待操作是指在收到一个通知前一直处于阻塞状态。

3、pthread_cond_wait:等待条件变量。此函数必须在持有互斥锁的情况下调用。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • 参数
    • cond: 指向条件变量的指针。
    • mutex: 指向互斥锁的指针。函数内部会在等待时释放互斥锁,并在被唤醒后重新加锁。
  • 返回值:成功返回0,失败返回错误码。

4、pthread_cond_signal:唤醒等待条件变量的一个线程。

int pthread_cond_signal(pthread_cond_t *cond);
  • 参数
    • cond: 指向条件变量的指针。
  • 返回值:成功返回0,失败返回错误码。

5、pthread_cond_broadcast:唤醒等待条件变量的所有线程。

int pthread_cond_broadcast(pthread_cond_t *cond);
  • 参数
    • cond: 指向条件变量的指针。
  • 返回值:成功返回0,失败返回错误码。

6、pthread_cond_timedwait:等待条件变量,带有超时功能。

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
  • 参数
    • cond: 指向条件变量的指针。
    • mutex: 指向互斥锁的指针。函数内部会在等待时释放互斥锁,并在被唤醒或超时后重新加锁。
    • abstime: 绝对时间,用于指定等待超时时间。
  • 返回值:成功返回0,超时返回ETIMEDOUT,失败返回其他错误码。
例子
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_mutex_t mutex;
static pthread_cond_t cond;
static int g_avail = 0;

static void *consumer_thread(void *arg)
{
    for (;;) {
        pthread_mutex_lock(&mutex);
        while (0 >= g_avail)
            pthread_cond_wait(&cond, &mutex);

        while (0 < g_avail)
            g_avail--;

        pthread_mutex_unlock(&mutex);
    }

    return (void*)0;
}

int main(int argc, char *argv[]) {
    pthread_t tid;
    int ret;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    for (;;) {
        pthread_mutex_lock(&mutex);  //上锁
        g_avail++;                   //生产
        pthread_mutex_unlock(&mutex);//解锁
        pthread_cond_signal(&cond);  //向条件变量发送信号
    }

    exit(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;
}

读写锁 (Read-Write Lock)

        读写锁是一种同步机制,允许多个线程同时读取共享数据,但在写数据时只允许一个线程访问。这种机制可以提高并发性,尤其是在读多写少的场景下。读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住 时,就可以说成是互斥模式锁住。读写锁有三种状态,分别是读模式下的上锁状态、写模式下的上锁状态、不上锁状态。

        读写锁两个规则:

        1)当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读 模式加锁还是以写模式加锁)的线程都会被阻塞。

        2)当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以 写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

读写锁的操作函数

1、pthread_rwlock_init:初始化读写锁。

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
  • 参数
    • rwlock: 指向读写锁的指针。
    • attr: 读写锁属性。通常为NULL,使用默认属性。
  • 返回值:成功返回0,失败返回错误码。

2、pthread_rwlock_destroy:销毁读写锁。

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 参数
    • rwlock: 指向读写锁的指针。
  • 返回值:成功返回0,失败返回错误码。

3、pthread_rwlock_rdlock:获取读锁。如果有其他线程持有写锁,则调用线程将阻塞。

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 参数
    • rwlock: 指向读写锁的指针。
  • 返回值:成功返回0,失败返回错误码。

4、pthread_rwlock_wrlock:获取写锁。如果有其他线程持有读锁或写锁,则调用线程将阻塞。

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • 参数
    • rwlock: 指向读写锁的指针。
  • 返回值:成功返回0,失败返回错误码。

5、pthread_rwlock_unlock:释放读锁或写锁。

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 参数
    • rwlock: 指向读写锁的指针。
  • 返回值:成功返回0,失败返回错误码。

6、pthread_rwlock_tryrdlock:尝试获取读锁。如果读写锁不可用,函数不会阻塞并立即返回错误码。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
  • 参数
    • rwlock: 指向读写锁的指针。
  • 返回值:成功返回0,失败返回错误码 EBUSY。

7、pthread_rwlock_trywrlock:尝试获取写锁。如果读写锁不可用,函数不会阻塞并立即返回错误码。

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
  • 参数
    • rwlock: 指向读写锁的指针。
  • 返回值:成功返回0,失败返回错误码 EBUSY。
例子
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_rwlock_t rwlock;
static int g_count = 0;

static void *read_thread(void *arg)
{
    int number = *((int*)arg);

    for (int i=0;i<10;i++) {
        pthread_rwlock_rdlock(&rwlock);
        printf("读线程%d, g_count = %d\n", number+1, g_count);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }    

    return (void*)0;
}

static void *write_thread(void *arg)
{
    int number = *((int*)arg);

    for (int i=0;i<10;i++) {
        pthread_rwlock_wrlock(&rwlock);
        printf("写线程%d, g_count = %d\n", number+1, g_count+=20);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }    

    return (void*)0;
}

static int nums[5] = {0, 1, 2, 3, 4};
int main(int argc, char **argv) 
{
    pthread_t tid[10];
    int j;

    /* 对读写锁进行初始化 */
    pthread_rwlock_init(&rwlock, NULL);
    
    /* 创建 5 个读 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
    pthread_create(&tid[j], NULL, read_thread, &nums[j]);
    
    /* 创建 5 个写 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
    pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
    
    /* 等待线程结束 */
    for (j = 0; j < 10; j++)
    pthread_join(tid[j], NULL);//回收线程
    
    /* 销毁自旋锁 */
    pthread_rwlock_destroy(&rwlock);
    
    exit(0);
}

  

屏障(Barrier)

        屏障是一种同步机制,用于协调多个线程(或进程)在程序执行的某个点上相互等待,直到所有参与的线程都到达这个同步点后才继续执行。可以把它想象成一个关卡,所有线程必须在关卡处等待,直到所有线程都到达关卡,然后它们才能继续执行后续的代码。

        其工作原理为:

        1)定义一个屏障点:在程序中指定一个点,所有线程在此点前都会阻塞等待其他线程到达。

        2)线程等待:每个线程在到达屏障点时调用等待函数,阻塞自己。

        3)同步:当所有指定的线程都到达屏障点时,屏障被“解除”,所有被阻塞的线程同时继续执行。

屏障的操作函数

1、pthread_barrier_init:初始化屏障

int pthread_barrier_init(pthread_barrier_t *restrict barrier,
                         const pthread_barrierattr_t *restrict attr,
                         unsigned count);
  • barrier:指向屏障对象的指针。
  • attr:屏障属性,通常为NULL。
  • count:需要等待的线程数量。

返回值:成功返回0,失败返回错误码。

2、pthread_barrier_wait:等待屏障函数,也就是设置的等待点。

int pthread_barrier_wait(pthread_barrier_t *barrier);
  • barrier:指向屏障对象的指针。

返回值:成功返回0或PTHREAD_BARRIER_SERIAL_THREAD,失败返回错误码。

3、pthread_barrier_destroy:销毁屏障

int pthread_barrier_destroy(pthread_barrier_t *barrier);
  • barrier:指向屏障对象的指针。

返回值:成功返回0,失败返回错误码。

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

// 定义屏障对象
pthread_barrier_t barrier;

// 线程函数
void* thread_function(void* arg) {
    int thread_num = *((int*)arg);
    printf("Thread %d reached the barrier\n", thread_num);

    // 线程在这里等待,直到所有线程都到达屏障
    int res = pthread_barrier_wait(&barrier);
    if (res == PTHREAD_BARRIER_SERIAL_THREAD) {
        printf("Thread %d is the serial thread\n", thread_num);
    } else if (res != 0) {
        perror("pthread_barrier_wait");
        exit(EXIT_FAILURE);
    }

    printf("Thread %d passed the barrier\n", thread_num);
    return NULL;
}

int main() {
    const int NUM_THREADS = 5;
    pthread_t threads[NUM_THREADS];
    int thread_nums[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_nums[i] = i + 1;
        if (pthread_create(&threads[i], NULL, thread_function, &thread_nums[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);
        }
    }

    // 销毁屏障
    if (pthread_barrier_destroy(&barrier) != 0) {
        perror("pthread_barrier_destroy");
        exit(EXIT_FAILURE);
    }

    return 0;
}

:代码中在线程中有一个判断如下,这是为了判断哪个线程是“串行线程”,所谓 “串行线程”就是在使用屏障机制时,所有到达屏障的线程在被解除阻塞后,会同时继续执行。但是,有一个线程会被选为“串行线程”(serial thread)。这个线程可以用于执行一些需要单独处理的操作,例如某些需要在所有线程继续之前完成的一次性初始化操作。

    if (res == PTHREAD_BARRIER_SERIAL_THREAD) {
        printf("Thread %d is the serial thread\n", thread_num);
    } else if (res != 0) {
        perror("pthread_barrier_wait");
        exit(EXIT_FAILURE);
    }

如运行的结果,可看出线程 5 为 “串行线程”,它是所有线程到底屏障点后第一个运行的线程。

                                

总结

信号量

优点:能够灵活的控制对共享资源的访问,二值信号量还能当简单的互斥锁来使用。

缺点:需要处理好初始值与等待操作,使用不当有可能造成死锁或饥饿。

使用场景:实现复杂的同步与资源管理,对有限资源的访问。

互斥锁

优点:实现简单,适用于大部分场景。几乎所有编程语言与库都提供支持。

缺点:获取释放锁有性能开销,多个线程相互等待对方持有的锁有可能导致死锁。

使用场景:保护共享资源,适用于需要简单实现保护的临界区。

条件变量

优点:运行线程等待某个条件的变化,能够解决复杂的同步问题。

缺点:需要仔细设计等待和通知的逻辑,高负荷情况下性能不如其他同步机制。

使用场景:需要等待某些条件成立的场景(生产者-消费者),复杂的线程协调和状态同步场景。

自旋锁

优点:低开销,避免线程上下文切换的开销,持锁时间短效率高。

缺点:持续占有CPU浪费资源。

使用场景:需要短时间锁定的临界区,如硬件中断处理程序中,多核处理器上短时间的资源竞争。

读写锁

优点:允许多个线程同时读取,提高了并发性能,可以分别控制读锁和写锁。

缺点:实现逻辑复杂,持续不断的读操作可能导致写操作饥饿。

使用场景:读操作多于写操作的场景,如缓存读取。

屏障

优点:同步多个线程,让一组线程在某个点上等待彼此

缺点:仅适用于同步夺冠线程的特定点。等待所有线程到达屏障点有性能开销。

使用场景:需要多个线程在某一个同步点进行协作,如并行计算中的同步点。

实际中使用哪种线程同步机制还需要根据具体的应用场景、性能需求等因素去考虑。

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值