linux线程同步信号量

2018-10-10 09:37:31 qq_41248872 阅读数 10236

本篇博文转自http://zhangxiaoya.github.io/2015/05/15/multi-thread-of-c-program-language-on-linux/

Linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量、信号量和读写锁。 
下面是思维导图:

简介

进程— 资源分配的最小单位
线程— 程序执行的最小单位

进程是一个程序的一个实例,拥有自己独立的各种段(数据段,代码段等等),每次创建一个进程需要从操作系统分配这些资源给他,消耗一定的时间,在linux下C语言创建一个进程使用fork()函数;
线程是一个轻量级的进程,除了自己少数的资源,不用用其他资源,且一个进程可以创建多个线程,这些线程共享进程的资源,创建线程的时间要比创建进程少很多,(几十分之一),从函数角度是使用pthread_create()创建。
使用线程处理文件I/O或者socket处理都是非常有优势的,将一个大人物分解成若干个小任务,每个线程处理一个任务,线程之间切换不需要花很多时间,而且线程之间数据交换很方便,共享存储区。

C语言中使用多线程的函数

表 1. 线程函数列表

对象 操作 Linux Pthread API Windows SDK 库对应 API
线程 创建 pthread_create CreateThread
退出 pthread_exit ThreadExit
等待 pthread_join WaitForSingleObject
互斥锁 创建 pthread_mutex_init CreateMutex
销毁 pthread_mutex_destroy CloseHandle
加锁 pthread_mutex_lock WaitForSingleObject
解锁 pthread_mutex_unlock ReleaseMutex
条件 创建 pthread_cond_init CreateEvent
销毁 pthread_cond_destroy CloseHandle
触发 pthread_cond_signal SetEvent
广播 pthread_cond_broadcast SetEvent / ResetEvent
等待 pthread_cond_wait / pthread_cond_timedwait SingleObjectAndWait

多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含四点:线程,互斥锁,条件变量、读写锁。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。

创建线程

int pthread_create(pthread_t * tid, const pthread_attr_t * attr, void * ( * func) (void * ), void * arg);
其返回值是一个整数,若创建进程成功返回0,否则,返回其他错误代码,也是正整数。

创建线程需要的参数:

  • 线程变量名:pthread_t *类型,是标示线程的id,一般是无符号整形,这里也可以是引用类型,目的是用于返回创建线程的ID
  • 线程的属性指针:制定线程的属性,比如线程优先*级初始栈大小等,通常情况使用的都是指针。
  • 创建线程的程序代码:一般是函数指针,进程创建后执行该函数指针只想的函数。
  • 程序代码的参数:若线程执行的函数包含由若干个参数,需要将这些参数封装成结构体,并传递给它指针。
    创建线程的函数的形式如下:

结束线程

结束进程的函数定义如下:

void pthread_exit (void *status);
参数是指针类型,用于存储线程结束后返回状态。

线程等待

int pthread_join (pthread_t tid, void ** status);

  • 第一个参数表示要等待的进程的id;
  • 第二参数表示要等待的进程的返回状态,是个二级指针。

线程创建后怎么执行,新线程和老线程谁先执行这些不是程序来决定,而是由操作系统进行调度的,但是在编程的时候我们常常需要多个线程配合工作,比如在结束某个线程之前,需要等待另外一个线程的处理结果(返回状态等信息),这时候就需要使用线程等待函数,这个函数的定义如下:

其他关于进程的函数

  1. 返回当前线程ID

    pthread_t pthread_self (void);
    用于返回当前进程的ID

  2. 制定线程变成分裂状态

    int pthread_detach (pthread_t tid);
    参数是指定线程的ID,指定的ID的线程变成分离状态;若指定线程是分离状态,则 如果线程退出,那么它所有的资源都将释放,如果线程不是分离状态,线程必须保留它的线程ID、退出状态,直到其他线程对他调用的pthread_join()函数

参考实例一

代码如下:

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

void print_message_func(void *ptr);

int main()
{
    int tmp1,tmp2;
    void *retival;
    pthread_t thread1,thread2;
    char *message1 = "thread1";
    char *message2 = "thread2";

    int ret_thread1,ret_thread2;
    ret_thread1 = pthread_create(&thread1,NULL,(void *)&print_message_func,(void *)message1);

    if(ret_thread1 == 0)
        printf("create thread 1 true\n");
    else
        printf("create thread 1 false\n");

    tmp1 = pthread_join(thread1,&retival);
    printf("thread 1 return value (tmp1) is %d\n",tmp1);

    if(tmp1 != 0)
        printf("cannot join with thread 1\n");

    ret_thread2 = pthread_create(&thread2,NULL,(void *)&print_message_func,(void *)message2);

    if(ret_thread2 == 0)
        printf("create thread 2 true\n");
    else
        printf("create thread 2 false\n");

    tmp2 = pthread_join(thread2,&retival);
    printf("thread 2 return value (tmp2) is %d\n",tmp2);
    if(tmp2 != 0)
        printf("cannot join with thread 2\n");
}

void print_message_func(void *ptr)
{
    for(int i=0;i<5;++i)
    {
        printf("%s:%d\n",(char*)ptr,i);
    }
}

这个代码比较简单,就是演示这几个常用函数的使用。

这里是纯C语言程序,在Linux下的编译命令是gcc test.c -o test -lpthread,运行程序是./test,后面的程序同样

多线程的同步与互斥

