3.2、线程之间的同步与互斥

  线程间的同步指的是多个任务(线程)按照约定的顺序相互配合完成一件事情。

  由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须考虑到线程间资源访问的同步与互斥问题。这里主要介绍POSIX中两种线程同步机制,分别为互斥锁和信号量。这两个同步机制可以互相通过调用对方来实现,但互斥锁更适合用于同时可用的资源是惟一的情况;信号量更适合用于同时可用的资源为多个的情况。

1.互斥锁线程控制

  1.1、函数说明

  互斥锁是用一种简单的加锁方法来控制对共享资源的原子操作。这个互斥锁只有两种状态:上锁和解锁。可以把互斥锁看作某种意义上的全局变量。在同一时刻只能有一个线程持有某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经被上锁的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。可以说,这把互斥锁保证让每个线程对共享资源按顺序进行原子操作。

  互斥锁是用来解决线程的执行的完整性,不能解决线程的执行顺序。互斥锁主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,任何时刻最多只能有一个线程能访问该资源。线程必须先获得互斥锁才能访问临界资源,访问资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。

  互斥锁机制主要包括下面的基本函数。

  (1)互斥锁初始化:pthread_mutex_init()

  (2)互斥锁上锁:pthread_mutex_lock()

  (3)互斥锁判断上锁:pthread_mutex_trylock()

  (4)互斥锁接锁:pthread_mutex_unlock()

  (5)消除互斥锁:pthread_mutex_destroy()

  其中,互斥锁可以分为快速互斥锁、递归互斥锁和检错互斥锁。这3种锁的区别主要在于其他未占有互斥锁的线程在希望得到互斥锁时是否需要阻塞等待。快速锁是指调用线程会阻塞直至拥有互斥锁的线程解锁为止。递归互斥锁能够成功地返回,并且增加调用线程在互斥上加锁的次数,而检错互斥锁则为快速互斥锁的非阻塞版本,它会立即返回并返回一个错误信息。默认属性为快速互斥锁。

头文件

#include <pthread.h>

函数原型

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

作用

互斥锁初始化

参数

mutex

要初始化的互斥锁

Mutexattr:默认属性为快速互斥锁

PTHREAD_MUTEX_INITIALIZER:创建快速互斥锁

PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP:创建递归互斥锁

PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP:创建检错互斥锁

返回值

成功

0

失败

返回错误码

 

头文件

#include <pthread.h>

函数原型

int pthread_mutex_lock(pthread_mutex_t *mutex,);   //加锁,若不成功则阻塞

int pthread_mutex_trylock(pthread_mutex_t *mutex,); //加锁,若不成功则返回

int pthread_mutex_unlock(pthread_mutex_t *mutex,); //解锁

int pthread_mutex_destroy(pthread_mutex_t *mutex,); //删除互斥锁

作用

互斥锁上锁、解锁、删除互斥锁

参数

mutex

互斥锁

返回值

成功

0

失败

返回错误码

  1.2、使用实例

下面的实例是在上节示例代码的基础上增加互斥锁功能,实现原本独立与无序的多个线程能够按顺序执行。
/*thread_mutex.c*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_NUMBER 3 /* 线程数 */
#define REPEAT_NUMBER 3 /* 每个线程的小任务数 */
#define DELAY_TIME_LEVELS 10.0 /*小任务之间的最大时间间隔*/

pthread_mutex_t mutex;

void *thrd_func(void *arg)
{
  int thrd_num = (int)arg;
  int delay_time = 0, count = 0;

  int res;

  /* 互斥锁上锁 */
  res = pthread_mutex_lock(&mutex);
  if (res)
  {
    printf("Thread %d lock failedn", thrd_num);
    pthread_exit(NULL);
  }

  printf("Thread %d is startingn", thrd_num);

  for (count = 0; count < REPEAT_NUMBER; count++)
  {
    delay_time = (int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX)) + 1;
    sleep(delay_time);
    printf("tThread %d: job %d delay = %dn",
    thrd_num, count, delay_time);
  }
  printf("Thread %d finishedn", thrd_num);
  pthread_exit(NULL);
}

