linux多线程同步的四种方式

1. 在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清楚是哪一个先执行。如果运行的结果依赖于多线程执行的顺序,那么就会形成竞争条件,每次运行的结果可能会不同,所以应该尽量避免竞争条件的形成。

2. 最常见的解决竞争条件的方法是将原先分离的两个指令构成一个不可分割的原子操作,其他任务就不能插入到原子操作中!

3. 对多线程来说,同步指的是在一定时间内只允许某一个线程访问某个资源,而在此时间内,不允许其他线程访问该资源!

4. 线程同步的常见方法:互斥锁,条件变量,读写锁,信号量

一、互斥锁(互斥量)

互斥锁是一种特殊的变量,有上锁(lock)和解锁(unlock两种状态。

当处于解锁状态时,线程想获取该互斥锁,就可以获取不被阻塞,互斥锁变为锁定状态;

当处于锁定状态时,线程获取互斥锁被阻塞,并加入到这个互斥锁的等待队列中。

互斥锁有点像打印机,空闲时你可以打印;别人在打印时,你就需要排队等待打印机空闲。

1.1 创建并初始化一个互斥锁

使用 pthread_mutex_t 类型的变量来表示互斥锁。在使用之前,必须对其进行初始化。

静态初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态初始化:pthread_mutex_init 函数

一般我们都使用静态初始化。理由:静态初始化通常比 pthread_mutex_init 更有效,而且可以在定义为全局变量时即完成初始化,这样可以保证在任何线程开始执行之前,初始化既已完成。

1.2 互斥锁相关属性及分类

//初始化互斥锁属性
pthread_mutexattr_init(pthread_mutexattr_t attr);

//销毁互斥锁属性
pthread_mutexattr_destroy(pthread_mutexattr_t attr);

//用于获取互斥锁属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr , int *restrict pshared);

//用于设置互斥锁属性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr , int pshared);

attr表示互斥锁的属性,pshared表示互斥锁的共享属性,有两种取值:

1)PTHREAD_PROCESS_PRIVATE:锁只能用于一个进程内部的两个线程进行互斥(默认情况)

2)PTHREAD_PROCESS_SHARED:锁可用于两个不同进程中的线程进行互斥,使用时还需要在进程共享内存中分配互斥锁,然后该互斥锁指定属性就可以了。

1.3 互斥锁常用函数

// 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 对互斥锁的锁定
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);

// 对互斥锁的解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_lock函数会调用这个函数线程一直阻塞到互斥锁可用为止,而pthread_mutex_trylock会立即返回

二、条件变量

用处:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就唤醒线程。由于涉及到共享数据,因此条件变量是结合互斥锁来使用的。使用 pthread_cond_t 来表示条件变量。

2.1 相关函数

1)创建

静态初始化:pthread_cond_t convar = PTHREAD_COND_INITIALIZER

动态初始化:int pthread_cond_init(&condvar, NULL)

2) 销毁

int pthread_cond_destroy(&condvar)

3)等待

条件等待:int pthread_cond_wait(&condvar, &mutex)

计时等待:int pthread_cond_timewait(&condvar, &mutex, time)

1. 计时等待如果在给定时刻前条件没有被满足,则返回 ETIMEOUT,结束等待

2. 无论哪种等待方式,都必须有一个互斥锁配合,以方式多个线程同时请求pthread_cond_wait形成竞争条件。也就是说,在使用 pthread_cond_wait 之前,必须使用互斥锁加锁(pthread_mutex_lock);pthread_cond_timewait 同理。

4)唤醒

唤醒一个等待线程:pthread_cond_signal(&condvar)

唤醒所有等待线程:pthread_cond_broadcast(&cond)

重要的是,pthread_cond_signal 不会存在惊群效应,也就是它只唤醒一个等待线程,不会给所有线程发信号唤醒他们,然后要求他们自己去争抢资源!pthread_cond_signal 会根据等待线程的优先级和等待时间来确定唤醒哪一个等待线程!

2.2 条件变量应用实例------condition_variables.c

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

int i = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;

void *threadfunc(void *pvoid)
{
    while (1)
    {
        pthread_mutex_lock(&mutex);
        if (i < 200)
        {
            i++;
            pthread_cond_signal(&condvar);    /**< 子线程唤醒主线程 */
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }

    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, &threadfunc, NULL);

    pthread_mutex_lock(&mutex);

    while (i < 100)
    {
        pthread_cond_wait(&condvar, &mutex);
    }

    printf("i = %d\n", i);
    pthread_mutex_unlock(&mutex);
    pthread_join(tid, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&condvar);

    return 0;
}

