Linux 系统编程 学习:11-线程:线程同步

情景导入

我们都知道引入线程在合理的范围内可以加快提高程序的效率。但我们先来看看如果多线程同时访问一个临界资源会怎么样。

例程:模拟多窗口售票

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

int ticket_sum = 20;

static int no = 0; // 为了登记是第几条线程在工作
void *sell_ticket(void *arg){
    no++;
    int number = no;

    for(int i=0; i<20; i++)
    {
        if(ticket_sum>0)
        {
            sleep(1);
            printf("No.%d sell the %d th\n", number, 20 - ticket_sum + 1);
            ticket_sum--;
        }else{
            break;
        }
    }
    return 0;
}

intmain(){
    int flag, i;
    pthread_t tids[4];

    for(i = 0; i < 4; i++)
    {
        flag = pthread_create(&tids[i],NULL, &sell_ticket, NULL);
        if(flag)
        {
            printf("Create failed");
            return flag;
        }
    }

    sleep(2);
    void *ans;
    for(int i=0; i<4; i++)
    {
        flag=pthread_join(tids[i],&ans);
        if(flag)
        {
            return flag;
        }
    }
    return 0;
}

由于没有引入同步机制,运行结果并不合理。

同步的有关概念

临界资源:每次只允许一个线程进行访问的资源
线程间互斥:多个线程在同一时刻都需要访问临界资源
同步:在特殊情况下,控制多线程间的相对执行顺序

多线程编程的本质有三个方面:

  • 并发性是多线程的本质
  • 在宏观上,所有线程并行执行
  • 线程间相互独立,互不干涉

在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源;
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。

有关同步机制

在多线程环境中,当我们需要保持线程同步时,通常通过 锁 来实现。线程同步的常见方法:互斥锁、条件变量、读写锁、自旋锁、信号量、线程栅栏。

锁 是大家都应该遵守的“君子”条约,因为如果有一个线程没有遵守,那么就没有意义了:

  • 对共享资源操作前一定要获得锁。
  • 完成操作以后一定要释放锁。(多个锁时, 若获得顺序是ABC连环扣, 释放顺序也应该是ABC。)
  • 尽量短时间地占用锁。
  • 线程错误返回时应该释放它所获得的锁。

互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。

这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。
在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

互斥锁的特点:

  • 原子性:把一个互斥锁锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥锁,没有其他线程在同一时间可以成功锁定这个互斥锁
  • 唯一性:如果一个线程锁定了一个互斥锁,在它解除锁定之前,没有其他线程可以锁定这个互斥锁
  • 非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥锁的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥锁。

应用互斥锁需要注意的几点

1、互斥锁需要时间来加锁和解锁。锁住较少互斥锁的程序通常运行得更快。所以,互斥锁应该尽量少,够用即可,每个互斥锁保护的区域应则尽量大。

2、互斥锁的本质是串行执行。如果很多线程需要领繁地加锁同一个互斥锁,则线程的大部分时间就会在等待,这对性能是有害的。如果互斥锁保护的数据(或代码)包含彼此无关的片段,则可以特大的互斥锁分解为几个小的互斥锁来提高性能。这样,任意时刻需要小互斥锁的线程减少,线程等待时间就会减少。所以,互斥锁应该足够多(到有意义的地步),每个互斥锁保护的区域则应尽量的少。

互斥锁初始化与销毁

互斥锁的初始化有动态初始化与静态初始化2种方式。

动态初始化、销毁

c
intpthread_mutex_init(pthread_mutex_t *restrict mutex, constpthread_mutexattr_t *restrict attr);

intpthread_mutex_destroy(pthread_mutex_t *mutex);

描述:动态互斥锁初始化函数(不再使用时应该调用 pthread_mutex_destroy进行销毁)

参数解析:

mutex:互斥锁对象的地址
attr:互斥锁属性对象的地址,当为NULL,代表默认互斥锁初始化。

静态初始化(不需要销毁)

仅局限于静态初始化的时候使用:将“声明”、“定义”、“初始化”一气呵成,除此之外的情况都只能使用pthread_mutex_init函数。

c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化例程

c
#if 0
	// 静态互斥锁初始化。 (不需要pthread_mutex_destroy)
	pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;	
#else
	// 动态互斥锁初始化。
	pthread_mutex_t mutex1;
	pthread_mutex_init(&mutex1, NULL);
	...
	pthread_mutex_destroy(&mutex1);
#endif

上锁与解锁

c
intpthread_mutex_lock(pthread_mutex_t *mutex);
intpthread_mutex_trylock(pthread_mutex_t *mutex);
intpthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
				   const struct timespec *restrict abstime);

intpthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_lock

上锁,如果上锁前已经锁上,则挂起等待。

pthread_mutex_trylock

尝试上锁,如果上锁前已经锁上时,返回EBUSY而不是挂起等待。

pthread_mutex_timedlock

pthread_mutex_lock是基本等价的,但是在达到超时时间值时,pthread_mutex_timedlock不会对互斥锁进行加锁,而是返回错误码ETIMEDOUT

pthread_mutex_unlock

使用结束后在进程退出前,解锁。这样,别的进程就可以获取到锁。

如果解锁一个未加锁的mutex互斥锁,行为是未知的。

互斥锁有关例程

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;

int count = 100;