锁机制

多线程之间可能需要互斥的访问一些全局变量,这就需要互斥的来访问,这些需要共享访问的字段被称作是临界资源,访问临界资源的程序段称作是临界区
实现线程间的互斥与同步机制的是锁机制,下面是常用的锁机制的函数和类。

  1. pthread_mutex_t mutex 锁对象
  2. pthread_mutex_init(&mutex,NULL) 在主线程中初始化锁为解锁状态
  3. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 编译时初始化锁位解锁状态
  4. pthread_mutex_lock(&mutex)(阻塞加锁)访问临界区加锁操作
  5.  pthread_mutex_trylock( &mutex)(非阻塞加锁); pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
  6. pthread_mutex_unlock(&mutex): 访问临界区解锁操作

参考实例二(不加锁访问互斥全局变量)

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

int sharei = 0;
void increase_num(void);

int main()
{
  int ret;
  pthread_t thread1,thread2,thread3;
  ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);

  pthread_join(thread1,NULL);
  pthread_join(thread2,NULL);
  pthread_join(thread3,NULL);

  printf("sharei = %d\n",sharei);

  return 0;
}

void increase_num(void)
{
  long i,tmp;
  for(i =0;i<=10000;++i)
  {
    tmp = sharei;
    tmp = tmp + 1;
    sharei = tmp;
  }
}

编译运行结果,多运行几次,发现结果都不一样。这就是因为对于全局变量,没有添加互斥锁,导致的问题。

参考实例三 (访问全局变量添加互斥锁)

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

int sharei = 0;
void increase_num(void);
// add mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int main()
{
  int ret;
  pthread_t thread1,thread2,thread3;
  ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
  ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);

  pthread_join(thread1,NULL);
  pthread_join(thread2,NULL);
  pthread_join(thread3,NULL);

  printf("sharei = %d\n",sharei);

  return 0;
}

void increase_num(void)
{
  long i,tmp;
  for(i =0;i<=10000;++i)
  {
    // lock
    if(pthread_mutex_lock(&mutex) != 0)
    {
      perror("pthread_mutex_lock");
      exit(EXIT_FAILURE);
    }
    tmp = sharei;
    tmp = tmp + 1;
    sharei = tmp;
    // unlock
    if(pthread_mutex_unlock(&mutex) != 0)
    {
      perror("pthread_mutex_unlock");
      exit(EXIT_FAILURE);
    }
  }
}

添加互斥锁后,就发现,多次运行的结果都是一样的。

  1. 其实这里的加锁不是对共享变量(全局变量)或者共享内存进行保护,这里的加锁实际上是对临界区的控制,所谓的临界区就是访问临界资源的那一段代码,这段代码对临界资源进行多种操作,正确的情况是不允许这段代码执行到一半,处理器使用权就被其他线程抢走,所以这段代码具有原子性,即要么执行,要么不执行,不能执行到一半就被抢走处理权,这样就会造成共享数据被污染。
  2. 还有一点,添加锁来控制临界区是有代价的,这个代价表现出来就是时间的额外开销,内部过程是因为要保护现场,会利用一些资源,也需要处理器处理的时间。

信号量机制

锁机制使用是有限制的,锁只有两种状态,即加锁和解锁,对于互斥的访问一个全局变量,这样的方式还可以对付,但是要是对于其他的临界资源,比如说多台打印机等,这种方式显然不行了。
信号量机制在操作系统里面学习的比较熟悉了,信号量是一个整数计数器,其数值表示空闲临界资源的数量。
当有进程释放资源时,信号量增加,表示可用资源数增加;当有进程申请到资源时,信号量减少,表示可用资源数减少。这个时候可以把锁机制认为是0-1信号量。
关于信号量机制的函数。

int sem_init(sem_t * sem, int pshared, unsigned int value);初始化信号量

- 成功返回0,失败返回-1;
- 参数sem:表示指向信号结构的指针。
- 参数pshared:不是0 的时候该信号量在进程间共享,否则只能在当前进程的所有线程间共享。
- 参数value:信号量的初始值。

int sem_wait(sem_t *sem); 信号量减一操作,有线程申请资源

- 成功返回0,否则返回-1
- 参数sem:指向一个信号量的指针

int sem_post(sem_t *sem);信号量加一操作,有线程释放资源

- 成功返回0,否则返回-1
- 参数sem:指向一个信号量指针

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

- 成功返回0,否则返回-1
- 参数sem:指向一个信号量的指针。

参考实例四(生产者消费者)

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

#define MAXSIZE 10

int stack[MAXSIZE];

int size =0;
sem_t sem;

void privide_data(void)
{
  int i;
  for(i =0;i<MAXSIZE;++i)
  {
    stack[i] = i;
    sem_post(&sem);
  }
}

void handle_data(void)
{
  int i;
  while((i = size ++) <MAXSIZE)
  {
    sem_wait(&sem);
    printf("cross : %d X %d = %d \n",stack[i],stack[i],stack[i] * stack[i]);
    sleep(1);
  }
}

int main()
{
  pthread_t privider,handler;
  sem_init(&sem,0,0);
  pthread_create(&privider,NULL,(void *)&privide_data,NULL);
  pthread_create(&handler,NULL,(void *)&handle_data,NULL);
  pthread_join(privider,NULL);
  pthread_join(handler,NULL);
  sem_destroy(&sem);

  return 0;
}

