Linux下线程同步机制(吐血整理)


资源竞争

当进程中的多个线程,同时读取一块内存数据,与此同时其中一个或多个线程修改了这块内存数据。这样就会导致不可预期的结果

因为线程不安全引起的错误往往非常难发现,因为这种现象是不能稳定复现的。

下面举个例子:

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

int g = 0;
//100000000-200000000
void *pf(void *arg){
    
    
    int i;
    for(i=0;i<100000000;i++){
    
    
        g = g+1;//读内存 加法指令 写内存    
    }
}

int main(){
    
    
    pthread_t ids[2];

    int ret1 = pthread_create(&ids[0],NULL,pf,NULL);
    int ret2 = pthread_create(&ids[1],NULL,pf,NULL);
    if(ret1 != 0 || ret2 != 0){
    
    
        printf("pthread_create:%s\n",strerror(ret1!=0?ret1:ret2));
        return -1;
    }
    int i;
    for(i=0;i<2;i++){
    
    
        pthread_join(ids[i],NULL);
    }   
    printf("%d\n",g);

    return 0;    
}

输出结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


线程同步

当多个线程同时访问其所共享的进程资源时,需要相互协调,以防止出现数据不一致、不完整的问题。这就叫线程同步。

1. 互斥锁

特点:对于读者和写者来说。只要有一方获取了锁,另一方则不能继续获取,进而执行 临界区代码。
1.互斥锁类型
1.普通锁 (PTHREAD_MUTEX_NORMAL) 互斥锁默认类型。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个 等待队列,并在该锁解锁后按照优先级获得它,这种锁类型保证了资源分配的公平性。一个 线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普 通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。

2.检错锁(PTHREAD_MUTEX_ERRORCHECK) 一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK;对一个已 经被其他线程加锁的检错锁解锁或者对一个已经解锁的检错锁再次解锁,则解锁操作返回 EPERM;

3.嵌套锁(PTHREAD_MUTEX_RECURSIVE) 该锁允许一个线程在释放锁之前多次对它加锁而不发生死锁;其他线程要获得这个锁, 则当前锁的拥有者必须执行多次解锁操作;对一个已经被其他线程加锁的嵌套锁解锁,或者 对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM

4.默认锁(PTHREAD_MUTEX_ DEFAULT) 一个线程如果对一个已经加锁的默认锁再次加锁,或者虽一个已经被其他线程加锁的默 认锁解锁,或者对一个解锁的默认锁解锁,将导致不可预期的后果;这种锁实现的时候可能 被映射成上述三种锁之一;

//有两种方法创建互斥锁,静态方式和动态方式。
//POSIX定义了一个宏 PTHREAD_MUTEX_INITIALIZER 来静态初始化互斥锁,方法如下: 
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
//在LinuxThreads实现中,pthread_mutex_t是一个结构,而 PTHREAD_MUTEX_INITIALIZER则是一个结构常量。 
//动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下:
int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);
//其中mutexattr用于指定互斥锁属性(见上),如果为NULL则使用缺省属性,PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。

int pthread_mutex_lock (pthread_mutex_t* mutex);

int pthread_mutex_unlock (pthread_mutex_t* mutex);

int pthread_mutex_destroy (pthread_mutex_t* mutex);

1)互斥量被初始化为非锁定状态;
2)线程1调用pthread_mutex_lock函数,立即返回,互斥量呈锁定状态;
3)线程2调用pthread_mutex_lock函数,阻塞等待;
4)线程1调用pthread_mutex_unlock函数,互斥量呈非锁定状态;
5)线程2被唤醒,从pthread_mutex_lock函数中返回,互斥量呈锁定状态;

范例:

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

int g = 0;
pthread_mutex_t lock;
//100000000-200000000
void *pf(void *arg){
    
    
    int i;
    for(i=0;i<100000000;i++){
    
    
        pthread_mutex_lock(&lock);
        g = g+1;//读内存 加法指令 写内存   
        pthread_mutex_unlock(&lock);
    }
}