void *routine(void *arg){
    printf("start thread\n");

    pthread_mutex_lock(&mutex);

    printf("child in mutex lock\n");

    if(count >= 100)
    {
        printf("in child\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("child count = %d\n", count);

    return NULL;
}

intmain(void){
    pthread_t tid;
    pthread_mutex_init(&mutex, NULL);

    errno = pthread_create(&tid, NULL, routine, NULL);
    if(errno != 0)
    {
        perror("create thread failed\n");
        return -1;
    }

    pthread_mutex_lock(&mutex);

    if(count >= 100)
    {
        printf("in parent\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("parent count = %d\n", count);

    pthread_join(tid, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}

条件变量

操作互斥锁时就会一直在循环判断,而每次判断都要加锁、解锁(即使本次并没有修改临界资源)。这就带来了问题:

1)CPU浪费严重。
2)响应处理可能会导致不够及时。

条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。

条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。

特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。

条件变量一般配合互斥锁进行使用。

条件变量的使用流程:

  • 线程A得到了互斥锁,出于某个原因开始等待条件T。
  • 等待时,系统先解开线程A得到的锁,然后将它挂起。
  • 如果还有线程B和A一样,那么同样也进入等待(同样地解锁,同样地挂起)
  • 此时,独立于线程AB以外的另外一个线程C,重置了条件T,C可以选择唤醒处于等待的AB:要么C 广播,引发惊群,让AB来抢;要么C 单独通知,告诉最先等待的线程(在这里是A)。
  • 假设这里C选择单独通知,那么A就会被唤醒,看到条件满足了以后就重新加锁。这样一来,就省去了对于资源读取时不用去轮训等待了。

什么是惊群:

举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。每扔一块食物,都会惊动所有的鸽子,即为惊群。

对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。

条件变量的初始化与销毁

条件变量的初始化也有动态初始化与静态初始化2种方式。这次我们放在一起讲:

c
// 动态初始化一个条件变量 
intpthread_cond_init(pthread_cond_t *restrict cond, constpthread_condattr_t *restrict attr);
	attr :默认NULL(LinuxThreads 实现条件变量不支持属性,因此 cond_attr 参数实际被忽略。)
// 销毁一个动态初始化的条件变量(在不使用条件变量时使用)
intpthread_cond_destroy(pthread_cond_t *cond);
	
// 静态初始化一个条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

进入等待

c
intpthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

intpthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

描述:挂起线程直到其他线程触发条件后才被唤醒。函数执行时先自动释放指定的锁(允许其他线程访问),然后等待条件变量的变化。在条件满足从而离开 pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求等待条件pthread_cond_wait()(或pthread_cond_timedwait())的竞争条件(Race Condition)。

条件变量的使用前提:

mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁 (PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁 (pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。

唤醒一个等待者

c
intpthread_cond_signal(pthread_cond_t * cond);

描述:通过条件变量cond发送消息,若多个线程在等待,它只唤醒最先等待的那一个。

注: 调用 pthread_cond_signal 后要立刻释放互斥锁

c
intpthread_cond_broadcast(pthread_cond_t *cond);
intpthread_cond_signal(pthread_cond_t *cond);
// 重启动等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。(引发 惊群 效应)

唤醒所有等待者

c
intpthread_cond_broadcast(pthread_cond_t *cond);

描述:唤醒等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。(引发 惊群 效应)

例程:生产者消费者模型

在经典的生产者-消费者场合中,生产者首先必须检查缓冲是否已满(numUsedBytes==BufferSize),如果缓冲区已满,线程停下来等待 bufferNotFull条件。如果没有满,在缓冲中生产数据,增加numUsedBytes,激活条件 bufferNotEmpty。使用mutex来保护对numUsedBytes的访问。

pthread_cond_wait 接收一个mutex作为参数,mutex被调用线程初始化为锁定状态。在线程进入休眠状态之前,mutex会被解锁。而当线程被唤醒时,mutex会处于锁定状态;从锁定状态到等待状态的转换是原子操作。当程序开始运行时,只有生产者可以工作,消费者被阻塞等待bufferNotEmpty条件,一旦生产者在缓冲中放入一个字节,bufferNotEmpty条件被激发,消费者线程于是被唤醒。

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;
pthread_cond_t buffer_is_not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t buffer_is_not_full  = PTHREAD_COND_INITIALIZER;

const int DataSize = 10;
const int BufferSize = 1;
static int usedSpace = 0;

void *producer(void *arg){
    printf("producer thread\n");

    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == BufferSize)
        {
            pthread_cond_wait(&buffer_is_not_full, &mutex);
        }
        ++usedSpace;
        printf("P, %d\n", usedSpace);

        pthread_cond_broadcast(&buffer_is_not_empty);
        pthread_mutex_unlock(&mutex);
    }
    return "OK.";
}

void * consumer(void *arg){
    printf("consumer thread\n");
    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == 0)
        {
            pthread_cond_wait(&buffer_is_not_empty, &mutex);
        }
        --usedSpace;
        printf("C, %d\n", usedSpace);
        pthread_cond_broadcast(&buffer_is_not_full);
        pthread_mutex_unlock(&mutex);
    }
    printf("\n");
    return "OK.";
}

intmain(void){
    pthread_t tid_producer;
    pthread_t tid_consumer;
    errno = pthread_create(&tid_producer, NULL, producer, NULL);
    if(errno != 0)
    {
        perror("create thread for producer failed\n");
        return -1;
    }
    errno = pthread_create(&tid_consumer, NULL, consumer, NULL);
    if(errno != 0)
    {
        perror("create thread for consumer failed\n");
        return -1;
    }

    sleep(1);

    pthread_join(tid_consumer, NULL);
    pthread_join(tid_producer, NULL);


    return 0;
}

读写锁

根据把对共享资源的访问情况,划分成读者和写者:

  • 读者只对共享资源进行读访问。
  • 写者则需要对共享资源进行写操作。

读写锁与互斥锁的功能类似,但比它有更高的并行性:

1)在加上读锁后不能写;加上写锁后不能读写,否则阻塞

2)允许多个读者可以同时进行读

3)写者互斥(只允许一个写者写)

4)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

读写锁的初始化与销毁

读写锁的初始化也有动态初始化与静态初始化2种方式。我们放在一起讲:

c
// 动态初始化 与 销毁
intpthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           constpthread_rwlockattr_t *restrict attr);
intpthread_rwlock_destroy(pthread_rwlock_t *rwlock);

// 静态初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

上读锁

c
intpthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 

描述:加读锁(允许其他读锁访问,不允许写锁访问)

上写锁

c
intpthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

描述:加写锁(不允许其他读 或 写锁访问)

解锁

c
intpthread_rwlock_unlock(pthread_rwlock_t *rwlock);

描述:解开当前加的锁

例程:读写锁例程

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

int num=5;
pthread_rwlock_t rwlock;

void *reader(void *arg){
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader %ld got the lock\n", (long)arg );

    pthread_rwlock_unlock(&rwlock);
    return 0;
}

void *writer(void *arg){
    pthread_rwlock_wrlock(&rwlock);
    printf("Writer %ld got the lock\n", (long)arg );
    pthread_rwlock_unlock(&rwlock);
    return 0;
}

intmain(){
    int flag;
    long n = 1, m = 1;
    pthread_t wid[5],rid[5];
    pthread_attr_t attr;

    flag = pthread_rwlock_init(&rwlock,NULL);
    if(flag)
    {
        printf("rwlock init error\n");
        return flag;
    }

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//thread sepatate

    for(int i=0;i<num;i++)
    {
        if(i%3)
        {
            pthread_create(&rid[n-1],&attr,reader,(void *)n);
            printf("create reader %ld\n", n);
            n++;
        }else
        {
            pthread_create(&wid[m-1],&attr,writer,(void *)m);
            printf("create Writer %ld\n", m);
            m++;
        }
    }

    sleep(5);//wait other done

    return 0;
}

自旋锁

自旋锁与互斥锁的区别:

在多处理器环境中,自旋锁最多只能被一个可执行线程持有。如果一个可执行线程试图获得一个被争用(已经被持有的)自旋锁,那么该线程就会一直进行忙等待,自旋,也就是空转,等待锁重新可用。如果锁未被争用,请求锁的执行线程便立刻得到它,继续向下执行。

