Posix多线程编程总结

Posix在线文档:

The Single UNIX Specification, Version 2 (opengroup.org)

本文主要参考这位大神的文章:

Posix多线程编程学习笔记 - 凌峰布衣 - 博客园 (cnblogs.com)

线程安全问题

多线程编程中,经常遇到的就是线程安全问题,或者说要考虑线程同步的问题。

线程同步这个概念其实挺抽象的。关于线程同步,是指各线程执行时需要按照一定的先后顺序,而不是各线程自己想干嘛就干嘛。

本人之前对线程同步,以及主要的同步技术如互斥锁、信号量、条件变量等没怎么搞透彻,这里再想一想看看。

考虑以下三个场景:

  • 场景1:多个线程,改变了同一个全局变量;
  • 场景2:多个线程,都调用printf来打印输出;
  • 场景3:多个线程,其中A线程必须等B线程执行结束才能执行。

针对场景1,可以使用互斥锁,保证同一时间只有一个线程来操作全局变量,本质是什么呢?本质是因为修改变量的过程并不是原子操作,而是会被打断的,所以,需要在修改变量期间,保证不会被其他线程打断;

针对场景2,我们需要的其实也是printf这个过程不会被打断,要不然就是这个线程打印一会儿,那个线程打印一会儿,输出的内容都串在一起了,这种情况下怎么处理呢?在freertos中,不想某段操作被其他任务打断,可以使用临界区函数来实现,那么Linux中呢?其实可以用信号量,想一想,信号量其实主要就是统计某个资源的使用情况,printf其实就是一个输出的资源,我们可以用信号量来实现,每次只有一个线程可以获取该资源;

针对这两个场景再想想,printf是个资源,其实一个全局变量也是个资源,是不是也可以用信号量呢?另外,printf要想不被打断,是不是也可以使用互斥锁呢?理论上都是可以互换的。

二者本质上都是为了实现原子操作。

再来看看第三个场景,各线程有先后执行顺序,就可以使用条件变量,这一点,相对前面两个场景,没那么容易搞混。

而这三个技术,互斥锁、信号量以及条件变量,就是多线程编程中,实现线程同步的主要手段。再看看线程同步这个词,意思其实就是我这个线程操作资源时,你其他线程得等着,这就是同步的含义。使用线程同步技术,从而保证线程安全。

这几个技术的本质应该是类似的,只是侧重点不一样,需要在实际开发中慢慢体会。

互斥锁和信号量的区别?

参考:

信号量和互斥量(锁)的区别_信号量和互斥锁的区别-CSDN博客

为了更好地区分应用场景,再想想。

互斥锁主要是用在某个会被多线程调用的代码段中;

而信号量,是先有个信号量,然后这个线程释放信号量,另一个线程获取信号量,其实也可以控制线程实现的先后顺序。 

根据《UNIX环境高级编程》,Pthread处理线程同步的,主要是互斥锁和条件变量,而信号量在进程间通信才被提及,貌似信号量主要是用于多进程对共享数据对象的访问。 

注意Linux中的相关概念和RTOS中的相关概念的区别。

Linux中会有多个进程,相对复杂,RTOS中一般只有一个进程,也就是我们的应用,然后里面有各个子任务,对应的是Linux中的线程的概念。

有哪些不一样呢?比如RTOS中消息队列就是在各线程任务之间传递消息,而Linux中主要是用在进程间通信;又比如RTOS中的信号量分为好几种,比如二值信号量、计数信号量、互斥信号量等等,都用在任务间的同步,而Linux中,这些技术有的是用于线程的,有的是用于进程的;再比如RTOS中有临界区保护这一技术手段,不过Linux中一般就是通过互斥锁信号量等等实现。

总之要明确的是,RTOS中一般只有线程的概念,也就是各子任务,所以各技术,都是用在任务之间;但是Linux中,既有线程又有进程,不同的技术有不同的应用场景,所以要加以区分。 另外,不同的操作系统,可能略有差别,实际使用时,需要进一步了解,不过用法都大差不差,重要的是,别以为都是统一的用法,然后产生疑惑,还不知道是咋回事。

Pthreads 概述