int main(){
    
    
    pthread_t ids[2];
    int ret = pthread_mutex_init(&lock,NULL);
    if(ret != 0){
    
    
        printf("pthread_mutex_init:%s\n",strerror(ret));   
    }
    int ret1 = pthread_create(&ids[0],NULL,pf,NULL);
    int ret2 = pthread_create(&ids[1],NULL,pf,NULL);
    if(ret1 != 0 || ret2 != 0){
    
    
        printf("pthread_create:%s\n",strerror(ret1!=0?ret1:ret2));
        return -1;
    }
    int i;
    for(i=0;i<2;i++){
    
    
        pthread_join(ids[i],NULL);
    }   
    printf("%d\n",g);
    pthread_mutex_destroy(&lock);
    return 0;    
}

2. 读写锁

概念:
        读写锁实际是一种特殊的自旋锁,这组锁它把对共享资源的访问者划分成读者和写 者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋 锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源, 最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写 者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

        因为读写锁保持期间也是抢占失效的。如果读写锁当前没有读者,也没有写者,那么 写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁 没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读 写锁。

特性:
        一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁

         ① 当读写锁是写加锁(独占)状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程 都会被阻塞

        ②当读写锁在读加锁(共享)状态时, 所有试图以读模式对它进行加锁的线程都可以得到访 问权。但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁,并且读 写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁 请求长期阻塞.

初始化和销毁:
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//成功则返回0,出错则返回错误编号. 同互斥量以上,在释放读写锁占用的内存之前,需要先通过 pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源. 

//读和写: 
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 
//成功则返回0,出错则返回错误编号.这3个函数分别实现获取读锁,获取写锁和释放锁的操作. 

//获 取锁的两个函数是阻塞操作,同样,非阻塞的函数为: 
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//成功则返回0,出错则返回错误编号.非阻塞的获取锁操作,如果可以获取则返回0,否则返回 错 误的EBUSY

3. 自旋锁

        特点:轮询忙等待。
        自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部 分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存 器中关闭/打开中断标志位,不需要自旋锁)
        在单核cpu下不起作用:被自旋锁保护的临界区代码执行时不能进行挂起状态。会造成死 锁
        自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的 线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有 时间过长。如果需要长时间锁定的话, 最好使用信号量。

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
// pshared可取以下属性:
// PTHREAD_PROCESS_PRIVATE 
// PTHREAD_PROCESS_SHARED 

int pthread_spin_destroy(pthread_spinlock_t *lock); 
int pthread_spin_lock(pthread_spinlock_t *lock);

int pthread_spin_trylock(pthread_spinlock_t *lock); 
int pthread_spin_unlock(pthread_spinlock_t *lock);

4. 信号量

信号量是一个计数器,用于控制访问有限共享资源的线程数。

#include <semaphore.h>

// 创建信号量
int sem_init (sem_t* sem, int pshared,unsigned int value);

sem     - 信号量ID,输出。

pshared - 一般取0,表示调用进程的信号量。
          非0表示该信号量可以共享内存的方式,
          为多个进程所共享(Linux暂不支持)。

value   - 信号量初值。

// 信号量减1,不够减即阻塞
int sem_wait (sem_t* sem);

// 信号量减1,不够减即返回-1,errno为EAGAIN
int sem_trywait (sem_t* sem);

// 信号量减1,不够减即阻塞,
// 直到abs_timeout超时返回-1,errno为ETIMEDOUT
int sem_timedwait (sem_t* sem,
    const struct timespec* abs_timeout);

struct timespec {
    
    
    time_t tv_sec;  // Seconds
    long   tv_nsec; // Nanoseconds [0 - 999999999]
};

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

// 销毁信号量
int sem_destroy (sem_t* sem);
    
int sval;
sem_getvalue (&g_sem, &sval); //获取当前信号量,并存入sval中

范例:

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


int g = 0;
sem_t sem;
//100000000-200000000
void *pf(void *arg){
    
    
    int i;
    for(i=0;i<100000000;i++){
    
    
        sem_wait(&sem);//信号量-1 
        g = g+1;//读内存 加法指令 写内存   
        sem_post(&sem);//信号量+1
    }
}

