线程间同步

多个线程同事访问共享数据时可能会冲突,这跟前面介绍信号时所说的可重入性是同样的问题。比如这两个线程都要把某个全局变量加1,这个操作在某平台需要三条指令完成:

1.从内存读变量值到寄存器

2.寄存器的值加1

3.将寄存器的值写回内存

对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,MutualExclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其他线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attr为NULL则表示缺省属性。

用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。

如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。

Mutex的加锁和解锁操作可以用下列函数:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthead_mutex_unlock(pthread_mutex_t *mutex);

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

一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待(阻塞),直到另一个线程调用pthread_mytex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。

如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock函数,如果Mytex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

代码演示:

#include "./common/head.h"

/*功能:
 *两个线程同时操作全局变量cnt,每次进行“读-修改-写回”操作,每个线程重复5000次。
 *如果不加锁,cnt最后的值不确定(小于10000),加锁后,cnt最后的值为10000。
*/

int cnt = 0;     //全局变量cnt
pthread_mutex_t add_lock = PTHREAD_MUTEX_INITIALIZER;    //初始化互斥锁

//线程执行函数
void *thr_fn(void *arg)
{
    int val;
    for(int i = 0; i < 5000; i++)
    {
        pthread_mutex_lock(&add_lock);
        val = cnt;
        cnt = val + 1;
        printf("cnt = %d\n", cnt);
        pthread_mutex_unlock(&add_lock);    //如果不释放,将会造成死锁。比如线程1已经获得了锁,下次循环又来获得锁,但是锁还在线程1上,这时线程1认为没有获得到锁而挂起,将永远不会被唤醒。
    }
    return NULL;
}

int main()
{
    pthread_t tid1, tid2;

    //两个线程都运行起来
    pthread_create(&tid, NULL, thr_fn, NULL);
    pthread_create(&tid, NULL, thr_fn, NULL);

    //等待给两个线程收尸
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

 “挂起等待”和“唤醒等待线程”的操作如何实现?

每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先先把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需要从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。

 死锁:

1.如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。

2.另一种典型的死锁情形是这样的:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B永远处于挂起状态了。不难想象,如果涉及到更多的线程和更多的锁,死锁的问题将会变得复杂和难以判断。

写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序获得锁,则不会出现死锁。比如一个程序中用到锁1、锁2、锁3,它们对应的Mutex变量是 锁1->锁2->锁3,那么所有线程在需要同时获得2个或3个锁时应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mytex_trylock调用代替pthread_mutex_lock调用,以免死锁(即不阻塞等锁,获得不到锁线程可以先干点别的)。

条件变量:

线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。Condition Variable用pthread_cond_t类型的变量表示,可以这样初始化和销毁:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

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

Condition Variable的操作可以用下列函数:

#include <pthread.h>

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

int pthread_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

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

pthread_cond_timedwait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。一个线程可以调用pthread_cond_signal唤醒在某个Condition Variable上等待的另一个线程,也可以调用pthread_cond_broadcast唤醒在这个Condition Variable上等待的所有线程。

代码演示:

#include "./common/head.h"

/*功能:
 *2个线程,一个线程生产货物,另一个线程消费货物。
 *生产者和消费者操作同一链表时,需要加锁。
 *消费者必须等待生产者把货物生产好,才能消费,需要条件锁。
*/

//货物结构体
typedef struct Goods{
    int data;
    struct Goods *next;
}Goods;

Goods *head = NULL;

//初始化锁(线程锁和条件变量)
pthread_mutex_t headlock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t hasGood = PTHREAD_COND_INITIALIZER;

//生产者线程
void *producer(void *arg){
    Goods *ng;
    while(1){
        ng = (Goods *)malloc(sizeof(Goods));
        pthread_mutex_lock(&headlock);    //加锁,防止与消费者同时操作
        //将新生产的货物插入链表,头插法
        ng->next = head;    
        head = ng;
        pthread_mutex_unlock(&headlock);    //解锁
        pthread_cond_broadcast(&hasGood, &headlock);    //唤醒消费者线程去抢headlock,此时已经有货物可以消费了;此处也可以使用pthread_cond_signal;解锁和唤醒线程这两步可以变换顺序
        printf("produce %d\n", ng->data);
        sleep(rand()%3);    //休息随机秒数再生产
    }       
}

//消费者线程
void *consumer(void *arg){
    Goods *k;
    while(1){  
        pthread_mutex_lock(&headlock);    //获取锁
        while(!head){    //当没有货物可以消费时,将锁丢出去,等待条件发生。此处必须用while,不能用if,因为如果有多个消费者,第一个消费者取到锁后消费掉了,另外消费者也可以取到锁运行,可是此时已经没有货物了。
            pthread_cond_wait(&hasGood, &headlock);    //此函数内部会将进行解锁操作
        }
        k = head;
        head = head->next;
        pthread_mutex_unlock(&headlock);    //解锁
        printf("consume %d\n", k->data);
        free(k);
        sleep(rand()%3);    //休息随机秒再消费    
    }
}

int main()
{
    srand(time(NULL));    //获取随机数种子
    
    pthread_t pid, cid;    //生产者,消费者
    
    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    //等待线程死亡
    pthread_join(pid, NULL);
    pthread-join(cid, NULL);

    return 0;
}

结果:

出现的问题及解决:

当消费者多起来后,运行一段时间,生产者会生产出超过100的数 。可能是因为打印时,链表又被其他消费者线程更改了。可以考虑将生产者的解锁和唤醒放在printf函数之后。

信号量:

 Mutex变量是非0即1的,可看作一种资源的可用数量,初始化时Mutex是1,表示有1个可用资源,加锁时获得该资源,将Mutex减到0,表示不再有可用资源,解锁时释放该资源,将Mutex重新加到1,表示又有了1个可用资源。

信号量(Semaphore)和Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1.这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步。

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

int sem_post(sem_t *sem);

int sem_destroy(sem_t *sem);

semaphore变量的类型为sem_t

sem_init()初始化一个semaphore变量,value参数表示可用资源数量,pshared参数为0表示信号量用于同一进程的线程间同步,为1表示不同进程的线程间同步。

在用完semaphore变量之后应该调用sem_destrooy()释放与semaphore相关的资源。

调用set_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()

调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。

代码演示:

未完待续!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值