int main(void)
{
  pthread_t thread[THREAD_NUMBER];
  int no = 0, res;
  void * thrd_ret;
  srand(time(NULL));
  /* 互斥锁初始化 */
  pthread_mutex_init(&mutex, NULL);
  for (no = 0; no < THREAD_NUMBER; no++)
  {
    res = pthread_create(&thread[no], NULL, thrd_func, (void*)no);
    if (res != 0)
    {
      printf("Create thread %d failedn", no);
      exit(res);
    }
  }

  printf("Create treads successn Waiting for threads to finish...n");
  for (no = 0; no < THREAD_NUMBER; no++)
  {
    res = pthread_join(thread[no], &thrd_ret);
    if (!res)
    {
      printf("Thread %d joinedn", no);
    }
    else
    {
      printf("Thread %d join failedn", no);
    }
    /* 互斥锁解锁 */
    pthread_mutex_unlock(&mutex);
  }
  pthread_mutex_destroy(&mutex);
  return 0;
}
该实例的运行结果如下所示。这里3个线程之间的运行顺序跟创建线程的顺序相同。
$ ./thread_mutex
Create treads success
Waiting for threads to finish...
Thread 0 is starting
Thread 0: job 0 delay = 7
Thread 0: job 1 delay = 7
Thread 0: job 2 delay = 6
Thread 0 finished
Thread 0 joined
Thread 1 is starting
Thread 1: job 0 delay = 3
Thread 1: job 1 delay = 5
Thread 1: job 2 delay = 10
Thread 1 finished
Thread 1 joined
Thread 2 is starting
Thread 2: job 0 delay = 6
Thread 2: job 1 delay = 10
Thread 2: job 2 delay = 8
Thread 2 finished
Thread 2 joined

2.信号量线程控制

  2.1、信号量说明

  前面已经讲到,信号量也就是操作系统中所用到的PV原子操作,它广泛用于进程或线程间的同步与互斥。信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。

  这里先来简单复习一下PV原子操作的工作原理: 

  PV原子操作是对整数计数器信号量sem的操作。一次P操作使sem减一,而一次V操作使sem加一。进程(或线程)根据信号量的值来判断是否对公共资源具有访问权限。当信号量sem的值大于等于零时,该进程(或线程)具有公共资源的访问权限;相反,当信号量sem的值小于零时,该进程(或线程)就将阻塞直到信号量sem的值大于等于0为止。

   PV原子操作主要用于进程或线程间的同步和互斥这两种典型情况。若用于互斥,几个进程(或线程)往往只设置一个信号量sem,它们的操作流程如下图所示:

  当信号量用于同步操作时,往往会设置多个信号量,并安排不同的初始值来实现它们之间的顺序执行,它们的操作流程如下图所示:

  2.2、函数说明

  POSIX中定义了两类信号量:无名信号量(基于内存的信号量)和有名信号量

  Linux实现了POSIX的无名信号量,主要用于线程间的互斥与同步。这里主要介绍几个常见函数。

  (1)sem_init()用于创建一个信号量,并初始化它的值。

  (2)sem_wait()和sem_trywait()都相当于P操作,在信号量大于零时它们都能将信号量的值减一,两者的区别在于,若信号量小于零时,sem_wait()将会阻塞进程,而sem_trywait()则会立即返回。

  (3)sem_post()相当于V操作,它将信号量的值加,1,同时发出信号来唤醒等待的进程。

  (4)sem_getvalue()用于获取信号量的值。

  (5)sem_destroy()用于删除信号量。

  2.3、函数格式

头文件

#include <semaphore.h>

函数原型

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

作用

初始化信号量

参数

sem

信号量指针

pshared