历史上,硬件销售商实现了私有版本的多线程库。这些实现在本质上各自不同,使得程序员难于开发可移植的应用程序。 为了使用线程所提供的强大优点,需要一个标准的程序接口。对于UNIX系统,IEEE POSIX 1003.1c(1995)标准制订了这一标准接口。依赖于该标准的实现就称为POSIX threads 或者Pthreads。现在多数硬件销售商也提供Pthreads,附加于私有的API。 Pthreads 被定义为一些C语言类型和函数调用,用pthread.h头(包含)文件和线程库实现。

所以说,Pthreads,其实就是POSIX threads,也就是符合posix标准的线程操作接口。

线程管理

Pthreads API中的函数可以非正式的划分为三大类: 

线程管理(Thread management: 第一类函数直接用于线程:创建(creating),分离(detaching),连接(joining)等等。包含了用于设置和查询线程属性(可连接,调度属性等)的函数。 

互斥量(Mutexes: 第二类函数是用于线程同步的,称为互斥量(mutexes),是"mutual exclusion"的缩写。Mutex函数提供了创建,销毁,锁定和解锁互斥量的功能。同时还包括了一些用于设定或修改互斥量属性的函数。 

条件变量(Condition variables):第三类函数处理共享一个互斥量的线程间的通信,基于程序员指定的条件。这类函数包括指定的条件变量的创建,销毁,等待和受信(signal)。设置查询条件变量属性的函数也包含其中。 

创建和结束线程

函数: 

pthread_create (thread,attr,start_routine,arg)  

pthread_exit (status)  

pthread_attr_init (attr)  

pthread_attr_destroy (attr)  

最初,main函数包含了一个缺省的线程,也就是主线程。其它线程则需要程序员显式地创建。 

有个疑惑,主线程mian里面创建完其他线程后,是进入while死循环,还是直接return返回?

参考:

线程创建后,避免主线程先于子线程结束的四种方式_怎么让新创建的线程不随主线程结束运行-CSDN博客

pthread_create 创建一个新线程并使之运行起来。该函数可以在程序的任何地方调用。 

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    void *(*start_routine)(void*), void *arg);

pthread_create参数: 

  • thread:返回一个新线程标识符,也就是线程ID
  • attr:线程属性对象。可以指定一个线程属性对象,或者NULL为缺省值,一般需要时会在属性里设置线程的优先级和栈大小;
  • start_routine:线程将会执行的函数,注意该函数的类型void *(*)(void *)
  • arg: 传递给start_routine单个参数,传递时必须转换成指向void的指针类型。没有参数传递时,可设置为NULL。 

一个进程可以创建的线程最大数量取决于系统实现。 

Q:一个线程被创建后,怎么知道操作系统何时调度该线程使之运行? 

A:线程何时何地被执行取决于操作系统实现,强壮的程序应该不依赖于线程执行的顺序。

线程属性: 

  • 线程被创建时会带有默认的属性。其中的一些属性可以被程序员用线程属性对象来修改;
  • pthread_attr_init 和 pthread_attr_destroy用于初始化/销毁先成属性对象; 

结束终止: 

  • 结束线程的方法有以下几种: 
    • 线程从主线程(main函数的初始线程)返回; 
    • 线程调用了pthread_exit函数; 
    • 其它线程使用 pthread_cancel函数结束线程; 
    • 调用exec或者exit函数,整个进程结束; 

pthread_exit用于显式退出线程。典型地,pthread_exit()函数在线程完成工作不再需要的时候被调用,退出线程。注意,哪个线程调用的,就结束哪个线程。

#include <pthread.h>

void pthread_exit(void *value_ptr);

参数retval 是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。

示例如下:

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

void *thread_fun1(void *param)
{
    while(1)
    {
        printf("I am thread-1\n");
        sleep(1);
    }
    
    return NULL;
}

void *thread_fun2(void *param)
{
    while(1)
    {
        printf("you are thread-2\n");
        sleep(1);
    }
    
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int rc;

    rc = pthread_create(&tid1, NULL, thread_fun1, NULL);
    if (rc < 0){ 
         printf("ERROR; return code from pthread_create() is %d\n", rc); 
         exit(-1); 
    } 

    rc = pthread_create(&tid2, NULL, thread_fun2, NULL);
    if (rc < 0){ 
         printf("ERROR; return code from pthread_create() is %d\n", rc); 
         exit(-1); 
    }

    pthread_exit(NULL);//主线程退出了
}

互斥量

互斥量(Mutex)是“mutual exclusion”的缩写。互斥量是实现线程同步,和保护同时写共享数据的主要方法。

互斥量对共享数据的保护就像一把锁。在Pthreads中,任何时候仅有一个线程可以锁定互斥量,因此,当多个线程尝试去锁定该互斥量时仅有一个会成功。直到锁定互斥量的线程解锁互斥量后,其他线程才可以去锁定互斥量。线程必须轮着访问受保护数据。 

互斥量可以防止“竞争”条件。

一个拥有互斥量的线程经常用于更新全局变量。确保了多个线程更新同样的变量以安全的方式运行,最终的结果和一个线程处理的结果是相同的(在受保护的地方,多个线程对于资源的操作,就像是一个线程中对数据的先后处理)。这个更新的变量属于一个“临界区(critical section)”。 

使用互斥量的典型顺序如下: 

  • 创建和初始一个互斥量 
  • 多个线程尝试去锁定该互斥量 
  • 仅有一个线程可以成功锁定改互斥量 
  • 锁定成功的线程做一些处理 
  • 线程解锁该互斥量 
  • 另外一个线程获得互斥量,重复上述过程 
  • 最后销毁互斥量 

当多个线程竞争同一个互斥量时,失败的线程会阻塞在lock调用处。可以用“trylock”替换“lock”,则失败时不会阻塞。 

当保护共享数据时,程序员有责任去确认是否需要使用互斥量。如,若四个线程会更新同样的数据,但仅有一个线程用了互斥量,则数据可能会损坏。 

为了方便演示,我们暂时补充下另外一个函数pthread_join

上面我们提到了一个问题,那就是线程创建后,如何避免主线程先于子线程结束,并且附了一篇参考文章。

线程创建后,避免主线程先于子线程结束的四种方式_怎么让新创建的线程不随主线程结束运行-CSDN博客

在前一个代码例程中,我们是直接使用pthread_exit(NULL)来退出了主线程。

这里,为了演示线程安全问题,我们需要等待其他线程结束,最后才退出主线程。

使用的函数就是pthread_join

linux中pthread_join()与pthread_detach()详解_pthread detach join-CSDN博客

pthread_join()即是子线程合入主线程,主线程阻塞等待子线程结束,然后回收子线程资源。

​
#include <pthread.h> 
int pthread_join(pthread_t thread, void **value_ptr);

pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

参数 :thread: 线程标识符,即线程ID,标识唯一线程。retval: 用户定义的指针,用来存储被等待线程的返回值,如果不关心的话,设置为NULL即可。

查看如下程序示例:

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

#define D_EXE_COUNT 100000

static int count = 0;

void *thread_fun1(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        count++;
    }
    
    return NULL;
}