一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,特别的浪费CPU时间,所以自旋锁不应该被长时间的持有。实际上,这就是自旋锁的设计初衷,在短时间内进行轻量级加锁。

信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用而不能在中断上下文使用,因为中断的上下文不允许休眠(trylock可以),因此在中断上下文只能使用自旋锁。
自旋锁保持期间是抢占失效的(内核不允许被抢占) ,而信号量和读写信号量保持期间是可以被抢占的。

自旋锁保护的临界区默认是可以相应中断的,但是如果在中断处理程序中请求相同的自旋锁,那么会发生死锁(内核自旋锁可以关闭中断)。

自旋锁 的 初始化与销毁

c
intpthread_spin_destroy(pthread_spinlock_t *lock);
intpthread_spin_init(pthread_spinlock_t *lock, int pshared);

描述:初始化/销毁一个自旋锁对象。

参数解析:

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示这个自旋锁是当前进程的局部自旋锁
  • PTHREAD_PROCESS_SHARED:这个自旋锁可以在多个进程之间共享

如果想要使用自旋锁同步多进程,那么设置pshared = PTHREAD_PROCESS_SHARED,然后在进程共享内存中分配pthread_spinlock_t 对象即可(pthread_mutex_t亦如此,但其需要进行pthread_mutexattr_setpshared())。

自旋锁操作

c
intpthread_spin_lock(pthread_spinlock_t *lock);
intpthread_spin_trylock(pthread_spinlock_t *lock);
intpthread_spin_unlock(pthread_spinlock_t *lock);

与互斥锁基本一样。

POSIX信号量

POSIX代表 “可移植操作系统接口” Portable Operation System Interface 。只要按照这个API标准写程序,理论上就可以在各个操作系统和硬件平台上编译运行。

POSIX 与 System V区别

  • System V IPC存在时间比较老,许多系统都支持,但是接口复杂,并且可能各平台上实现略有区别(如ftok的实现及限制)
  • posix信号量只能单个操作,IPC信号量可以多个信号量同时操作,IPC信号量更加强大
  • POSIX是新标准,语法简单,并且各平台上实现都一样

信号量:一个定义在内核系统中的特殊的变量,有以下定义:
1)P操作:减操作
2)V操作:加操作
3)该变量最小为0,如果等于0的情况下还去进行P操作,就会阻塞

The canonical names V and P come from the initials of Dutch words. V is generally explained as verhogen ("increase"). Several explanations have been offered for P, including proberen ("to test" or "to try"), passeren ("pass"), and pakken ("grab"). Dijkstra's earliest paper on the subject givespassering ("passing") as the meaning for P, and vrijgave ("release") as the meaning for V. It also mentions that the terminology is taken from that used in railroad signals. Dijkstra subsequently wrote that he intended P to stand for the portmanteauprolaag, short for probeer te verlagen, literally "try to reduce," or to parallel the terms used in the other case, "try to decrease."

信号量(sem)和互斥锁的区别:

互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区

使用时,需要以下头文件:

c
#include<semaphore.h>

POSIX信号量有两种:有名信号量(可以在进程中使用)和无名信号量。有名信号量与无名信号量使用同一套函数(但是在初始化和销毁的函数有所不同)

我们这里介绍无名信号量。

c
初始化并打开有名信号量:sem_open();       初始化无名信号量:sem_init()

操作信号量:sem_wait()/sem_trywait()/sem_timedwait()/sem_post()/sem_getvalue()

    
关闭有名信号量:sem_close(); //关闭有名信号量
销毁有名信号量:sem_unlink(); //试图销毁信号量,一旦所有占用该信号量的进程都关闭了该信号量,那么就会销毁这个信号量

销毁无名信号量:sem_destroy()

初始化与销毁信号量

c
intsem_init(sem_t *sem, int pshared, unsignedint value);
intsem_destory(sem_t *sem);

描述:初始化/销毁一个信号量对象

参数解析:

sem:信号量对象

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示这个信号量是当前进程的局部信号量
  • PTHREAD_PROCESS_SHARED:这个信号量可以在多个进程之间共享

value:初始值

返回值:成功符合0,失败返回-1。

信号量操作

P操作

c
intsem_wait(sem_t *sem);
intsem_trywait(sem_t *sem); //试图占用信号量,如果信号量已经为0,立即报错(errno 设置为 EAGAIN))
intsem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

描述:以原子操作的方式将信号量的值减去1。

返回值:成功返回0;失败返回-1,设置errno。

V操作

c
intsem_post(sem_t *sem);

描述::以原子操作的方式将信号量的值加上1。

返回值:成功返回0;失败返回-1,设置errno。

取值

c
intsem_getvalue(sem_t *sem, int *sval);

描述:获得信号量当前的值,放到sval中。

参数解析:

sval:存放结果的容器。

注意:如果有线程正在block这个信号量,sval可能返回两个值其中的一个,0或“-正在block的线程的数目”,在Linux中返回0。

If one or more processes or threads are blocked waiting to lock the semaphore with sem_wait(), POSIX.1 permits two possibilities for the value returned in #sval: either 0 is returned; or a negative number whose absolute value is the count of the number of processes and threads currently blocked in sem_wait(). Linux adopts the former behavior.

例程:信号量

通过信号量模拟2个窗口,10个客人进行服务的过程。

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<semaphore.h>


int num=10;
sem_t sem;

void *get_service(void *cid){
    int id=*((int*)cid);
    if(sem_wait(&sem)==0)
    {
        printf("customer %d get the service \n", id);
        sleep(2);
        printf("customer %d done \n", id);
        sem_post(&sem);
    }
    return 0;
}

intmain(){
    sem_init(&sem,0,2);
    pthread_t customer[num];
    int flag;

    for(int i=0;i<num;i++)
    {
        int id=i;
        flag=pthread_create(&customer[i],NULL,get_service,&id);
        if(flag)
        {
            return flag;
        }else
        {
        }
        sleep(1);
    }

    //wait all thread done
    for(int j=0;j<num;j++)
    {
        pthread_join(customer[j],NULL);
    }
    sem_destroy(&sem);
    return 0;
}

线程栅栏

把先后到达的多个线程挡在同一栅栏前,直到所有线程(可指定个数)到齐,然后撤下栅栏同时放行。

使用场景

这种“栅栏”机制最大的特点就是最后一个执行wait的动作最为重要,就像赛跑时的起跑枪一样,它来之前所有人都必须等着。所以实际使用中,pthread_barrier_wait常常用来让所有线程等待“起跑枪”响起后再一起行动。

