OS之进程管理---多线程模型和线程库(POSIX PTread)

 

多线程简介

线程是CPU使用的基本单元,包括线程ID,程序计数器、寄存器组、各自的堆栈等,在相同线程组中,所有线程共享进程代码段,数据段和其他系统资源。
传统的的单线程模式是每一个进程只能单个控制线程,但是随着计算机硬件的提升和多(多处理器)的普及,传统的单线程模式已经不适用于现在,所以希望一个进程能够具有多个控制线程,这样就可以同时执行多个任务了。
在这里插入图片描述

多线程模型

有两种方法来提供线程的支持:用户层的用户线程(User-Level Thread)、内核层的内核线程(Kernel-Level Thread)。

  • 用户线程:用户线程是指不需要内核的支持而在用户程序中实现的线程,其不依赖于操作系统核心,用户进程可以利用线程库来进行创建、管理线程,不需要通过用户态/核心态转变
  • 内核线程:由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。几乎所有的现代操作系统(win, linux,mac os,Solaris…)都支持内核线程。

用户线程和内核线程之间存在三种关联方式:多对一模型、一对一模型、多对多模型。

多对一模型

多对一模型映射多个用户级线程到一个内核线程。线程管理是由用户空间的线程库来完成的,所以效率挺高。但是,如果一个线程执行阻塞系统调用,那么整个进程将会阻塞。另外,因为任一时间只有一个线程可以访问内核,所以多个线程不能运行在多核系统上,现在已经几乎没有操作系统来使用这个模型了,因为它无法利用多个处理核。
在这里插入图片描述

一对一模型

一对一模型映射每个用户线程到一个内核线程。该模型在一个线程执行阻塞系统调用时,能够允许另外一个线程继续执行,所以它提供了比多对一模型更好的并发功能。它页允许多个线程并行运行在多核系统上。但是这个模型存在一个缺点:创建一个用户线程就要创建一个相应的内核线程。由于创建内核线程的开销会影响操作系统的性能,所以这种模型的大多数实现限制了系统支持的线程数量,Linux、Window家族都实现了一对一模型。
在这里插入图片描述

多对多模型

多对多模型多路复用多个用户级线程到同样数量或更少数量的内核线程。内核线程的数量可能与特定应用程序或特定机器有关(应用程序在多处理器上比在单处理器上可能分配到更多数量的线程)。
在这里插入图片描述

多对多模型的一种变种仍然是多路复用多个用户级线程到同样数量或更少数量的内核线程,但是也允许绑定某个用户线程到一个内核线程上。这种变种成为双层模型(two-level-model)。比如在Solaris系统第九版之前就支持这种双层模型

多对多模型没有上面两个模型的缺点,开发人员可以创建任意多的用户线程,并且相应的内核线程能在多处理器上来并发的执行,而且,当一个线程执行堵塞系统调用时,内核可以调度另外一个线程来执行。

线程库

在上面的模型介绍中,我们提到了通过线程库来创建、管理线程,那么什么是线程库呢?线程库(Thread library)是为开发人员提供创建和管理线程的一套API.
实现线程库有两种方法:

  1. 在用户空间中提供一个没有内核支持的库。这种库的所有代码和数据结构都在用户空间,调用库内的一个函数只是导致了用户空间的一个本地函数的调用,而不是系统调用。
  2. 实现由操作系统直接支持的内核级的一个库。库内的代码和数据结构位于内核空间,调用库中的一个API函数将会导致对内核的系统调用。

目前有三种主要的线程库:POSIX Pthread、Window API、Java

Pthread作为POSIX标准的扩展,可以提供用户级或内核级的库。
Window线程库是用于Window操作系统的内核级线程库。
Java线程API通常采用宿主系统的线程库来实现,也就是说在Win系统上,Java线程API通常采用Win API来实现,在UNIX类系统上,采用Pthread来实现。

创建和销毁线程

当一个多线程程序开始执行的时候,它有一个线程在跑,就是执行main()函数的线程。这已经是一个完整的线程,有它自己的thread ID。创建一个新的线程,应该调用**pthread_create()**函数。下面给出了使用它的例子:

#include <stdio.h>       /* standard I/O routines                 */
#include <pthread.h>     /* pthread functions and data structures */