该实例中,主线程监视全局变量 i 的值,如果 i 小于100,则等待。子线程递增 i 的值,直到 i 等于200。主线程直 i 大于或等于100之后,才能继续执行。 运行结果可能是 i = 100,也可能是i = xxx,其中xxx是一个100到200之间的数。子线程每次唤醒条件变量并释放互斥锁之后,将于主线程一同竞争互斥锁。当 i 大于100时,如果主线程获得互斥锁,就会显示 i 的值。也就是说,等待条件变量的线程在被唤醒时,并不自动获得互斥锁。

编译代码命令:

gcc -o condition_variables condition_variables.c -pthread

三、读写锁

  读写锁与互斥锁类型,也叫共享互斥锁。互斥锁有上锁(lock)和解锁(unlock)两种状态,而且一次只有一个线程可以对其加锁。读写锁可以有三种状态:读模式加锁,写模式加锁,不加锁。

  一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁(允许多个线程读但只允许一个线程写)

读写锁的特点:

  如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作

  如果有其他线程写数据,则其他线程都不允许读、写操作

读写锁的规则:

  如果某线程申请了读锁,其他线程可以再申请读锁,但不能申请写锁

       如果与其他线程申请了写锁,则其他线程不能申请读锁,也不能申请写锁

读写锁适合于对数据结构的读次数比写次数多得多的情况

#include <pthread.h>
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); 

// 申请读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); 

// 申请写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); 

// 尝试以非阻塞的方式来在读写锁上获取写锁,
// 如果有任何的读者或写者持有该锁,则立即失败返回。
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 

// 解锁
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); 

// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

实例:

// 一个使用读写锁来实现 4 个线程读写一段数据是实例。
// 在此示例程序中,共创建了 4 个线程,
// 其中两个线程用来写入数据,两个线程用来读取数据
#include <stdio.h>  
#include <unistd.h>  
#include <pthread.h>  

pthread_rwlock_t rwlock; //读写锁  
int num = 1;  
  
//读操作,其他线程允许读操作,却不允许写操作  
void *fun1(void *arg)  
{  
    while(1)  
    {  
        pthread_rwlock_rdlock(&rwlock);
        printf("first read num == %d\n", num);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
  
//读操作,其他线程允许读操作,却不允许写操作  
void *fun2(void *arg)
{
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("second read num == %d\n", num);
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
}
 
//写操作,其它线程都不允许读或写操作  
void *fun3(void *arg)
{
    while(1)
    {
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("write thread first\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
}
 
//写操作,其它线程都不允许读或写操作  
void *fun4(void *arg)
{
    while(1)
    {  
        pthread_rwlock_wrlock(&rwlock);  
        num++;  
        printf("write thread second\n");  
        pthread_rwlock_unlock(&rwlock);  
        sleep(1);  
    }  
}  
  
int main()  
{  
    pthread_t ptd1, ptd2, ptd3, ptd4;  
      
    pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁  
      
    //创建线程  
    pthread_create(&ptd1, NULL, fun1, NULL);  
    pthread_create(&ptd2, NULL, fun2, NULL);  
    pthread_create(&ptd3, NULL, fun3, NULL);  
    pthread_create(&ptd4, NULL, fun4, NULL);  
      
    //等待线程结束,回收其资源  
    pthread_join(ptd1, NULL);  
    pthread_join(ptd2, NULL);  
    pthread_join(ptd3, NULL);  
    pthread_join(ptd4, NULL);  
      
    pthread_rwlock_destroy(&rwlock);//销毁读写锁  
      
    return 0;  
}  

四、信号量

  信号量广泛用于进程或线程间的同步或互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

  根据信号量的值来判断是否对公共资源具有访问权限,当信号量的值大于0时,可以访问,否则将阻塞带有两个原子操作 P 和 V,一次 P 操作使信号量减1,一次 V 操作使信号量加 1。

#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);

// 信号量 P 操作(减 1)
int sem_wait(sem_t *sem);

// 以非阻塞的方式来对信号量进行减 1 操作
int sem_trywait(sem_t *sem);

// 信号量 V 操作(加 1)
int sem_post(sem_t *sem);

// 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);

// 销毁信号量
int sem_destroy(sem_t *sem);

// 信号量用于同步实例
#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);
        ch++;
        sleep(1);
        sem_post(&sem_p);
    }
}
 
void *pthread_p(void *arg)  //此线程打印ch的值
{
    while(1)
    {
        sem_wait(&sem_p);
        printf("%c",ch);
        fflush(stdout);     // 刷新标准输出缓冲区
        sem_post(&sem_g);
    }
}
 
int main(int argc, char *argv[])
{
    pthread_t tid1,tid2;
    sem_init(&sem_g, 0, 0); // 初始化信号量为0
    sem_init(&sem_p, 0, 1); // 初始化信号量为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;
}

结果会依次打印26个字母,代码的执行顺序是,打印线程------>自增线程

参考:https://blog.csdn.net/daaikuaichuan/article/details/82950711

https://www.cnblogs.com/yinbiao/p/11190336.html

猜你喜欢

转载自www.cnblogs.com/gezhuangzhuang/p/12690419.html