比如我们可以用pthread_create()生成100个线程,每个子线程在被create出的瞬间就会自顾自的立刻进入回调函数运行。但我们可能不希望它们这样做,因为这时主进程还没准备好,和它们一起配合的其它线程还没准备好,我们希望它们在回调函数中申请完线程空间、初始化后停下来,一起等待主进程释放一个“开始”信号,然后所有线程再开始执行业务逻辑代码。

为了解决上述场景问题,我们可以在init时指定n+1个等待,其中n是线程数。而在每个线程执行函数的首部调用wait()。这样100个pthread_create()结束后所有线程都停下来等待最后一个wait()函数被调用。这个wait()由主进程在它觉得合适的时候调用就好。最后这个wait()就是鸣响的起跑枪。

栅栏的初始化与销毁

c
intpthread_barrier_init(pthread_barrier_t *restrict barrier, constpthread_barrierattr_t *restrict attr, unsigned count);

intpthread_barrier_destroy(pthread_barrier_t *barrier);

描述:初始化/销毁一个栅栏对象,初始化时要指定等待者个数。

参数解析:

count:等待者个数

在栅栏前等待

c
intpthread_barrier_wait(pthread_barrier_t *barrier);
到栅栏前等待放行,如果该条函数执行的次数等于 #count 时,放行。

描述:让一个线程在栅栏前告诉大家它已经就绪,等待放行。

例程:栅栏的使用

c
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>

pthread_barrier_t barrier;

void *Task1(void *arg);
void *Task2(void *arg);

intmain(void){
    int policy,inher;
    pthread_t tid;
    pthread_attr_t attr;
    structsched_paramparam;

    //初始化线程属性
    pthread_attr_init(&attr);
    pthread_barrier_init(&barrier,NULL,2 + 1);//2+1个等待(2个线程,1个自己)

    //创建线程1
    pthread_create(&tid, &attr,Task1,NULL);

    //创建线程2
    pthread_create(&tid, &attr,Task2,NULL);
    
    printf("main process will sleep 6s.\n");
    sleep(6);/*等待6s后,才让线程运行*/
    pthread_barrier_wait(&barrier);//起跑枪“砰!”

    pthread_join(tid, NULL);
    pthread_barrier_destroy(&barrier);
}

void *Task1(void *arg){
    printf("Task1 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有线程都被阻塞在这里
    printf("Task1 is running.\n");
    sleep(3);//延时3s
    pthread_exit(NULL);
}

void *Task2(void *arg){
    printf("Task2 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有线程都被阻塞在这里
    printf("Task2 is running.\n");
    sleep(3);//延时3s
    pthread_exit(NULL);
}

为同步对象设置属性

https://blog.csdn.net/bytxl/article/details/8822551

在上文的学习中,为了降低学习的理解难度,我们没有介绍互斥锁的属性。现在我们就来介绍有关内容。

线程和线程的同步对象(互斥锁,读写锁,条件变量,栅栏)都具有属性。在修改属性前都需要对该结构进行初始化。使用后要把该结构回收。

除了互斥锁还可以设置锁的类型以外,所有的同步对象只能设置作用域。

互斥锁属性

初始化与销毁

c
#include<pthread.h>

intpthread_mutexattr_destroy(pthread_mutexattr_t *attr);
intpthread_mutexattr_init(pthread_mutexattr_t *attr);

描述:类似于线程属性(但没有静态初始化方法),使用时初始化(设置了默认属性),使用后销毁(设置了无效属性)

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

设置互斥锁的共享方式

c
#include<pthread.h>

intpthread_mutexattr_getpshared(constpthread_mutexattr_t
                                 *restrict attr, int *restrict pshared);
intpthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
                                 int pshared);

描述:设置互斥锁的作用域。

参数解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的互斥锁只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许互斥锁在进程间中使用)

在进程间的用法:设置以后,将一个互斥锁放置到共享内存中即可。

设置互斥锁的类型

c
#include<pthread.h>

intpthread_mutexattr_gettype(constpthread_mutexattr_t *restrict attr,
                              int *restrict type);
intpthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

描述:设置互斥锁的类型。

参数解析:

type: 由于 DEFAULT ( NORMAL) 属性有太多的未定义行为,应该尽可能避免使用。

  • PTHREAD_MUTEX_DEFAULT(默认)
  • PTHREAD_MUTEX_NORMAL
  • PTHREAD_MUTEX_ERRORCHECK
  • PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_NORMAL

这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起这个线程的死锁。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。

PTHREAD_MUTEX_ERRORCHECK

这种类型的互斥锁会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。

PTHREAD_MUTEX_RECURSIVE

如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE)。

条件变量属性

初始化与销毁

c
intpthread_condattr_init(pthread_condattr_t *cattr);
intpthread_condattr_destroy(pthread_condattr_t *cattr);

类似上文,不再描述。

设置共享方式

c
intpthread_condattr_getpshared(constpthread_condattr_t *restrict attr,
                                int *restrict pshared);
intpthread_condattr_setpshared(pthread_condattr_t *attr,
                                int pshared);

参数解析:既然条件变量是搭配互斥锁用的,那么同样地,就可以设置共享方式。

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的条件变量只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许条件变量在进程间中使用)

在进程间的用法:设置以后,将条件变量放置到共享内存中即可。

读写锁属性

初始化与销毁

c
intpthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
intpthread_rwlockattr_init(pthread_rwlockattr_t *attr);

类似上文,不再描述。

设置共享方式

c
intpthread_rwlockattr_getpshared(constpthread_rwlockattr_t
                                  *restrict attr, int *restrict pshared);
intpthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,
                                  int pshared);

参数解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的条件变量只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许条件变量在进程间中使用)

在进程间的用法:设置以后,将读写锁放置到共享内存中即可。

栅栏属性

初始化与销毁

c
intpthread_barrierattr_destroy(pthread_barrierattr_t *attr);
intpthread_barrierattr_init(pthread_barrierattr_t *attr);

类似上文,不再描述。

设置共享方式

c
intpthread_barrierattr_getpshared(constpthread_barrierattr_t
                                   *restrict attr, int *restrict pshared);
intpthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
                                   int pshared);

参数解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的条件变量只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许条件变量在进程间中使用)

在进程间的用法:设置以后,将栅栏放置到共享内存中即可。

情景导入

我们都知道引入线程在合理的范围内可以加快提高程序的效率。但我们先来看看如果多线程同时访问一个临界资源会怎么样。

例程:模拟多窗口售票

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

int ticket_sum = 20;

static int no = 0; // 为了登记是第几条线程在工作
void *sell_ticket(void *arg){
    no++;
    int number = no;

    for(int i=0; i<20; i++)
    {
        if(ticket_sum>0)
        {
            sleep(1);
            printf("No.%d sell the %d th\n", number, 20 - ticket_sum + 1);
            ticket_sum--;
        }else{
            break;
        }
    }
    return 0;
}