这段代码是经典的生产者消费者问题,只有当生产者把资源放入存储区,消费者才能取得。

 

 

附:(知识点)

Linux 线程编程中的 5 条经验

尽量设置 recursive 属性以初始化 Linux 的互斥变量

互斥锁是多线程编程中基本的概念,在开发中被广泛使用。其调用次序层次清晰简单:建锁,加锁,解锁,销毁锁。但是需要注意的是,与诸如 Windows 平台的互斥变量不同,在默认情况下,Linux 下的同一线程无法对同一互斥锁进行递归加速,否则将发生死锁。

所谓递归加锁,就是在同一线程中试图对互斥锁进行两次或两次以上的行为。其场景在 Linux 平台上的代码可由清单 1 所示。

清单 1. Linux 重复对互斥锁加锁实例

1

2

3

4

5

6

7

8

9

10

11

12

// 通过默认条件建锁

    pthread_mutex_t *theMutex = new pthread_mutex_t;

    pthread_mutexattr_t attr;

    pthread_mutexattr_init(&attr);

    pthread_mutex_init(theMutex,&attr);

    pthread_mutexattr_destroy(&attr);

 

    // 递归加锁

    pthread_mutex_lock (theMutex);

    pthread_mutex_lock (theMutex);

    pthread_mutex_unlock (theMutex);

    pthread_mutex_unlock (theMutex);

在以上代码场景中,问题将出现在第二次加锁操作。由于在默认情况下,Linux 不允许同一线程递归加锁,因此在第二次加锁操作时线程将出现死锁。

Linux 互斥变量这种奇怪的行为或许对于特定的某些场景会所有用处,但是对于大多数情况下看起来更像是程序的一个 bug 。毕竟,在同一线程中对同一互斥锁进行递归加锁在尤其是二次开发中经常会需要。

这个问题与互斥锁的中的默认 recursive 属性有关。解决问题的方法就是显式地在互斥变量初始化时将设置起 recursive 属性。基于此,以上代码其实稍作修改就可以很好的运行,只需要在初始化锁的时候加设置一个属性。请看清单 2 。

清单 2. 设置互斥锁 recursive 属性实例

1

2

3

4

pthread_mutexattr_init(&attr);

    // 设置 recursive 属性

    pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);

    pthread_mutex_init(theMutex,&attr);

因此,建议尽量设置 recursive 属性以初始化 Linux 的互斥锁,这样既可以解决同一线程递归加锁的问题,又可以避免很多情况下死锁的发生。这样做还有一个额外的好处,就是可以让 Windows 和 Linux 下让锁的表现统一。

注意 Linux 平台上触发条件变量的自动复位问题

条件变量的置位和复位有两种常用模型:第一种模型是当条件变量置位(signaled)以后,如果当前没有线程在等待,其状态会保持为置位(signaled),直到有等待的线程进入被触发,其状态才会变为复位(unsignaled),这种模型的采用以 Windows 平台上的 Auto-set Event 为代表。其状态变化如图 1 所示:

图 1. Windows 的条件变量状态变化流程

Windows 的条件变量状态变化流程

第二种模型则是 Linux 平台的 Pthread 所采用的模型,当条件变量置位(signaled)以后,即使当前没有任何线程在等待,其状态也会恢复为复位(unsignaled)状态。其状态变化如图 2 所示:

图 2. Linux 的条件变量状态变化流程

Linux 的条件变量状态变化流程

具体来说,Linux 平台上 Pthread 下的条件变量状态变化模型是这样工作的:调用 pthread_cond_signal() 释放被条件阻塞的线程时,无论存不存在被阻塞的线程,条件都将被重新复位,下一个被条件阻塞的线程将不受影响。而对于 Windows,当调用 SetEvent 触发 Auto-reset 的 Event 条件时,如果没有被条件阻塞的线程,那么条件将维持在触发状态,直到有新的线程被条件阻塞并被释放为止。

这种差异性对于那些熟悉 Windows 平台上的条件变量状态模型而要开发 Linux 平台上多线程的程序员来说可能会造成意想不到的尴尬结果。试想要实现一个旅客坐出租车的程序:旅客在路边等出租车,调用条件等待。出租车来了,将触发条件,旅客停止等待并上车。一个出租车只能搭载一波乘客,于是我们使用单一触发的条件变量。这个实现逻辑在第一个模型下即使出租车先到,也不会有什么问题,其过程如图 3 所示:

图 3. 采用 Windows 条件变量模型的出租车实例流程

索引使用的容量要求

然而如果按照这个思路来在 Linux 上来实现,代码看起来可能是清单 3 这样。

清单 3. Linux 出租车案例代码实例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

……

 // 提示出租车到达的条件变量

 pthread_cond_t taxiCond;

 

 // 同步锁

 pthread_mutex_t taxiMutex;

 

 // 旅客到达等待出租车

 void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);

    pthread_cond_wait (&taxiCond, &taxtMutex);

    pthread_mutex_unlock (&taxtMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }

 

 // 出租车到达

 void * taxi_arrive(void *name) {

    cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;

    pthread_cond_signal(&taxtCond);

    pthread_exit( (void *)0 );

 }

 

 void main() { 

    // 初始化

    taxtCond= PTHREAD_COND_INITIALIZER;

    taxtMutex= PTHREAD_MUTEX_INITIALIZER;

    pthread_t thread;

    pthread_attr_t threadAttr;

    pthread_attr_init(&threadAttr);

 

    pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” ));

    sleep(1);

    pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” ));

    sleep(1);

    pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” ));

    sleep(1);

 

    return 0;

 }