void *thread_fun2(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        count++;
    }
    
    return NULL;
}

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

    ret = pthread_create(&tid1, NULL, thread_fun1, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    ret = pthread_create(&tid2, NULL, thread_fun2, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    //waiting for thread end
    ret = pthread_join(tid1, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    ret = pthread_join(tid2, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    //print result
    printf("count = %d\n", count);

    return 0;
}

结果如下

理论上的结果应该是200000,结果明显不对。

这里面其实就存在线程安全的问题。

怎么解决呢?

使用互斥锁即可。

具体内容直接参考《【正点原子】I.MX6U嵌入式Linux C应用编程指南V1.4》互斥锁部分。

本文不再赘述。

也可以直接参考这篇文章:

Posix多线程编程学习笔记 - 凌峰布衣 - 博客园 (cnblogs.com)

总的来说,使用互斥锁只需要以下几个步骤:

1、定义互斥锁对象pthread_mutex_t mutex;;

2、初始化互斥锁对象pthread_mutex_init(&mutex, NULL); ;

3、对共享资源部分进行上锁pthread_mutex_lock(&mutex);和解锁pthread_mutex_unlock(&mutex);

4、可选,最后不用时可以手动销毁pthread_mutex_destroy(&mutex);

示例代码如下:

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

#define D_EXE_COUNT 100000

static pthread_mutex_t mutex;//in fact,mutex is a global var,but handle for thread safe
static int count = 0;

void *thread_fun1(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        pthread_mutex_lock(&mutex);
        count++;
        pthread_mutex_unlock(&mutex);
    }
    
    return NULL;
}

void *thread_fun2(void *param)
{
    for(int i = 0; i < D_EXE_COUNT; i++)
    {
        pthread_mutex_lock(&mutex);
        count++;
        pthread_mutex_unlock(&mutex);
    }
    
    return NULL;
}

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

    //init mutex
    pthread_mutex_init(&mutex, NULL);

    ret = pthread_create(&tid1, NULL, thread_fun1, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    ret = pthread_create(&tid2, NULL, thread_fun2, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    //waiting for thread end
    ret = pthread_join(tid1, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    ret = pthread_join(tid2, NULL);
    if (ret < 0) 
    {
        printf("ERROR; return code from pthread_create() is %d\n", ret);
        exit(-1);
    }

    //print result
    printf("count = %d\n", count);

    return 0;
}

结果如下:

可以看到,结果是对的。

注意,同一个共享资源,只要线程操作了,就得上锁,并且,上的必须是同一把锁。

当保护共享数据时,程序员有责任去确认是否需要使用互斥量。如,若四个线程会更新同样的数据,但仅有一个线程用了互斥量,则数据可能会损坏。

经验证,去掉一个线程中的锁,结果如下:

思考下:

线程访问共享资源的两个方式,一个是直接在各自线程里访问,另一种是各线程都调用同一个函数,这个函数里操作了共享资源,上述的例程属于第二种情况。

二者有区别吗?

这两种场景很类似,也很容易搞混,捋一捋。

多个线程都调用同一个函数,该函数是不可重入的,也就是说,里面有共享资源,此时,可以给共享资源部分上锁,因此,每次就只会有一个线程访问该共享资源。相当于每个人都需要进同一个门里去拿东西,这时候这个门就上把锁。这种情况比较好处理。

但是,另外一种情况下,各个线程直接操作的共享资源,首先,线程A操作共享资源时,是有可能被线程B抢占执行的,这时候就会出现线程安全的问题。那么,这种情况怎么解决呢?是给每个线程里的操作部分都给上个锁?换另一种思路就是,如果没法在被调用函数里面上锁,那么如何在调用处解决这个线程同步的问题?这里我有个想法,是不是,只要是同一把锁,同一时间就只能有一把钥匙?如果多个线程里直接操作了同一个全局变量,那么在各自线程里都给操作部分上锁,因为是同一把锁,所以,同时只有一个线程能够处于执行中,只有当线程释放锁之后,其他线程才能进入,就算是在各自线程里操作。如果是这样的话,那就可以解决这个问题了。

这个问题的关键是:使用的是同一把锁。

自旋锁

内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:

一个是原地等待

一个是挂起当前进程,调度其他进程执行(睡眠)

Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源)。

自旋锁的使用

在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock。

这里为什么把中断上下文标红加粗呢?因为在中断上下文,是不允许睡眠的(原因详见文章《Linux 中断之中断处理浅析》中的第四章),所以,这里需要的是一个不会导致睡眠的锁——spinlock。

换言之,中断上下文要用锁,首选 spinlock。

更多参考:

Linux 内核同步(二):自旋锁(Spinlock)_spin lock-CSDN博客

条件变量

条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。

注意,为什么要配合互斥锁一起使用呢?

因为条件变量只是一种机制,啥意思呢?就是说,我们自己还得定义一个“条件变量”,当满足条件时,然后通知其他线程,其他等待的线程就会被唤醒。

所以说,这个机制本身只是提供通知和阻塞等待的功能,而并不提供“条件”。

感觉这个机制设计得比较奇怪。

这种情况下,如果我们自己就使用一个标志位,行不行呢?也可以,就是使用标志位的地方会一直轮询标志位,故而造成 CPU 资源的浪费,而使用条件变量机制,就可以在等到时阻塞,条件满足时就会被唤醒。类似于“信号”机制。

使用条件变量主要包括两个动作:

一个线程等待某个条件满足而被阻塞;

另一个线程中,条件满足时发出“信号”。

条件变量允许一个线程休眠(阻塞等待)直至获取到另一个线程的通知(收到信号)再去执行自己的操作。

前面说到,条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,这里说的条件其实就是我们自己定义的一个标志性变量。

说实话,条件变量这个名字真的很容易让人产生误解。不知道是不是翻译的有问题。

参考这两篇文章加深理解:

别再稀里糊涂地使用条件变量了_条件变量的用法-CSDN博客

深入解析条件变量(condition variables) - HarlanC - 博客园 (cnblogs.com)

具体内容直接参考《【正点原子】I.MX6U嵌入式Linux C应用编程指南V1.4》条件变量部分。

本文不再赘述。

这里实际做个测试。

现在想要用两个线程打印一句话

static void *thread_fun1(void *param)
{
    while(1)
    {
        printf("this is a ");
        usleep(100000);
    }    

    return NULL;
}

static void *thread_fun2(void *param)
{
    while(1)
    {
        printf("test for condition var\n");
        printf("---------------------------------\n");
        usleep(100000);
    }
   
    return NULL;
}

我希望总是先执行线程1,再执行线程2;

如果不干预,显然是会混乱的。

怎么办呢?

可以先参照裸机开发中用个标志位来搞定。

static void *thread_fun1(void *param)
{
    while(1)
    {
        if(flag == 0)
        {
            printf("this is a ");
            flag = 1;
        }
    }
    
    return NULL;
}

static void *thread_fun2(void *param)
{
    while(1)
    {
        if(flag == 1)
        {
            printf("test for condition var\n");
            printf("---------------------------------\n");
            flag = 0;
        }
    }

    return NULL;
}

这样其实也可以解决。

但是,上面也说了,这样其实每个线程都在不断地执行两个循环判断,永无休止地在执行,浪费cpu资源。

因此,我们用条件变量来优化一下看看。

static void *thread_fun1(void *param)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);

        if(flag == 0)
        {
            printf("this is a ");
            flag = 1;   
            pthread_cond_signal(&cond);//send signal to conditon var
        }

        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

static void *thread_fun2(void *param)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);

        while(flag == 0)
        {
            pthread_cond_wait(&cond, &mutex);//waiting for condition arrive
        }

        if(flag == 1)
        {
            printf("test for condition var-%d\n", count++);
            printf("---------------------------------\n");
            flag = 0;
        }

        pthread_mutex_unlock(&mutex);
    }   

    return NULL;
}