intmain(){
    int flag, i;
    pthread_t tids[4];

    for(i = 0; i < 4; i++)
    {
        flag = pthread_create(&tids[i],NULL, &sell_ticket, NULL);
        if(flag)
        {
            printf("Create failed");
            return flag;
        }
    }

    sleep(2);
    void *ans;
    for(int i=0; i<4; i++)
    {
        flag=pthread_join(tids[i],&ans);
        if(flag)
        {
            return flag;
        }
    }
    return 0;
}

由于没有引入同步机制,运行结果并不合理。

同步的有关概念

临界资源:每次只允许一个线程进行访问的资源
线程间互斥:多个线程在同一时刻都需要访问临界资源
同步:在特殊情况下,控制多线程间的相对执行顺序

多线程编程的本质有三个方面:

  • 并发性是多线程的本质
  • 在宏观上,所有线程并行执行
  • 线程间相互独立,互不干涉

在多任务操作系统中,同时运行的多个任务可能:

  • 都需要访问/使用同一种资源;
  • 多个任务之间有依赖关系,某个任务的运行依赖于另一个任务。

有关同步机制

在多线程环境中,当我们需要保持线程同步时,通常通过 锁 来实现。线程同步的常见方法:互斥锁、条件变量、读写锁、自旋锁、信号量、线程栅栏。

锁 是大家都应该遵守的“君子”条约,因为如果有一个线程没有遵守,那么就没有意义了:

  • 对共享资源操作前一定要获得锁。
  • 完成操作以后一定要释放锁。(多个锁时, 若获得顺序是ABC连环扣, 释放顺序也应该是ABC。)
  • 尽量短时间地占用锁。
  • 线程错误返回时应该释放它所获得的锁。

互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。

这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。
在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

互斥锁的特点:

  • 原子性:把一个互斥锁锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥锁,没有其他线程在同一时间可以成功锁定这个互斥锁
  • 唯一性:如果一个线程锁定了一个互斥锁,在它解除锁定之前,没有其他线程可以锁定这个互斥锁
  • 非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥锁的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥锁。

应用互斥锁需要注意的几点

1、互斥锁需要时间来加锁和解锁。锁住较少互斥锁的程序通常运行得更快。所以,互斥锁应该尽量少,够用即可,每个互斥锁保护的区域应则尽量大。

2、互斥锁的本质是串行执行。如果很多线程需要领繁地加锁同一个互斥锁,则线程的大部分时间就会在等待,这对性能是有害的。如果互斥锁保护的数据(或代码)包含彼此无关的片段,则可以特大的互斥锁分解为几个小的互斥锁来提高性能。这样,任意时刻需要小互斥锁的线程减少,线程等待时间就会减少。所以,互斥锁应该足够多(到有意义的地步),每个互斥锁保护的区域则应尽量的少。

互斥锁初始化与销毁

互斥锁的初始化有动态初始化与静态初始化2种方式。

动态初始化、销毁

c
intpthread_mutex_init(pthread_mutex_t *restrict mutex, constpthread_mutexattr_t *restrict attr);

intpthread_mutex_destroy(pthread_mutex_t *mutex);

描述:动态互斥锁初始化函数(不再使用时应该调用 pthread_mutex_destroy进行销毁)

参数解析:

mutex:互斥锁对象的地址
attr:互斥锁属性对象的地址,当为NULL,代表默认互斥锁初始化。

静态初始化(不需要销毁)

仅局限于静态初始化的时候使用:将“声明”、“定义”、“初始化”一气呵成,除此之外的情况都只能使用pthread_mutex_init函数。

c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化例程

c
#if 0
	// 静态互斥锁初始化。 (不需要pthread_mutex_destroy)
	pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;	
#else
	// 动态互斥锁初始化。
	pthread_mutex_t mutex1;
	pthread_mutex_init(&mutex1, NULL);
	...
	pthread_mutex_destroy(&mutex1);
#endif

上锁与解锁

c
intpthread_mutex_lock(pthread_mutex_t *mutex);
intpthread_mutex_trylock(pthread_mutex_t *mutex);
intpthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
				   const struct timespec *restrict abstime);

intpthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_lock

上锁,如果上锁前已经锁上,则挂起等待。

pthread_mutex_trylock

尝试上锁,如果上锁前已经锁上时,返回EBUSY而不是挂起等待。

pthread_mutex_timedlock

pthread_mutex_lock是基本等价的,但是在达到超时时间值时,pthread_mutex_timedlock不会对互斥锁进行加锁,而是返回错误码ETIMEDOUT

pthread_mutex_unlock

使用结束后在进程退出前,解锁。这样,别的进程就可以获取到锁。

如果解锁一个未加锁的mutex互斥锁,行为是未知的。

互斥锁有关例程

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;

int count = 100;