好的,运行一下,看看结果如清单 4 。

清单 4. 程序结果输出

1

2

3

4

Taxi Jack arrives.

    Traveler Susan needs a taxi now!

    Taxi Mike arrives.

    Traveler Susan now got a taxi.

其过程如图 4 所示:

图 4. 采用 Linux 条件变量模型的出租车实例流程

图 4. 采用Linux条件变量模型的出租车实例流程

通过对比结果,你会发现同样的逻辑,在 Linux 平台上运行的结果却完全是两样。对于在 Windows 平台上的模型一, Jack 开着出租车到了站台,触发条件变量。如果没顾客,条件变量将维持触发状态,也就是说 Jack 停下车在那里等着。直到 Susan 小姐来了站台,执行等待条件来找出租车。 Susan 搭上 Jack 的出租车离开,同时条件变量被自动复位。

但是到了 Linux 平台,问题就来了,Jack 到了站台一看没人,触发的条件变量被直接复位,于是 Jack 排在等待队列里面。来迟一秒的 Susan 小姐到了站台却看不到在那里等待的 Jack,只能等待,直到 Mike 开车赶到,重新触发条件变量,Susan 才上了 Mike 的车。这对于在排队系统前面的 Jack 是不公平的,而问题症结是在于 Linux 平台上条件变量触发的自动复位引起的一个 Bug 。

条件变量在 Linux 平台上的这种模型很难说好坏。但是在实际开发中,我们可以对代码稍加改进就可以避免这种差异的发生。由于这种差异只发生在触发没有被线程等待在条件变量的时刻,因此我们只需要掌握好触发的时机即可。最简单的做法是增加一个计数器记录等待线程的个数,在决定触发条件变量前检查下该变量即可。改进后 Linux 函数如清单 5 所示。

清单 5. Linux 出租车案例代码实例

……

 // 提示出租车到达的条件变量

 pthread_cond_t taxiCond;


 // 同步锁

 pthread_mutex_t taxiMutex;


 // 旅客人数,初始为 0

 int travelerCount=0;


 // 旅客到达等待出租车

 void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);


    // 提示旅客人数增加

    travelerCount++;

    pthread_cond_wait (&taxiCond, &taxiMutex);

    pthread_mutex_unlock (&taxiMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }


 // 出租车到达

 void * taxi_arrive(void *name)

 {

    cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;


 while(true)

 {

        pthread_mutex_lock(&taxiMutex);



        // 当发现已经有旅客在等待时,才触发条件变量

        if(travelerCount>0)

        {

            pthread_cond_signal(&taxtCond);

            pthread_mutex_unlock (&taxiMutex);

            break;

        }

        pthread_mutex_unlock (&taxiMutex);

    }

    pthread_exit( (void *)0 );

 }


注意条件返回时互斥锁的解锁问题因此我们建议在 Linux 平台上要出发条件变量之前要检查是否有等待的线程,只有当有线程在等待时才对条件变量进行触发。

在 Linux 调用 pthread_cond_wait 进行条件变量等待操作时,我们增加一个互斥变量参数是必要的,这是为了避免线程间的竞争和饥饿情况。但是当条件等待返回时候,需要注意的是一定不要遗漏对互斥变量进行解锁。

Linux 平台上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函数返回时,互斥锁 mutex 将处于锁定状态。因此之后如果需要对临界区数据进行重新访问,则没有必要对 mutex 就行重新加锁。但是,随之而来的问题是,每次条件等待以后需要加入一步手动的解锁操作。正如前文中乘客等待出租车的 Linux 代码如清单 6 所示:

清单 6. 条件变量返回后的解锁实例

1

2

3

4

5

6

7

8

void * traveler_arrive(void * name) {

    cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;

    pthread_mutex_lock(&taxiMutex);

    pthread_cond_wait (&taxiCond, &taxtMutex);

    pthread_mutex_unlock (&taxtMutex);

    cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;

    pthread_exit( (void *)0 );

 }

等待的绝对时间问题

超时是多线程编程中一个常见的概念。例如,当你在 Linux 平台下使用 pthread_cond_timedwait() 时就需要指定超时这个参数,以便这个 API 的调用者最多只被阻塞指定的时间间隔。pthread_cond_timedwait() 函数定义

1

2

3

int pthread_cond_timedwait(pthread_cond_t *restrict cond,

              pthread_mutex_t *restrict mutex,

              const struct timespec *restrict abstime);

参数 abstime 在这里用来表示和超时时间相关的一个参数,但是需要注意的是它所表示的是一个绝对时间,而不是一个时间间隔数值,只有当系统的当前时间达到或者超过 abstime 所表示的时间时,才会触发超时事件。

 相对时间到绝对时间转换实例

1

2

3

4

5

6

7

/* get the current time */

    struct timeval now;

    gettimeofday(&now, NULL);

     

    /* add the offset to get timeout value */

    abstime ->tv_nsec = now.tv_usec * 1000 + (dwMilliseconds % 1000) * 1000000;

    abstime ->tv_sec = now.tv_sec + dwMilliseconds / 1000;