/* function to be executed by the new thread */
void*
do_loop(void* data)
{

    int i;			/* counter, to print numbers */
    int j;			/* counter, for delay        */
    int me = *((int*)data);     /* thread identifying number */

    for (i=0; i<10; i++) {
	for (j=0; j<5000000; j++) /* delay loop */
	    ;
        printf("'%d' - Got '%d'\n", me, i);
    }

    /* terminate the thread */
    pthread_exit(NULL);
}

/* like any C program, program's execution begins in main */
int
main(int argc, char* argv[])
{
    int        thr_id;         /* thread ID for the newly created thread */
    pthread_t  p_thread;       /* thread's structure                     */
    int        a         = 1;  /* thread 1 identifying number            */
    int        b         = 2;  /* thread 2 identifying number            */

    /* create a new thread that will execute 'do_loop()' */
    thr_id = pthread_create(&p_thread, NULL, do_loop, (void*)&a);
    /* run 'do_loop()' in the main thread as well */
    do_loop((void*)&b);
    
    /* NOT REACHED */
    return 0;
}

关于该程序需要知道的几点:

  • 注意main函数也是一个线程,所以它与它所创建的线程一起执行do_loop()函数
  • pthread_create()函数需要四个参数。第一个参数由函数 pthread_create()使用来提供该线程的信息(即线程标识符)。第二个参数用来指定新线程的属性。在我们的例子中我们传递一个NULL指针给pthread_create(),以使用默认属性。第三个参数是该线程执行程序的名称。第四个参数是传递给该函数(该线程要执行的函数)的参数。注意映射到void*并非由ANSI-C语法的要求,但是放在这儿更加清晰。
  • 函数中的循环延迟只为了演示线程是并行执行的。如果你的CPU跑的快使用一个大的延迟;并且你会在另一个线程之前看到一个线程的所有打印。
  • pthread_exit()函数的调用,使得线程退出并且会释放所有该线程占用的资源。在一个线程的最外层函数的结束并不需要调用这个函数,因为当其返回(return)时,该线程会自动地结束(exit)。这个函数在我们想要在一个线程中间结束它时用。

为了使用编译多线程程序gcc,我们需要将它与pthreads库链接。假设系统上已经安装了这个库(pthread),下面是如何编译我们的第一个程序:

gcc pthread_create.c -o pthread_create -lpthread 
使用互斥锁同步线程

运行多个使用相同内存空间的线程时的一个基本问题是确保它们不会“踩到彼此的脚趾”。通过这个我们指的是使用来自两个不同线程的数据结构的问题。

例如,考虑两个线程尝试更新两个变量的情况。一个尝试将两者都设置为0,另一个尝试将两者都设置为1.如果两个线程同时尝试这样做,我们可能会遇到一个变量包含1,一个包含0的情况。这是因为上下文切换(我们已经知道现在是什么,对吧?)可能会在第一个步骤清零第一个变量后发生,然后第二个线程会将两个变量都设置为1,当第一个线程恢复运行时,它将第二个变量归零,从而将第一个变量设置为“1”,将第二个变量设置为“0”。

什么是互斥体?

由pthreads库提供的解决此问题的基本机制称为互斥锁。互斥锁是一种保证三件事的锁:

  • 原子性 - 锁定互斥锁是一种原子操作,这意味着操作系统(或线程库)会向您保证,如果锁定了互斥锁,则其他线程无法同时锁定此互斥锁。
  • 奇点 - 如果线程设法锁定互斥锁,则确保在原始线程释放锁之前,没有其他线程能够锁定线程。
  • 非忙等待 - 如果线程试图锁定被第二个线程锁定的线程,则第一个线程将被挂起(并且不会消耗任何CPU资源),直到第二个线程释放锁定为止。此时,第一个线程将被唤醒并继续执行,并由其锁定互斥锁。

从这三点我们可以看到如何使用互斥锁来确保对变量的独占访问(或者在一般的关键代码段中)。下面是一些伪代码,用于更新我们讨论的两个变量,并且可以由第一个线程使用:

锁定互斥锁'X1'。
将第一个变量设置为“0”。
将第二个变量设置为“0”。
解锁互斥锁'X1'。

同时,第二个线程将执行以下操作:

锁定互斥锁'X1'。
将第一个变量设置为“1”。
将第二个变量设置为“1”。
解锁互斥锁'X1'。