感觉哪里怪怪的,貌似和直接使用标志位区别不大,最大的不同就是,线程2当条件不满足时是阻塞的,而且,这跟我自己在这死等有啥区别?其他没啥变化,反而相比标志位更麻烦。

不知道是不是用法不对。。。。。。。

暂且不管了吧。

在这份示例代码中,我们使用 了 while 循环、而不是 if 语句,来控制对 pthread_cond_wait()的调用,这是为何呢?

必须使用 while 循环,而不是 if 语句,这是一种通用的设计原则:当线程从 pthread_cond_wait()返回时,并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。

更多待补充吧。。。

实际中,当需要满足的条件比较复杂时,可以采用这种条件变量。

信号量

Linux中,信号量是实际开发中,实现线程同步的常用方式。

注:互斥锁处理互斥访问或者原子操作的问题;条件变量和信号量解决的是线程同步的问题。宏观上,应该都可以称为线程同步的问题,因为本质都是让线程有先后顺序。不必过于纠结。

在linux中,线程就相当于一个轻量级的进程,它常常被用来完成某种特定功能的事情。假如一个进程创建了多个线程,这些线程要一起配合完成一件更大的事情,这个时候就需要用到线程同步机制了。在Linux中通常用信号量实现线程间的同步。

信号量分为有名信号量和无名信号量,实际上线程间同步用的是无名信号量,进程间同步才用有名信号量(也可以用无名信号量进行进程间同步)。