Linux 的绝对时间看似简单明了,却是开发中一个非常隐晦的陷阱。而且一旦你忘了时间转换,可以想象,等待你的错误将是多么的令人头疼:如果忘了把相对时间转换成绝对时间,相当于你告诉系统你所等待的超时时间是过去式的 1970 年 1 月 1 号某个时间段,于是操作系统毫不犹豫马上送给你一个 timeout 的返回值,然后你会举着拳头抱怨为什么另外一个同步线程耗时居然如此之久,并一头扎进寻找耗时原因的深渊里。

正确处理 Linux 平台下的线程结束问题

在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。 Pthread_join() 函数的定义:

1

int pthread_join(pthread_t th, void **thread_return);

调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。

如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为 detached 状态可以通过两种方式来实现。一种是调用 pthread_detach() 函数,可以将线程 th 设置为 detached 状态。

pthread_detach 函数定义:

1

int pthread_detach(pthread_t th);

另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如下。

创建 detach 线程代码实例:

1

    pthread_t tid;

    pthread_attr_t  attr;

    pthread_attr_init(&attr);

    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。

总结与补充

本文以上部分详细介绍了 Linux 的多线程编程的 5 条高效开发经验。另外你也可以考虑尝试其他一些开源类库来进行线程开发。

1. Boost 库

Boost 库来自于由 C++ 标准委员会类库工作组成员发起,致力于为 C++ 开发新的类库的 Boost 组织。虽然该库本身并不是针对多线程而产生,但是发展至今,其已提供了比较全面的多线程编程的 API 支持。 Boost 库对于多线程支持的 API 风格上更类似于 Linux 的 Pthread 库,差别在于其将线程,互斥锁,条件等线程开发概念都封装成了 C++ 类,以方便开发调用。 Boost 库目前对跨平台支持的很不错,不仅支持 Windows 和 Linux ,还支持各种商用的 Unix 版本。如果开发者想使用高稳定性的统一线程编程接口减轻跨平台开发的难度, Boost 库将是首选。

2. ACE

ACE 全称是 ADAPTIVE Communication Environment,它是一个免费的,开源的,面向对象的工具框架,用以开发并发访问的软件。由于 ACE 最初是面向网络服务端的编程开发,因此对于线程开发的工具库它也能提供很全面的支持。其支持的平台也很全面,包括 Windows,Linux 和各种版本 Unix 。 ACE 的唯一问题是如果仅仅是用于线程编程,其似乎显得有些过于重量级。而且其较复杂的配置也让其部署对初学者而言并非易事。

2018-09-30 14:06:36 TSZ0000 阅读数 140

Linux-线程同步信号量

    信号量用于多线程多任务同步,一个线程完成了某一个动作后就通过信号量告诉其他线程再进行某些动作,其他线程在等待某一个线程时会阻塞。

    init
        sem_t sem; 
        sem_init(&sem,0,0); 函数原型如下:
        int sem_init(sem_t *sem, int pshared, unsigned int value);
            pshared:0:shared between the threads of process.
                           !0:shared between processes.
            value: initial value for the semaphore.
    wait
        sem_wait(&sem);    
        P操作,if(value>0);value--;执行代码。if(value<=0);阻塞并等待post把value+1。
    post
        sem_post(&sem);
        V操作,value++;通知wait进行value的原子操作,V操作不阻塞继续执行。
    trywait
        sem_trywait(&sem);
        非阻塞版,如果信号量计数大于0,则信号量立即减1并返回0,否则立即返回-1。
    getvalue
        sem_getvalue(&sem, &sval)
        读取sem中信号量计数,存于sval中,并返回0。

/*gcc sem.c -lpthread*/
//用户从终端输入任意字符然后统计个数显示,输入end则结束
//使用多线程实现:主线程获取用户输入并判断是否退出,子线程计数
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>

char buf[100]={0}; 
int flag = 0;
sem_t sem;

// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void*arg)
{
	// 子线程首先应该有个循环
	// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符
	// 长度,然后打印;完成后再次被阻塞
	sem_wait(&sem);

	while(flag==0)
	{
		printf("长度为:%ld.\n",strlen(buf));
		memset(buf, 0, sizeof(buf));
		sem_wait(&sem);
	}
    pthread_exit(NULL);
}


int main(void)
{
	int ret=-1;
	pthread_t th;

	sem_init(&sem,0,0);

	ret=pthread_create(&th,NULL,func,NULL);
	if (ret != 0)
	{
		printf("pthread_create error.\n");
		return -1;
	}                    

	printf("输入一个字符串,以回车结束.\n");
	while(scanf("%s",buf))
	{
		// 去比较用户输入的是不是end,如果是则退出,如果不是则继续 
		if(!strncmp(buf,"end",3))
		{
			printf("输入的字符串为:%s\n",buf);
			flag=1;
			sem_post(&sem);
			break;
		}
		// 主线程在收到用户收入的字符串,并且确认不是end后
		// 就去发信号激活子线程来计数。
		// 子线程被阻塞,主线程可以激活,这就是线程的同步问题。
		// 信号量就可以用来实现这个线程同步 
		sem_post(&sem);
     }

	// 回收子线程
	printf("等待回收子线程\n");
	ret = pthread_join(th, NULL);
	if (ret != 0)
	{
		printf("pthread_join error.\n");
		exit(-1);
	}
	printf("子线程回收成功\n");

	sem_destroy(&sem);

	return 0;
 }


//intsem_wait(sem_t *sem); 等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回。
//如果信号量的值为0,则线程阻塞。相当于P操作。成功返回0,失败返回-1。
//intsem_post(sem_t *sem); 释放信号量,让信号量的值加1。相当于V操作。

 