int main(){
    
    
    pthread_t ids[2];
    int ret = sem_init(&sem,0,1);
    if(ret != 0){
    
    
        perror("sem_init");
    }
    int ret1 = pthread_create(&ids[0],NULL,pf,NULL);
    int ret2 = pthread_create(&ids[1],NULL,pf,NULL);
    if(ret1 != 0 || ret2 != 0){
    
    
        printf("pthread_create:%s\n",strerror(ret1!=0?ret1:ret2));
        return -1;
    }
    int i;
    for(i=0;i<2;i++){
    
    
        pthread_join(ids[i],NULL);
    }   
    printf("%d\n",g);
    sem_destroy(&sem);
    return 0;    
}

1)信号量APIs没有声明在pthread.h中,而是声明在semaphore.h中,失败也不返回错误码,而是返回-1,同时设置errno。

2)互斥量任何时候都只允许一个线程访问共享资源,而信号量则允许最多value个线程同时访问共享资源,当value为1时,与互斥量等价。

范例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define MAX_CONNS 5  // 最大连接数
#define MAX_USERS 50 // 最大用户数

sem_t g_sem;

void* thread_user (void* arg) 
{
    
    
    pthread_t tid = pthread_self ();

    int sval;
    sem_getvalue (&g_sem, &sval);
    printf ("%lu线程:等待数据库连接(还剩%d个空闲连接)...\n", tid, sval);

    sem_wait (&g_sem);
    sem_getvalue (&g_sem, &sval);
    printf ("%lu线程:获得数据库连接(还剩%d个空闲连接)!\n", tid, sval);

    usleep (1000000);

    sem_post (&g_sem);
    sem_getvalue (&g_sem, &sval);
    printf ("%lu线程:释放数据库连接(还剩%d个空闲连接)。\n", tid, sval);

    return NULL;
}

int main (void) 
{
    
    
    size_t i;
    pthread_t tids[MAX_USERS];
    int error;

    sem_init (&g_sem, 0, MAX_CONNS);

    for (i = 0; i < sizeof (tids) / sizeof (tids[0]); i++)
    {
    
    
        if ((error = pthread_create (&tids[i], NULL, thread_user,NULL)) != 0) 
        {
    
    
            fprintf (stderr, "pthread_create: %s\n", strerror (error));
            return -1;
        }
    }

    for (i = 0; i < sizeof (tids) / sizeof (tids[0]); i++)
    {
    
    
        if ((error = pthread_join (tids[i], NULL)) != 0) 
        {
    
    
            fprintf (stderr, "pthread_join: %s\n", strerror (error));
            return -1;
        }
    }

    sem_destroy (&g_sem);

    return 0;
}

5. 条件变量

生产者消费者模型
生产者:产生数据的线程。
消费者:使用数据的线程。
在这里插入图片描述
通过缓冲区隔离生产者和消费者,与二者直连相比,避免相互等待,提高运行效率。
生产快于消费,缓冲区满,撑死。
消费快于生产,缓冲区空,饿死。

条件变量可以让调用线程在满足特定条件的情况下暂停。

int pthread_cond_init (pthread_cond_t* cond,const pthread_condattr_t* attr);
//亦可pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 使调用线程睡入条件变量cond,同时释放互斥锁mutex
int pthread_cond_wait (pthread_cond_t* cond,pthread_mutex_t* mutex);

int pthread_cond_timedwait (pthread_cond_t* cond,
    pthread_mutex_t* mutex,
    const struct timespec* abstime);

struct timespec {
    
    
    time_t tv_sec;  // Seconds
    long   tv_nsec; // Nanoseconds [0 - 999999999]
};

// 从条件变量cond中唤出一个线程,
// 令其重新获得原先的互斥锁
int pthread_cond_signal (pthread_cond_t* cond);

注意:被唤出的线程此刻将从pthread_cond_wait函数中返回,
但如果该线程无法获得原先的锁,则会继续阻塞在加锁上。