使用信号量需要包含头文件semaphore.h,且编译时需要链接库-lrt或者-lpthread。

主要参考:

linux线程同步方式3——信号量(semaphore)_linux c++ 跨进程的信号量semaphore.h-CSDN博客

补充参考:

无名信号量——线程间同步 - Suzkfly - 博客园 (cnblogs.com)

信号量基本操作如下:

  • 信号量初始化
  • 发送信号量
    • 将信号量值加 1
  • 等待信号量
    • 当信号量值为 0 时,程序等待;当信号量值大于 0 时,信号量减 1,程序继续运行。
  • 销毁信号量。

对应的posix的API如下:

信号量初始化sem_init

对由sem指定的信号量进行初始化,设置好它的共享选项,并指定一个整数类型的初始值

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

调用成功时返回0,失败返回-1.

sem:初始化的信号量
pshared:信号量共享的范围。

        0:线程间使用。

        非0:进程间使用。
value:信号量的初始值;

信号量等待sem_wait

该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。

如果信号量的值大于0,将信号量的值减1,立即返回。如果信号量的值为0,则线程阻塞。直到该信号量值为非0值,相当于P操作。

int sem_wait(sem_t *sem);

调用成功时返回0,失败返回-1.

sem:指向的对象是由sem_init调用初始化的信号量