2015-08-03 19:28:47 Guyuebingchuan 阅读数 306
信号量、同步这些名词在进程间通信时就已经说过,在这里它们的意思是相同的,只不过是同步的对象不同而已。但是下面介绍的信号量的接口是用于线程的信号量,注意不要跟用于进程间通信的信号量混淆,关于用于进程间通信的信号量的详细介绍可以参阅我的另一篇博文:Linux进程间通信——使用信号量。相似地,线程同步是控制线程执行和访问临界区域的方法

一、什么是信号量
线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。

而只有0和1两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。

二、信号量的接口和使用

信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。

1、sem_init函数
该函数用于创建信号量,其原型如下:
  1. int sem_init(sem_t *sem, int pshared, unsigned int value);  
该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.

2、sem_wait函数
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:
  1. int sem_wait(sem_t *sem);  
sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

3、sem_post函数
该函数用于以原子操作的方式将信号量的值加1。它的原型如下:
  1. int sem_post(sem_t *sem);  
与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

4、sem_destroy函数
该函数用于对用完的信号量的清理。它的原型如下:
  1. int sem_destroy(sem_t *sem);  
成功时返回0,失败时返回-1.

三、使用信号量同步线程

下面以一个简单的多线程程序来说明如何使用信号量进行线程同步。在主线程中,我们创建子线程,并把数组msg作为参数传递给子线程,然后主线程等待直到有文本输入,然后调用sem_post来增加信号量的值,这样就会立刻使子线程从sem_wait的等待中返回并开始执行。线程函数在把字符串的小写字母变成大写并统计输入的字符数量之后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值。

  1. #include <unistd.h>  
  2. #include <pthread.h>  
  3. #include <semaphore.h>  
  4. #include <stdlib.h>  
  5. #include <stdio.h>  
  6. #include <string.h>  
  7.   
  8. //线程函数  
  9. void *thread_func(void *msg);  
  10. sem_t sem;//信号量  
  11.   
  12. #define MSG_SIZE 512  
  13.   
  14. int main()  
  15. {  
  16.     int res = -1;  
  17.     pthread_t thread;  
  18.     void *thread_result = NULL;  
  19.     char msg[MSG_SIZE];  
  20.     //初始化信号量,其初值为0  
  21.     res = sem_init(&sem, 0, 0);  
  22.     if(res == -1)  
  23.     {  
  24.         perror("semaphore intitialization failed\n");  
  25.         exit(EXIT_FAILURE);  
  26.     }  
  27.     //创建线程,并把msg作为线程函数的参数  
  28.     res = pthread_create(&thread, NULL, thread_func, msg);  
  29.     if(res != 0)  
  30.     {  
  31.         perror("pthread_create failed\n");  
  32.         exit(EXIT_FAILURE);  
  33.     }  
  34.     //输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”  
  35.     printf("Input some text. Enter 'end'to finish...\n");  
  36.     while(strcmp("end\n", msg) != 0)  
  37.     {  
  38.         fgets(msg, MSG_SIZE, stdin);  
  39.         //把信号量加1  
  40.         sem_post(&sem);  
  41.     }  
  42.   
  43.     printf("Waiting for thread to finish...\n");  
  44.     //等待子线程结束  
  45.     res = pthread_join(thread, &thread_result);  
  46.     if(res != 0)  
  47.     {  
  48.         perror("pthread_join failed\n");  
  49.         exit(EXIT_FAILURE);  
  50.     }  
  51.     printf("Thread joined\n");  
  52.     //清理信号量  
  53.     sem_destroy(&sem);  
  54.     exit(EXIT_SUCCESS);  
  55. }  
  56.   
  57. void* thread_func(void *msg)  
  58. {  
  59.     //把信号量减1  
  60.     sem_wait(&sem);  
  61.     char *ptr = msg;  
  62.     while(strcmp("end\n", msg) != 0)  
  63.     {  
  64.         int i = 0;  
  65.         //把小写字母变成大写  
  66.         for(; ptr[i] != '\0'; ++i)  
  67.         {  
  68.             if(ptr[i] >= 'a' && ptr[i] <= 'z')  
  69.             {  
  70.                 ptr[i] -= 'a' - 'A';  
  71.             }  
  72.         }  
  73.         printf("You input %d characters\n", i-1);  
  74.         printf("To Uppercase: %s\n", ptr);  
  75.         //把信号量减1  
  76.         sem_wait(&sem);  
  77.     }  
  78.     //退出线程  
  79.     pthread_exit(NULL);  
  80. }  
运行结果如下:



从运行的结果来看,这个程序的确是同时在运行两个线程,一个控制输入,另一个控制处理统计和输出。

四、分析此信号量同步程序的缺陷
但是这个程序有一点点的小问题,就是这个程序依赖接收文本输入的时间足够长,这样子线程才有足够的时间在主线程还未准备好给它更多的单词去处理和统计之前处理和统计出工作区中字符的个数。所以当我们连续快速地给它两组不同的单词去统计时,子线程就没有足够的时间支执行,但是信号量已被增加不止一次,所以字符统计线程(子线程)就会反复处理和统计字符数目,并减少信号量的值,直到它再次变成0为止。