void *routine(void *arg){
    printf("start thread\n");

    pthread_mutex_lock(&mutex);

    printf("child in mutex lock\n");

    if(count >= 100)
    {
        printf("in child\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("child count = %d\n", count);

    return NULL;
}

intmain(void){
    pthread_t tid;
    pthread_mutex_init(&mutex, NULL);

    errno = pthread_create(&tid, NULL, routine, NULL);
    if(errno != 0)
    {
        perror("create thread failed\n");
        return -1;
    }

    pthread_mutex_lock(&mutex);

    if(count >= 100)
    {
        printf("in parent\n");
        sleep(3);
        count -= 100;
    }

    pthread_mutex_unlock(&mutex);

    printf("parent count = %d\n", count);

    pthread_join(tid, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}

条件变量

操作互斥锁时就会一直在循环判断,而每次判断都要加锁、解锁(即使本次并没有修改临界资源)。这就带来了问题:

1)CPU浪费严重。
2)响应处理可能会导致不够及时。

条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。

条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。

特别要注意,如果在信号处理程序中调用 pthread_cond_signal 或 pthread_cond_boardcast 函数,可能导致调用线程死锁。

条件变量一般配合互斥锁进行使用。

条件变量的使用流程:

  • 线程A得到了互斥锁,出于某个原因开始等待条件T。
  • 等待时,系统先解开线程A得到的锁,然后将它挂起。
  • 如果还有线程B和A一样,那么同样也进入等待(同样地解锁,同样地挂起)
  • 此时,独立于线程AB以外的另外一个线程C,重置了条件T,C可以选择唤醒处于等待的AB:要么C 广播,引发惊群,让AB来抢;要么C 单独通知,告诉最先等待的线程(在这里是A)。
  • 假设这里C选择单独通知,那么A就会被唤醒,看到条件满足了以后就重新加锁。这样一来,就省去了对于资源读取时不用去轮训等待了。

什么是惊群:

举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。每扔一块食物,都会惊动所有的鸽子,即为惊群。

对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:
1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。
2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。

条件变量的初始化与销毁

条件变量的初始化也有动态初始化与静态初始化2种方式。这次我们放在一起讲:

c
// 动态初始化一个条件变量 
intpthread_cond_init(pthread_cond_t *restrict cond, constpthread_condattr_t *restrict attr);
	attr :默认NULL(LinuxThreads 实现条件变量不支持属性,因此 cond_attr 参数实际被忽略。)
// 销毁一个动态初始化的条件变量(在不使用条件变量时使用)
intpthread_cond_destroy(pthread_cond_t *cond);
	
// 静态初始化一个条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

进入等待

c
intpthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

intpthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

描述:挂起线程直到其他线程触发条件后才被唤醒。函数执行时先自动释放指定的锁(允许其他线程访问),然后等待条件变量的变化。在条件满足从而离开 pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求等待条件pthread_cond_wait()(或pthread_cond_timedwait())的竞争条件(Race Condition)。

条件变量的使用前提:

mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁 (PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁 (pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。

唤醒一个等待者

c
intpthread_cond_signal(pthread_cond_t * cond);

描述:通过条件变量cond发送消息,若多个线程在等待,它只唤醒最先等待的那一个。

注: 调用 pthread_cond_signal 后要立刻释放互斥锁

c
intpthread_cond_broadcast(pthread_cond_t *cond);
intpthread_cond_signal(pthread_cond_t *cond);
// 重启动等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。(引发 惊群 效应)

唤醒所有等待者

c
intpthread_cond_broadcast(pthread_cond_t *cond);

描述:唤醒等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。(引发 惊群 效应)

例程:生产者消费者模型

在经典的生产者-消费者场合中,生产者首先必须检查缓冲是否已满(numUsedBytes==BufferSize),如果缓冲区已满,线程停下来等待 bufferNotFull条件。如果没有满,在缓冲中生产数据,增加numUsedBytes,激活条件 bufferNotEmpty。使用mutex来保护对numUsedBytes的访问。

pthread_cond_wait 接收一个mutex作为参数,mutex被调用线程初始化为锁定状态。在线程进入休眠状态之前,mutex会被解锁。而当线程被唤醒时,mutex会处于锁定状态;从锁定状态到等待状态的转换是原子操作。当程序开始运行时,只有生产者可以工作,消费者被阻塞等待bufferNotEmpty条件,一旦生产者在缓冲中放入一个字节,bufferNotEmpty条件被激发,消费者线程于是被唤醒。

c
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>

pthread_mutex_t mutex;
pthread_cond_t buffer_is_not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t buffer_is_not_full  = PTHREAD_COND_INITIALIZER;

const int DataSize = 10;
const int BufferSize = 1;
static int usedSpace = 0;

void *producer(void *arg){
    printf("producer thread\n");

    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == BufferSize)
        {
            pthread_cond_wait(&buffer_is_not_full, &mutex);
        }
        ++usedSpace;
        printf("P, %d\n", usedSpace);

        pthread_cond_broadcast(&buffer_is_not_empty);
        pthread_mutex_unlock(&mutex);
    }
    return "OK.";
}

void * consumer(void *arg){
    printf("consumer thread\n");
    for (int i = 0; i < DataSize; ++i)
    {
        pthread_mutex_lock(&mutex);
        while (usedSpace == 0)
        {
            pthread_cond_wait(&buffer_is_not_empty, &mutex);
        }
        --usedSpace;
        printf("C, %d\n", usedSpace);
        pthread_cond_broadcast(&buffer_is_not_full);
        pthread_mutex_unlock(&mutex);
    }
    printf("\n");
    return "OK.";
}

intmain(void){
    pthread_t tid_producer;
    pthread_t tid_consumer;
    errno = pthread_create(&tid_producer, NULL, producer, NULL);
    if(errno != 0)
    {
        perror("create thread for producer failed\n");
        return -1;
    }
    errno = pthread_create(&tid_consumer, NULL, consumer, NULL);
    if(errno != 0)
    {
        perror("create thread for consumer failed\n");
        return -1;
    }

    sleep(1);

    pthread_join(tid_consumer, NULL);
    pthread_join(tid_producer, NULL);


    return 0;
}

读写锁

根据把对共享资源的访问情况,划分成读者和写者:

  • 读者只对共享资源进行读访问。
  • 写者则需要对共享资源进行写操作。

读写锁与互斥锁的功能类似,但比它有更高的并行性:

1)在加上读锁后不能写;加上写锁后不能读写,否则阻塞

2)允许多个读者可以同时进行读

3)写者互斥(只允许一个写者写)

4)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

读写锁的初始化与销毁

读写锁的初始化也有动态初始化与静态初始化2种方式。我们放在一起讲:

c
// 动态初始化 与 销毁
intpthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           constpthread_rwlockattr_t *restrict attr);
intpthread_rwlock_destroy(pthread_rwlock_t *rwlock);

// 静态初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

上读锁

c
intpthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 

描述:加读锁(允许其他读锁访问,不允许写锁访问)

上写锁

c
intpthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

描述:加写锁(不允许其他读 或 写锁访问)

解锁

c
intpthread_rwlock_unlock(pthread_rwlock_t *rwlock);

描述:解开当前加的锁

例程:读写锁例程

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

int num=5;
pthread_rwlock_t rwlock;

void *reader(void *arg){
    pthread_rwlock_rdlock(&rwlock);
    printf("Reader %ld got the lock\n", (long)arg );

    pthread_rwlock_unlock(&rwlock);
    return 0;
}

void *writer(void *arg){
    pthread_rwlock_wrlock(&rwlock);
    printf("Writer %ld got the lock\n", (long)arg );
    pthread_rwlock_unlock(&rwlock);
    return 0;
}

intmain(){
    int flag;
    long n = 1, m = 1;
    pthread_t wid[5],rid[5];
    pthread_attr_t attr;

    flag = pthread_rwlock_init(&rwlock,NULL);
    if(flag)
    {
        printf("rwlock init error\n");
        return flag;
    }

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//thread sepatate

    for(int i=0;i<num;i++)
    {
        if(i%3)
        {
            pthread_create(&rid[n-1],&attr,reader,(void *)n);
            printf("create reader %ld\n", n);
            n++;
        }else
        {
            pthread_create(&wid[m-1],&attr,writer,(void *)m);
            printf("create Writer %ld\n", m);
            m++;
        }
    }

    sleep(5);//wait other done

    return 0;
}

自旋锁

自旋锁与互斥锁的区别:

在多处理器环境中,自旋锁最多只能被一个可执行线程持有。如果一个可执行线程试图获得一个被争用(已经被持有的)自旋锁,那么该线程就会一直进行忙等待,自旋,也就是空转,等待锁重新可用。如果锁未被争用,请求锁的执行线程便立刻得到它,继续向下执行。

一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,特别的浪费CPU时间,所以自旋锁不应该被长时间的持有。实际上,这就是自旋锁的设计初衷,在短时间内进行轻量级加锁。

信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用而不能在中断上下文使用,因为中断的上下文不允许休眠(trylock可以),因此在中断上下文只能使用自旋锁。
自旋锁保持期间是抢占失效的(内核不允许被抢占) ,而信号量和读写信号量保持期间是可以被抢占的。

自旋锁保护的临界区默认是可以相应中断的,但是如果在中断处理程序中请求相同的自旋锁,那么会发生死锁(内核自旋锁可以关闭中断)。

自旋锁 的 初始化与销毁

c
intpthread_spin_destroy(pthread_spinlock_t *lock);
intpthread_spin_init(pthread_spinlock_t *lock, int pshared);

描述:初始化/销毁一个自旋锁对象。

参数解析:

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示这个自旋锁是当前进程的局部自旋锁
  • PTHREAD_PROCESS_SHARED:这个自旋锁可以在多个进程之间共享

如果想要使用自旋锁同步多进程,那么设置pshared = PTHREAD_PROCESS_SHARED,然后在进程共享内存中分配pthread_spinlock_t 对象即可(pthread_mutex_t亦如此,但其需要进行pthread_mutexattr_setpshared())。

自旋锁操作

c
intpthread_spin_lock(pthread_spinlock_t *lock);
intpthread_spin_trylock(pthread_spinlock_t *lock);
intpthread_spin_unlock(pthread_spinlock_t *lock);

与互斥锁基本一样。

POSIX信号量

POSIX代表 “可移植操作系统接口” Portable Operation System Interface 。只要按照这个API标准写程序,理论上就可以在各个操作系统和硬件平台上编译运行。

POSIX 与 System V区别

  • System V IPC存在时间比较老,许多系统都支持,但是接口复杂,并且可能各平台上实现略有区别(如ftok的实现及限制)
  • posix信号量只能单个操作,IPC信号量可以多个信号量同时操作,IPC信号量更加强大
  • POSIX是新标准,语法简单,并且各平台上实现都一样

信号量:一个定义在内核系统中的特殊的变量,有以下定义:
1)P操作:减操作
2)V操作:加操作
3)该变量最小为0,如果等于0的情况下还去进行P操作,就会阻塞

The canonical names V and P come from the initials of Dutch words. V is generally explained as verhogen ("increase"). Several explanations have been offered for P, including proberen ("to test" or "to try"), passeren ("pass"), and pakken ("grab"). Dijkstra's earliest paper on the subject givespassering ("passing") as the meaning for P, and vrijgave ("release") as the meaning for V. It also mentions that the terminology is taken from that used in railroad signals. Dijkstra subsequently wrote that he intended P to stand for the portmanteauprolaag, short for probeer te verlagen, literally "try to reduce," or to parallel the terms used in the other case, "try to decrease."

信号量(sem)和互斥锁的区别:

互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区

使用时,需要以下头文件:

c
#include<semaphore.h>

POSIX信号量有两种:有名信号量(可以在进程中使用)和无名信号量。有名信号量与无名信号量使用同一套函数(但是在初始化和销毁的函数有所不同)

我们这里介绍无名信号量。

c
初始化并打开有名信号量:sem_open();       初始化无名信号量:sem_init()

操作信号量:sem_wait()/sem_trywait()/sem_timedwait()/sem_post()/sem_getvalue()

    
关闭有名信号量:sem_close(); //关闭有名信号量
销毁有名信号量:sem_unlink(); //试图销毁信号量,一旦所有占用该信号量的进程都关闭了该信号量,那么就会销毁这个信号量

销毁无名信号量:sem_destroy()

初始化与销毁信号量

c
intsem_init(sem_t *sem, int pshared, unsignedint value);
intsem_destory(sem_t *sem);

描述:初始化/销毁一个信号量对象

参数解析:

sem:信号量对象

pshared:共享方式

  • PTHREAD_PROCESS_PRIVATE:表示这个信号量是当前进程的局部信号量
  • PTHREAD_PROCESS_SHARED:这个信号量可以在多个进程之间共享

value:初始值

返回值:成功符合0,失败返回-1。

信号量操作

P操作

c
intsem_wait(sem_t *sem);
intsem_trywait(sem_t *sem); //试图占用信号量,如果信号量已经为0,立即报错(errno 设置为 EAGAIN))
intsem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

描述:以原子操作的方式将信号量的值减去1。

返回值:成功返回0;失败返回-1,设置errno。

V操作

c
intsem_post(sem_t *sem);

描述::以原子操作的方式将信号量的值加上1。

返回值:成功返回0;失败返回-1,设置errno。

取值

c
intsem_getvalue(sem_t *sem, int *sval);

描述:获得信号量当前的值,放到sval中。

参数解析:

sval:存放结果的容器。

注意:如果有线程正在block这个信号量,sval可能返回两个值其中的一个,0或“-正在block的线程的数目”,在Linux中返回0。

If one or more processes or threads are blocked waiting to lock the semaphore with sem_wait(), POSIX.1 permits two possibilities for the value returned in #sval: either 0 is returned; or a negative number whose absolute value is the count of the number of processes and threads currently blocked in sem_wait(). Linux adopts the former behavior.

例程:信号量

通过信号量模拟2个窗口,10个客人进行服务的过程。

c
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<semaphore.h>


int num=10;
sem_t sem;

void *get_service(void *cid){
    int id=*((int*)cid);
    if(sem_wait(&sem)==0)
    {
        printf("customer %d get the service \n", id);
        sleep(2);
        printf("customer %d done \n", id);
        sem_post(&sem);
    }
    return 0;
}

intmain(){
    sem_init(&sem,0,2);
    pthread_t customer[num];
    int flag;

    for(int i=0;i<num;i++)
    {
        int id=i;
        flag=pthread_create(&customer[i],NULL,get_service,&id);
        if(flag)
        {
            return flag;
        }else
        {
        }
        sleep(1);
    }

    //wait all thread done
    for(int j=0;j<num;j++)
    {
        pthread_join(customer[j],NULL);
    }
    sem_destroy(&sem);
    return 0;
}

线程栅栏

把先后到达的多个线程挡在同一栅栏前,直到所有线程(可指定个数)到齐,然后撤下栅栏同时放行。

使用场景

这种“栅栏”机制最大的特点就是最后一个执行wait的动作最为重要,就像赛跑时的起跑枪一样,它来之前所有人都必须等着。所以实际使用中,pthread_barrier_wait常常用来让所有线程等待“起跑枪”响起后再一起行动。