发送(释放)信号量sem_post

以原子操作的方式将信号量的值加1。当信号量的值大于0时,其它正在调用sem_wait等待信号量的线程将被唤醒,选择机制同样是由线程的调度策略决定的。

int sem_post(sem_t *sem);

调用成功时返回0,失败返回-1.

sem:指向的对象是由sem_init调用初始化的信号量

销毁信号量 sem_destroy

用于对用完的信号量的清理

int sem_destroy(sem_t *sem);

sem:是要销毁的信号量

只有用sem_init初始化的信号量才能用sem_destroy销毁。

使用演示

我们还是以上述打印一句完整的话为例来讲解信号量。

需要包含头文件:#include <semaphore.h>

一开始的程序是这样的:

static void *thread_fun1(void *param)
{
    while(1)
    {
        printf("this is a ");
        sem_post(&sem);
    }
    
    return NULL;
}

static void *thread_fun2(void *param)
{
    while(1)
    {
        sem_wait(&sem);
        printf("test for condition var\n");
        printf("---------------------------------\n"); 
    }
    
    return NULL;
}

发现有问题

问题在哪呢?那就是执行线程1的时候,可能程序会执行好多次,然后一直在释放信号量,信号量的值就会很大,当线程2获取到信号量的时候,又因为信号量可能很大,所以会连续打印很多次。因此没办法连成一句完整的话。

我的本意是,当线程1执行一次后,就线程2执行,然后反复循环,每次都是打印一句完整的话。

需要改进,让线程1执行一次后,阻塞一段时间,让线程2有机会被执行。

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

static sem_t sem;

static void *thread_fun1(void *param)
{
    int sval = 0;

    while(1)
    {
        printf("this is a ");
        sem_post(&sem);

        sem_getvalue(&sem, &sval);
        if(sval == 1)
        {
            sleep(1);
        }
    }
    
    return NULL;
}

static void *thread_fun2(void *param)
{
    while(1)
    {
        sem_wait(&sem);
        printf("test for condition var\n");
        printf("---------------------------------\n"); 
    }
    
    return NULL;
}

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

    //init semaphore
    ret = sem_init(&sem, 0, 0);
    if (ret < 0) 
    {
        printf("sem_init failed\n");
        exit(-1);
    }

    ret = pthread_create(&tid1, NULL, thread_fun1, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    ret = pthread_create(&tid2, NULL, thread_fun2, NULL);
    if (ret < 0)
    {
         printf("ERROR; return code from pthread_create() is %d\n", ret);
         exit(-1);
    }

    pthread_exit(NULL);
}

相对条件变量来说,好用很多。

另外,补充下上述涉及到的一个测试信号量函数sem_getvalue

int sem_getvalue(sem_t *sem, int *sval);

获取信号量 sem 的当前值,把该值保存在 sval,若有 1 个或者多个线程正在调用 sem_wait 阻塞在该信号量上,该函数返回阻塞在该信号量上进程或线程个数。

补充

编译时报错:

undefined reference to 'pthread_create'

参考:

Linux下undefined reference to ‘pthread_create’问题解决-CSDN博客

问题原因:
pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。
问题解决:
在编译中要加 -lpthread参数
gcc thread.c -o thread -lpthread
thread.c为你些的源文件,不要忘了加上头文件#include<pthread.h>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值