为了更加清楚地说明上面所说的情况,修改主线程的while循环中的代码,如下:
  1. printf("Input some text. Enter 'end'to finish...\n");  
  2. while(strcmp("end\n", msg) != 0)  
  3. {  
  4.     if(strncmp("TEST", msg, 4) == 0)  
  5.     {  
  6.         strcpy(msg, "copy_data\n");  
  7.         sem_post(&sem);  
  8.     }  
  9.     fgets(msg, MSG_SIZE, stdin);  
  10.     //把信号量加1  
  11.     sem_post(&sem);  
  12. }  
重新编译程序,此时运行结果如下:



当我们输入TEST时,主线程向子线程提供了两个输入,一个是来自键盘的输入,一个来自主线程复数据到msg中,然后从运行结果可以看出,运行出现了异常,没有处理和统计从键盘输入TEST的字符串而却对复制的数据作了两次处理。原因如上面所述。

五、解决此缺陷的方法

解决方法有两个,一个就是再增加一个信号量,让主线程等到子线程处理统计完成之后再继续执行;另一个方法就是使用互斥量。

下面给出用增加一个信号量的方法来解决该问题的代码,源文件名为semthread2.c,源代码如下:
  1. #include <unistd.h>  
  2. #include <pthread.h>  
  3. #include <semaphore.h>  
  4. #include <stdlib.h>  
  5. #include <stdio.h>  
  6. #include <string.h>  
  7.   
  8.   
  9. //线程函数  
  10. void *thread_func(void *msg);  
  11. sem_t sem;//信号量  
  12. sem_t sem_add;//增加的信号量  
  13.   
  14.   
  15. #define MSG_SIZE 512  
  16.   
  17.   
  18. int main()  
  19. {  
  20.     int res = -1;  
  21.     pthread_t thread;  
  22.     void *thread_result = NULL;  
  23.     char msg[MSG_SIZE];  
  24.     //初始化信号量,初始值为0  
  25.     res = sem_init(&sem, 0, 0);  
  26.     if(res == -1)  
  27.     {  
  28.         perror("semaphore intitialization failed\n");  
  29.         exit(EXIT_FAILURE);  
  30.     }  
  31.     //初始化信号量,初始值为1  
  32.     res = sem_init(&sem_add, 0, 1);  
  33.     if(res == -1)  
  34.     {  
  35.         perror("semaphore intitialization failed\n");  
  36.         exit(EXIT_FAILURE);  
  37.     }  
  38.     //创建线程,并把msg作为线程函数的参数  
  39.     res = pthread_create(&thread, NULL, thread_func, msg);  
  40.     if(res != 0)  
  41.     {  
  42.         perror("pthread_create failed\n");  
  43.         exit(EXIT_FAILURE);  
  44.     }  
  45.     //输入信息,以输入end结束,由于fgets会把回车(\n)也读入,所以判断时就变成了“end\n”  
  46.     printf("Input some text. Enter 'end'to finish...\n");  
  47.       
  48.     sem_wait(&sem_add);  
  49.     while(strcmp("end\n", msg) != 0)  
  50.     {  
  51.         if(strncmp("TEST", msg, 4) == 0)  
  52.         {  
  53.             strcpy(msg, "copy_data\n");  
  54.             sem_post(&sem);  
  55.             //把sem_add的值减1,即等待子线程处理完成  
  56.             sem_wait(&sem_add);  
  57.         }  
  58.         fgets(msg, MSG_SIZE, stdin);  
  59.         //把信号量加1  
  60.         sem_post(&sem);  
  61.         //把sem_add的值减1,即等待子线程处理完成  
  62.         sem_wait(&sem_add);  
  63.     }  
  64.   
  65.   
  66.     printf("Waiting for thread to finish...\n");  
  67.     //等待子线程结束  
  68.     res = pthread_join(thread, &thread_result);  
  69.     if(res != 0)  
  70.     {  
  71.         perror("pthread_join failed\n");  
  72.         exit(EXIT_FAILURE);  
  73.     }  
  74.     printf("Thread joined\n");  
  75.     //清理信号量  
  76.     sem_destroy(&sem);  
  77.     sem_destroy(&sem_add);  
  78.     exit(EXIT_SUCCESS);  
  79. }  
  80.   
  81.   
  82. void* thread_func(void *msg)  
  83. {  
  84.     char *ptr = msg;  
  85.     //把信号量减1  
  86.     sem_wait(&sem);  
  87.     while(strcmp("end\n", msg) != 0)  
  88.     {  
  89.         int i = 0;  
  90.         //把小写字母变成大写  
  91.         for(; ptr[i] != '\0'; ++i)  
  92.         {  
  93.             if(ptr[i] >= 'a' && ptr[i] <= 'z')  
  94.             {  
  95.                 ptr[i] -= 'a' - 'A';  
  96.             }  
  97.         }  
  98.         printf("You input %d characters\n", i-1);  
  99.         printf("To Uppercase: %s\n", ptr);  
  100.         //把信号量加1,表明子线程处理完成  
  101.         sem_post(&sem_add);  
  102.         //把信号量减1  
  103.         sem_wait(&sem);  
  104.     }  
  105.     sem_post(&sem_add);  
  106.     //退出线程  
  107.     pthread_exit(NULL);  
  108. }  
其运行结果如下:


分析:这里我们多使用了一个信号量sem_add,并把它的初值赋为1,在主线程在使用sem_wait来等待子线程处理完全,由于它的初值为1,所以主线程第一次调用sem_wait总是立即返回,而第二次调用则需要等待子线程处理完成之后。而在子线程中,若处理完成就会马上使用sem_post来增加信号量的值,使主线程中的sem_wait马上返回并执行紧接下面的代码。从运行结果来看,运行终于正常了。注意,在线程函数中,信号量sem和sem_add使用sem_wait和sem_post函数的次序,它们的次序不能错乱,否则在输入end时,可能运行不正常,子线程不能正常退出,从而导致程序不能退出。

至于使用互斥量的方法,将会在下篇文章:Linux多线程——使用互斥量同步线程中详细介绍。
2016-02-16 17:43:30 u013378057 阅读数 212

这里的信号量指的是POSIX  的未命名信号量

POSIX的信号量还可用于进程间的同步  这里只说线程的同步

信号量的工作方式

1 测试信号量是否为0

2是 休眠 直到 信号量大于0时才可能被唤醒

3否  信号量减一


测试与减1是一个原子操作  否则会出现减2的事情



int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化一个信号量  如果用于线程之间pshared  == 0  value指定了信号量的初值其值为 0 - SEM_VALUE_MAX

int sem_wait(sem_t *sem);//如果信号量计数为零则阻塞等待唤醒   否则信号计数减一    

int sem_trywait(sem_t *sem);// 如果信号量为0 不会阻塞  返回-1 errno被设置

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//超时的

int sem_post(sem_t *sem);//使信号量+1    如果有wait阻塞  则唤醒+1的信号量在次减一

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


sem_t  //信号量中需要的数据结构

如果信号量被初始化为1 那么就跟互斥量一样了



2017-10-12 11:42:04 yishizuofei 阅读数 2660

信号量的概念

1、信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此。这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。但如果是普通变量,来自同一程序中不同线程的冲突操作所导致的结果将是不确定的。
2、最简单的信号量是二进制信号量,它只有0和1两种取值。还有更通用的信号量——计数信号量,它可以有更大的取值范围。信号量一般常用来保护一段代码,使其每次只能被一个执行线程运行,要完成这个工作,就要使用二进制信号量。有时,我们希望可以允许有限数目的线程执行一段指定的代码,这就需要计数信号量。

信号量的使用

信号量函数的名字都以sem_开头,信号量类型是sem_t,线程中使用的基本信号量函数有4个。分别是:
1、信号量创建函数

#include <semaphore.h>
int sem_init(sem_t *sem,int pshared,usigned int value);

这个函数初始化sem指向的信号量对象,设置它的共享选项,并给它一个初始化的整数值。pshared参数控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享。(Linux还不支持这种共享,给pshared参数传递一个非零值将导致调用失败???)
2、信号量控制函数

#include <semaphore.h>
int sem_wait(sem_t *sem);//相当于p操作
int sem_trywait(sem_t *sem);//sem_wait的非阻塞版本
int sem_post(sem_t *sem);//相当于v操作

(1)sem_post函数的作用是以原子操作方式给信号量加1.所谓原子操作是指,如果两个线程企图同时给一个信号量加1,它们之间不会互相干扰,而不像如果两个程序同时对同一个文件进行读取、增加、写入操作时可能会引起冲突。信号量的值总是会被正确加2,因为有两个线程试图改变它。
(2)sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。因此,如果对值为2的信号量调用sem_wait,线程将继续进行,但信号量的值会减到1.如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其他线程增加了该信号量的值使其不再是0为止。如果两个线程同时在sem_wait调用上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待。
(3)sem_trywait函数是sem_wait的非阻塞版本,sem_trywait()函数仅在信号量当前没有锁定的情况下(也就是说,如果信号量值是正的。),锁定由sem所引用的信号量;否则,它就不能锁定信号量。
如果调用进程成功地执行了由sem指定的信号量锁操作,那么sem_trywait()和sem_wait()函数将返回零。如果调用不成功,信号量的状态将保持不变,函数将返回-1的值,并设置errno来指示错误。
3、信号量清理函数

#include <semaphore.h>
int sem_destroy(sem_t *sem);

与前几个函数一样,这个函数也以一个信号量指针为参数,并清理该信号量拥有的所有资源。如果企图清理的信号量正在被一些线程等待,就会收到一个错误。函数在成功时会返回0。

信号量的举例

例1:主线程负责接收用户输入,函数线程统计用户输入的字符个数。(用信号量实现)

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

char buf[128] = {0};
sem_t sem;
//函数线程
void * fun_pthread1(void *arg)
{
    while(1)
    {
        sem_wait(&sem);
        if(strncmp(buf,"end",3) == 0)
        {
            break;
        }
        int count = 0;
        while(1)
        {
            if(buf[count] == 0 || buf[count] == '\n')
            {
                break;
            }
            count++;
        }
        printf("count:%d\n",count);
        sem_post(&sem);
        sleep(1);
    }

}
//主线程
void main()
{
    sem_init(&sem,5,1);
    sem_wait(&sem);
    pthread_t id;
    pthread_create(&id,NULL,fun_pthread1,NULL);
    while(1)
    {
        printf("请输入\n");
        fgets(buf,128,stdin);
        sem_post(&sem);
        if(strncmp(buf,"end",3)==0)
        {
            break;
        }
        sleep(1);
        sem_wait(&sem);
    }
    pthread_join(id,NULL);
    sem_destroy(&sem);
    pthread_exit(NULL);
}

运行结果:
sem1.c