// 从条件变量cond中唤出所有线程
int pthread_cond_broadcast (pthread_cond_t* cond);

int pthread_cond_destroy (pthread_cond_t* cond);

范例:

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t notempty = PTHREAD_COND_INITIALIZER;//不为空的条件  
pthread_cond_t notfull = PTHREAD_COND_INITIALIZER;//不为满的条件

#define CAPCITY 20
char stock[CAPCITY];
int size = 0;

void show(const char *who,const char *con,char s){
    
    
    int i;
    for(i=0;i<size;i++){
    
    
        printf("%c",stock[i]);    
    }
    printf("  %s %c(%s)\n",con,s,who);
}

//生产者线程
void *producer(void *arg){
    
    
    const char *who = (char*)arg;    
    for(;;){
    
    
        pthread_mutex_lock(&mutex);
        //被唤醒之后 需要重新判断仓库是否满了
        while(size >= CAPCITY){
    
    //仓库满了,不能进行生产
            printf("仓库满了,%s线程不能进行生产!\n",who);
            //释放锁 要等待 仓库不满的信号 
            pthread_cond_wait(&notfull,&mutex);//阻塞释放锁
            //被唤醒之后 重新获得锁
            printf("有人进行了消费,%s线程可以进行生产了!\n",who);
        }
        char prod = 'A'+rand()%26;
        show(who,"<--",prod);
        stock[size++] = prod;
        pthread_cond_signal(&notempty);
        pthread_mutex_unlock(&mutex);
        usleep ((rand () % 100) * 1000);
    }
    return NULL;
}
//消费者线程
void *customer(void *arg){
    
    
    const char *who = (const char *)arg;
    for(;;){
    
    
        pthread_mutex_lock(&mutex);
        while(size == 0){
    
    
            printf("仓库空了,%s消费者等待!\n",who);
            pthread_cond_wait(&notempty,&mutex);
            printf("仓库有货物了,%s可以进行消费了!\n",who);
        }
        
        char prod = stock[--size];
        show(who,"-->",prod);

        pthread_cond_signal(&notfull);
        pthread_mutex_unlock(&mutex);
        usleep ((rand () % 100) * 1000);
    }
    return NULL;
}


int main(){
    
    
    pthread_t pid[2],cid[2];
    
    pthread_create(&pid[0],NULL,producer,"生产者1号");
    pthread_create(&pid[1],NULL,producer,"生产者2号");
    

    pthread_create(&cid[0],NULL,customer,"消费者1号");
    pthread_create(&cid[1],NULL,customer,"消费者2号");


    getchar();
    return 0;    
}

6. 屏障

        屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到 所有的合作线程都到达某一点,然后从该点继续执行。pthread_join函数就是一种屏障,允 许一个线程等待,直到另一个线程退出。
        但屏障对象的概念更广,允许任意数量的线程等待,直到所有的线程完成处理工作,而 线程不需要退出。所有的线程达到屏障后可以接着工作。

int pthread_barrier_init(pthread_barrier_t *barrier,const pthread_barrrierattr_t *attr,unsigned int count) 

int pthread_barrier_destroy(pthread_barrier_t *barrier);

int pthread_barrier_wait(pthread_barrier_t *barrier)

范例:

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

pthread_barrier_t barr;
void pf(void *arg){
    
    
    int x = (int)(arg);
    x = (x+2)*3;//6 9 12
    printf("%lu run....\n",pthread_self());
    sleep(x);
    printf("%lu weak!\n",pthread_self());
    pthread_barrier_wait(&barr);
    printf("%lu go on...\n",pthread_self());
}

int main(){
    
    
    pthread_t ids[3];
    pthread_barrier_init(&barr,NULL,3);
    int i;
    for(i=0;i<3;i++){
    
    
        pthread_create(&ids[i],NULL,pf,(void*)i);    
    }
    
    getchar();
    pthread_barrier_destroy(&barr);
    return 0;    
}

猜你喜欢

转载自blog.csdn.net/LUCAS004/article/details/108553561