假设两个线程使用相同的互斥锁,我们可以确保在它们都运行此代码后,两个变量都设置为“0”,或者两者都设置为“1”。你需要注意这需要程序员的一些工作 - 如果第三个线程是通过一些不使用这个互斥锁的代码访问这些变量,它仍然可能搞乱变量的内容。因此,在一小组函数中包含访问这些变量的所有代码非常重要,并且始终只使用这些函数来访问这些变量。

创建和初始化互斥锁

需要声明一个类型的变量 pthread_mutex_t,然后对其进行初始化。最简单的方法是将它分配给PTHREAD_MUTEX_INITIALIZER常数。

pthread_mutex_t a_mutex = PTHREAD_MUTEX_INITIALIZER;

这种类型的初始化会创建一个名为“fast mutex”的互斥锁。这意味着如果一个线程锁定了互斥锁,然后再次尝试将其锁定,它将被卡住 - 它将处于死锁状态。

还有另一种类型的互斥锁,称为“递归互斥锁”,它允许锁定它的线程多次锁定它,而不会被阻塞(但是其他试图锁定互斥锁的线程现在将被阻塞)。如果线程然后解锁互斥锁,它仍将被锁定,直到它被解锁的次数与锁定时相同。这与现代门锁的工作方式类似 - 如果顺时针旋转两次锁定它,则需要逆时针旋转两次以解锁它。可以通过将常量赋值给:
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP 互斥变量来创建这种互斥锁。

锁定和解锁互斥锁

为了锁定互斥锁,我们可以使用该功能 pthread_mutex_lock()。此函数尝试锁定互斥锁,或者如果互斥锁已被另一个线程锁定,则阻止该线程。在这种情况下,当第一个进程解锁互斥锁时,该函数将返回,并且我们的进程锁定了互斥锁。以下是如何锁定互斥锁(假设它已在之前初始化):

int rc = pthread_mutex_lock(&a_mutex);
if (rc) { /* an error has occurred */
    perror("pthread_mutex_lock");
    pthread_exit(NULL);
}
/* mutex is now locked - do your stuff. */

在线程执行了必要的操作(更改变量或数据结构,处理文件或其他任何操作)后,它应该使用该pthread_mutex_unlock()函数释放互斥锁,如下所示:

rc = pthread_mutex_unlock(&a_mutex);
if (rc) {
    perror("pthread_mutex_unlock");
    pthread_exit(NULL);
}

在我们使用互斥锁后,我们应该销毁它。完成使用意味着没有线程需要它。如果只有一个线程完成了互斥锁,它应该保持活动状态,对于可能仍然需要使用它的其他线程。一旦完成使用它,最后一个可以使用以下pthread_mutex_destroy()功能销毁它 :

rc = pthread_mutex_destroy(&a_mutex);

在此调用之后,此变量(a_mutex)可能不再用作互斥锁,除非它再次初始化。因此,如果太早破坏互斥锁,而另一个线程试图锁定或解锁它,该线程将从EINVAL锁定或解锁函数获取错误代码。

我写了一个例子:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
 
void *functionC(void* type);
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int  counter = 0;
int counsers = 0; 
void main()
{
   int rc1, rc2;
   pthread_t thread1, thread2;
 
    int        a         = 1;  /* thread 1 identifying number            */
    int        b         = 2;  /* thread 2 identifying number            */


   /* Create independent threads each of which will execute functionC */
 
   if( (rc1=pthread_create( &thread1, NULL, &functionC, (void*)&a)) )
   {
      printf("Thread creation failed: %d\n", rc1);
   }
   if( (rc2=pthread_create( &thread2, NULL, &functionC, (void*)&b)) )
   {
      printf("Thread creation failed: %d\n", rc1);
   }

 
 
   //pthread_join( thread1, NULL);
   printf("counter=%d\n", counter);
   printf("counsers=%d\n", counsers);

   //delete pthread mutex destroy
   //pthread_mutex_destroy(&mutex1);
 
   exit(EXIT_SUCCESS);
}
 
void *functionC(void* type)
{

	int me = *((int*)type);
  	pthread_mutex_lock( &mutex1 );
	if(me == 1) {
		counter=1;
		counsers = 1;
	}
	else if(me == 2) {
		counter = 0;
		counsers = 0;
	}
   	pthread_mutex_unlock( &mutex1 );
}
参考:
  1. POSIX thread (pthread) libraries

  2. Multi-Threaded Programming With POSIX Threads

  3. 《操作系统概念》(第九版)

 

posted @ 2019-03-20 17:34  如是说  阅读(1451)  评论(0编辑  收藏  举报