决定信号量能否在几个进程间共享。由于目前Linux还没有实现进程间共享信号量,所以这个值只能够取0,就表示这个信号量是当前进程的局部信号量

 

value

信号量初始化值

返回值

成功

0

失败

-1

 

头文件

#include <pthread.h>

函数原型

int sem_wait(sem_t *sem);   //获取信号量,若不成功则阻塞

int sem_trywait(sem_t *sem);  //获取信号量,若不成功立即返回

int sem_post(sem_t *sem);    //释放信号量

int sem_getvalue(sem_t *sem);  //获取信号量的值

int sem_destroy(sem_t *sem);   //删除信号量

作用

获取、释放、删除信号量

参数

sem

信号量对象

返回值

成功

0

失败

-1

 

  2.4、使用实例
  在前面已经通过互斥锁同步机制实现了多线程的顺序执行。下面的例子是用信号量同步机制实现3个线程之间的有序执行,只是执行顺序是跟创建线程的顺序相反。
/*thread_sem.c*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>

#define THREAD_NUMBER 3 /* 线程数 */
#define REPEAT_NUMBER 3 /* 每个线程中的小任务数 */
#define DELAY_TIME_LEVELS 10.0 /*小任务之间的最大时间间隔*/

sem_t sem[THREAD_NUMBER];

void *thrd_func(void *arg)
{
  int thrd_num = (int)arg;
  int delay_time = 0;
  int count = 0;

  /* 进行P操作 */
  sem_wait(&sem[thrd_num]);
  printf("Thread %d is startingn", thrd_num);
  for (count = 0; count < REPEAT_NUMBER; count++)
  {
    delay_time = (int)(rand() * DELAY_TIME_LEVELS/(RAND_MAX)) + 1;
    sleep(delay_time);
    printf("tThread %d: job %d delay = %dn",
    thrd_num, count, delay_time);
  }

  printf("Thread %d finishedn", thrd_num);
  pthread_exit(NULL);
}

int main(void)
{
  pthread_t thread[THREAD_NUMBER];
  int no = 0, res;
  void * thrd_ret;
  srand(time(NULL));

  for (no = 0; no < THREAD_NUMBER; no++)
  {
    sem_init(&sem[no], 0, 0);
    res = pthread_create(&thread[no], NULL, thrd_func, (void*)no);
    if (res != 0)
    {
      printf("Create thread %d failedn", no);
      exit(res);
    }
  }

  printf("Create treads successn Waiting for threads to finish...n");
  /* 对最后创建的线程的信号量进行V操作 */
  sem_post(&sem[THREAD_NUMBER - 1]);
  for (no = THREAD_NUMBER - 1; no >= 0; no--)
  {
    res = pthread_join(thread[no], &thrd_ret);
    if (!res)
    {
      printf("Thread %d joinedn", no);
    }
    else
    {
      printf("Thread %d join failedn", no);
    }

    /* 进行V操作 */
    sem_post(&sem[(no + THREAD_NUMBER - 1) % THREAD_NUMBER]);
  }
  for (no = 0; no < THREAD_NUMBER; no++)
  {
    /* 删除信号量 */
    sem_destroy(&sem[no]);
  }
  return 0;
}

 

该程序运行结果如下所示:
$ ./thread_sem
Create treads success
Waiting for threads to finish...
Thread 2 is starting
Thread 2: job 0 delay = 9
Thread 2: job 1 delay = 5
Thread 2: job 2 delay = 10
Thread 2 finished
Thread 2 joined
Thread 1 is starting
Thread 1: job 0 delay = 7
Thread 1: job 1 delay = 4
Thread 1: job 2 delay = 4
Thread 1 finished
Thread 1 joined
Thread 0 is starting
Thread 0: job 0 delay = 10
Thread 0: job 1 delay = 8
Thread 0: job 2 delay = 9
Thread 0 finished
Thread 0 joined

 

posted @ 2020-02-11 16:42  孤情剑客  阅读(768)  评论(0)    收藏  举报