比如我们可以用pthread_create()生成100个线程,每个子线程在被create出的瞬间就会自顾自的立刻进入回调函数运行。但我们可能不希望它们这样做,因为这时主进程还没准备好,和它们一起配合的其它线程还没准备好,我们希望它们在回调函数中申请完线程空间、初始化后停下来,一起等待主进程释放一个“开始”信号,然后所有线程再开始执行业务逻辑代码。

为了解决上述场景问题,我们可以在init时指定n+1个等待,其中n是线程数。而在每个线程执行函数的首部调用wait()。这样100个pthread_create()结束后所有线程都停下来等待最后一个wait()函数被调用。这个wait()由主进程在它觉得合适的时候调用就好。最后这个wait()就是鸣响的起跑枪。

栅栏的初始化与销毁

c
intpthread_barrier_init(pthread_barrier_t *restrict barrier, constpthread_barrierattr_t *restrict attr, unsigned count);

intpthread_barrier_destroy(pthread_barrier_t *barrier);

描述:初始化/销毁一个栅栏对象,初始化时要指定等待者个数。

参数解析:

count:等待者个数

在栅栏前等待

c
intpthread_barrier_wait(pthread_barrier_t *barrier);
到栅栏前等待放行,如果该条函数执行的次数等于 #count 时,放行。

描述:让一个线程在栅栏前告诉大家它已经就绪,等待放行。

例程:栅栏的使用

c
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>

pthread_barrier_t barrier;

void *Task1(void *arg);
void *Task2(void *arg);

intmain(void){
    int policy,inher;
    pthread_t tid;
    pthread_attr_t attr;
    structsched_paramparam;

    //初始化线程属性
    pthread_attr_init(&attr);
    pthread_barrier_init(&barrier,NULL,2 + 1);//2+1个等待(2个线程,1个自己)

    //创建线程1
    pthread_create(&tid, &attr,Task1,NULL);

    //创建线程2
    pthread_create(&tid, &attr,Task2,NULL);
    
    printf("main process will sleep 6s.\n");
    sleep(6);/*等待6s后,才让线程运行*/
    pthread_barrier_wait(&barrier);//起跑枪“砰!”

    pthread_join(tid, NULL);
    pthread_barrier_destroy(&barrier);
}

void *Task1(void *arg){
    printf("Task1 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有线程都被阻塞在这里
    printf("Task1 is running.\n");
    sleep(3);//延时3s
    pthread_exit(NULL);
}

void *Task2(void *arg){
    printf("Task2 will be blocked.\n");
    pthread_barrier_wait(&barrier);//所有线程都被阻塞在这里
    printf("Task2 is running.\n");
    sleep(3);//延时3s
    pthread_exit(NULL);
}

为同步对象设置属性

https://blog.csdn.net/bytxl/article/details/8822551

在上文的学习中,为了降低学习的理解难度,我们没有介绍互斥锁的属性。现在我们就来介绍有关内容。

线程和线程的同步对象(互斥锁,读写锁,条件变量,栅栏)都具有属性。在修改属性前都需要对该结构进行初始化。使用后要把该结构回收。

除了互斥锁还可以设置锁的类型以外,所有的同步对象只能设置作用域。

互斥锁属性

初始化与销毁

c
#include<pthread.h>

intpthread_mutexattr_destroy(pthread_mutexattr_t *attr);
intpthread_mutexattr_init(pthread_mutexattr_t *attr);

描述:类似于线程属性(但没有静态初始化方法),使用时初始化(设置了默认属性),使用后销毁(设置了无效属性)

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

设置互斥锁的共享方式

c
#include<pthread.h>

intpthread_mutexattr_getpshared(constpthread_mutexattr_t
                                 *restrict attr, int *restrict pshared);
intpthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
                                 int pshared);

描述:设置互斥锁的作用域。

参数解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的互斥锁只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许互斥锁在进程间中使用)

在进程间的用法:设置以后,将一个互斥锁放置到共享内存中即可。

设置互斥锁的类型

c
#include<pthread.h>

intpthread_mutexattr_gettype(constpthread_mutexattr_t *restrict attr,
                              int *restrict type);
intpthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

描述:设置互斥锁的类型。

参数解析:

type: 由于 DEFAULT ( NORMAL) 属性有太多的未定义行为,应该尽可能避免使用。

  • PTHREAD_MUTEX_DEFAULT(默认)
  • PTHREAD_MUTEX_NORMAL
  • PTHREAD_MUTEX_ERRORCHECK
  • PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_NORMAL

这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起这个线程的死锁。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。

PTHREAD_MUTEX_ERRORCHECK

这种类型的互斥锁会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。

PTHREAD_MUTEX_RECURSIVE

如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE)。

条件变量属性

初始化与销毁

c
intpthread_condattr_init(pthread_condattr_t *cattr);
intpthread_condattr_destroy(pthread_condattr_t *cattr);

类似上文,不再描述。

设置共享方式

c
intpthread_condattr_getpshared(constpthread_condattr_t *restrict attr,
                                int *restrict pshared);
intpthread_condattr_setpshared(pthread_condattr_t *attr,
                                int pshared);

参数解析:既然条件变量是搭配互斥锁用的,那么同样地,就可以设置共享方式。

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的条件变量只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许条件变量在进程间中使用)

在进程间的用法:设置以后,将条件变量放置到共享内存中即可。

读写锁属性

初始化与销毁

c
intpthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
intpthread_rwlockattr_init(pthread_rwlockattr_t *attr);

类似上文,不再描述。

设置共享方式

c
intpthread_rwlockattr_getpshared(constpthread_rwlockattr_t
                                  *restrict attr, int *restrict pshared);
intpthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,
                                  int pshared);

参数解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的条件变量只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许条件变量在进程间中使用)

在进程间的用法:设置以后,将读写锁放置到共享内存中即可。

栅栏属性

初始化与销毁

c
intpthread_barrierattr_destroy(pthread_barrierattr_t *attr);
intpthread_barrierattr_init(pthread_barrierattr_t *attr);

类似上文,不再描述。

设置共享方式

c
intpthread_barrierattr_getpshared(constpthread_barrierattr_t
                                   *restrict attr, int *restrict pshared);
intpthread_barrierattr_setpshared(pthread_barrierattr_t *attr,
                                   int pshared);

参数解析:

pshared:

  • PTHREAD_PROCESS_PRIVATE(默认,由这个属性对象创建的条件变量只能在进程内使用)

  • PTHREAD_PROCESS_SHARED(允许条件变量在进程间中使用)

在进程间的用法:设置以后,将栅栏放置到共享内存中即可。

上一篇:进程间通信概述


下一篇:OpenGL ES2.0贴图