Linux多线程编程

Posted by JIN Blog on May 27, 2018

Linux多线程基本编程

线程概念

​ 线程是指运行中的程序的调度单位。一个线程指的是进程中一个单一顺序的控制流,也被称为轻量级线程。它是系统独立调度和分配的基本单位。同一进程中的多个线程将共享该系统中的全部系统资源,比如文件描述符和信号处理等。一个进程可以有很多线程,每个线程并行执行不同的任务。

基于线程的并发编程

线程由内核自动调度,每个线程都有它自己的线程上下文(thread context),包括一个惟一的整数线程ID(Thread ID,TID),栈,栈指针,程序计数器,通用目的寄存器和条件码。每个线程和其他线程一起共享进程上下文的剩余部分,包括整个用户的虚拟地址空间,它是由只读文本(代码),读/写数据,堆以及所有的共享库代码和数据区域组成的,还有,线程也共享同样的打开文件的集合。

线程与进程比较

① 和进程相比,它是一种非常“节俭”的多任务操作方式。在Linux系统中,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护其代码段、堆栈段和数据段,这种多任务工作方式的代价非常“昂贵”。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且线程间彼此切换所需要时间也远远小于进程间切换所需要的时间。

② 线程间方便的通信机制。对不同进程来说它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行。这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其他线程所用,不仅方便,而且快捷。

线程基本编程

Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。因为pthread的库不是Linux系统的库,所以在编译时要加上 -lpthread。例如:gcc filename -lpthread。注意,这里要讲的线程相关操作都是用户空间中的线程的操作。

线程创建:创建线程实际上就是确定调用该线程函数的入口点,这里通常使用的函数是pthread_create()。在线程创建后,就开始运行相关的线程函数。

线程退出:在线程创建后,就开始运行相关的线程函数,在该函数运行完之后,该线程也就退出了,这也是线程退出的一种方法。另一种退出线程的方法是使用函数pthread_exit(),这是线程的主动行为。这里要注意的是,在使用线程函数时,不能随意使用exit()退出函数来进行出错处理。由于exit()的作用是使调用进程终止,而一个进程往往包含多个线程,因此,在使用exit()之后,该进程中的所有线程都终止了。在线程中就可以使用pthread_exit()来代替进程中的exit()。

线程等待:由于一个进程中的多个线程是共享数据段的,因此,通常在线程退出后,退出线程所占用的资源并不会随着线程的终止而得到释放。正如进程之间可以用wait()系统调用来同步终止并释放资源一样,线程之间也有类似机制,那就是pthread_join()函数。pthread_join()用于将当前进程挂起来等待线程的结束。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源就被收回。

线程取消:前面已经提到线程调用pthread_exit()函数主动终止自身线程,但是在很多线程应用中,经常会遇到在别的线程中要终止另一个线程的问题,此时调用pthread_cancel()函数来实现这种功能,但在被取消的线程的内部需要调用pthread_setcancel()函数和pthread_setcanceltype()函数设置自己的取消状态。例如,被取消的线程接收到另一个线程的取消请求之后,是接受函数忽略这个请求;如果是接受,则再判断立刻采取终止操作还是等待某个函数的调用等。

线程标识符获取:获取调用线程的标识ID。

线程清除:线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式;非正常终止是线程在其它线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。不论是可预见的线程终止还是异常终止,都回存在资源释放的问题,如何保证线程终止时能顺利地释放掉自己所占用的资源,是一个必须考虑的问题。

从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(包括调用pthread_exit()和异常终止,不包括return)都将执行pthread_cleanup_push()所指定的清理函数。


实验1

功能:使用pthread_create()函数创建线程的实例

代码:thread_create.c文件

/*thread_create.c*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> 
#include <unistd.h>  
void thread_one(void)
{
	int i;
	for(i=0;i<5;i++)
	{
		printf("one  thread\n");
		sleep(2);
	}
}
void thread_two(void)
{
	int i;
	for(i=0;i<5;i++)
	{
		printf("two  thread\n");
		sleep(2);
	}
}
int main(int argc, const char *argv[])
{
	pthread_t ID_one,ID_two;
	int flag; //用来表示创建是否成功 0成功 非0失败
	/*创建一个线程,并使得线程执行thread_one函数*/
	flag=pthread_create(&ID_one,NULL,(void *)thread_one,NULL);
	if(flag)
	{
		printf("pthread_one error\n");
		return 1;
	}
	/*创建一个线程,并使得线程执行thread_two函数*/
	flag=pthread_create(&ID_two,NULL,(void *)thread_two,NULL);
	if(flag)
	{
		printf("pthread_two error\n");
		return 1;
	}
	pthread_join(ID_one,NULL);
	pthread_join(ID_two,NULL);
	return 1;
}

编译:使用命令:gcc thread_create.c -o thread_create -lpthread编译,注意不要忘了加 -lpthread,否则会出现编译错误。

运行结果:两两同时出现就像是同时在运行

monkey@ubuntu:~/demo$ ./thread_create

two thread

one thread

two thread

one thread

two thread

one thread

two thread

one thread

two thread

one thread

monkey@ubuntu:~/demo$

实验2

功能:使用pthread_exit()函数退出线程的举例

代码:thread_exit.c文件

#include <stdio.h>
#include <pthread.h>
void *thread(void *str)
{
	printf("thread is created!\n");
	pthread_exit((void *)1);//这里的参数是返回值任意值
}
 int main(int argc, const char *argv[])
 {
 	pthread_t ID;
	int res;
	void * temp;
	res=pthread_create(&ID,NULL,thread,NULL);
	printf("this is the main thread\n");
	if (res)
	{
		printf("thread is not created!\n");
		return -1;
	}
	res=pthread_join(ID,&temp);//将标记值传给函数
	if (res)
	{
		printf("thread is not exit!\n");
		return -2;
	}
	printf("thread code is %d\n",(int)temp);
 	return 0;
 }

实验结果:

this is the main thread

thread is created!

thread code is 1

实验3

功能:用pthread_join()实现线程等待。

代码:thread_join.c文件

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void * thread()
{
	printf("thread wait...\n");
	sleep(2);
	return NULL;
}
 int main(int argc, const char *argv[])
 {
 	pthread_t ID;
	pthread_create(&ID,NULL,thread,NULL);
	pthread_join(ID,NULL);
	printf("mian GOING\n");
 	return 0;
 }

运行结果:

Monkey@ubuntu:~/demo$ ./join

thread wait… (等待2S)

mian GOING

可以看出,pthread_join()等到线程结束后,程序才继续执行。可以看出,pthread_join()等到线程结束后,程序才继续执行。


线程之间的同步和互斥

同步

一,共享数据

一组并发线程运行在一个进程的上下文中,每个线程都有它自己独立的线程上下文,包括线程ID、栈、栈指针、程序计数器、条件代码和通用寄存器。每个 线程和其他线程一起共享进程上下文的剩余部分。包括整个用户虚拟地址空间,它是由只读文本、读写数据、堆以及所有的共享库代码和数据区域组成的。线程也共 享同样的打开文件的集合。所以任何线程都可以访问共享虚拟存储器的任意位置。如果某个线程修改了一个存储器位置,那么其他每个线程最终都能在它读这个位置 时发现这个变化。

多线程的C程序中的变量根据他们的存储类型被映射到虚拟存储器。

全局变量:全局变量定义在外的变量。在运行函数之时,虚拟存储器的读/写区域只包含每个全 局变量的一个实例。本地自动变量:本地自动变量就是定义在函数内部但没有static属性的变量。在运行时,每个线程的栈都包含它自己的所有本地自动变量 的实例。即使多个线程执行同一个线程程。因为线程拥有自己的栈空间。本地静态变量:本地静态变量是定义在函数内部并有static属性的变量。和全局变量 一样,虚拟存储器的读/写区域只包含在程序中声明的每个本地静态变量的一个实例。由此可知全局变量和本地静态变量都属于共享变量。

信号量的本质是一个非负整数计数器,是共享资源的数目,通常被用来控制对共享资源的访问信号量可以实现线程的同步和互斥,通过sem_post()和sem_wait()函数对信号量 进行加减操作从而解决线程的同步和互斥 信号量数据类型: sem_t

二,用信号量同步线程

共享变量十分方便,同时也引入同步错误的可能性。,可以通过信号量来同步线程。Posix信号量的三个基本操作时sem_init,sem_wait和sem_post,学过操作系统的都知道PV操作,其中的sem_wait相当于P操作,sem_post相当于V操作。

信号量

信号量就是操作系统中多用到的PV原子操作,它广泛应用于进程或线程间的同步与互斥。信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。这里先简单复习一下PV原子操作的工作原理。详细的请看博客:http://blog.csdn.net/mybelief321/article/details/9086151

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

PV原子操作主要用于进程或线程间的同步和互斥这两种典型情况。若用于互斥,几个进程(或线程)往往只设置一个信号量sem,其操作流程如图1所示。当信号量用于同步操作时,往往会设置多个信号量,并安排不同的初始值来实现它们之间的顺序执行,其操作流程如图2所示。

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

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

● sem_wait()和sem_trywait()都相当于P操作,在信号量>0时,它们能将信号量的值减1。两者的区别在于信号量<0时,sem_wait(0将会阻塞进程,而sem_trywait则会立即返回。

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

● sem_getvalue()用于得到信号量的值。

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

信号量数据类型:sem_t

#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned value);
int sem_destory(sem_t *sem);
返回:成功返回0,出错返回错误编号
参数:
    sem信号量指针
    pthread是否在进程间共享的标志,0为不共享,1为共享
    value初始的时候信号量值

信号量的加减操作

#include<semaphore.h>
int sem_post(sem_t *sem);
    功能:增加信号量的值
int sem_wait(sem_t *sem);
    功能:减少信号量的值
int sem_trywait(sem_t *sem);
    功能:sem_wait()的非阻塞版本
    返回:0成功,出错返回错误编码

注意: 调用:sem_post()一次信号量作加1操作 调用sem_wait()一次信号量减1操作 当线程调用sem_wait后,若信号量的值小于0,则线程阻塞,只有其它线程在调用sem_post对信号量做加操作后并且其值大于或者等于0的时候,阻塞的线程才能继续运行; 一般调用原则: 如果当做互斥锁的时候,一般初始化的时候初始化为1,然后对其进行PV操作 如果需要进行同步的时候,一般初始化的时候信号量为0,然后对其进行PV操作

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

//定义两个全局变量,sem_one,sem_two
sem_t sem_one,sem_two;

/* *
 *线程1执行的函数,由于其第一个执行,所以是不需要进行等待的,但是其需要唤醒别的
 所以在执行结束后需要post去将计数器加1
 * */
void *one_th(void* argv)
{
  printf("one:%lx runing\n",pthread_self());
  sem_post(&sem_one);
  return (void*)0;
}
/* **线程2执行的函数* */
void *two_th(void* argv)
{
  sem_wait(&sem_one);
  printf("two:%lx runing\n",pthread_self());
  sem_post(&sem_two);
  return (void*)0;
}

/* **线程3执行的函数* */
void *three_th(void* argv){
  sem_wait(&sem_two);
  printf("three:%lx runing\n",pthread_self());
  return (void*)0;
}
int main(int argc,char * argv[])
{
  pthread_t thread_one,thread_two,thread_three;
  //初始化函数,因为是同步,所以将其初始化的时候设置为0
  sem_init(&sem_one,0,0);
  sem_init(&sem_two,0,0);
  int err = 0 ;
  if((err = pthread_create(&thread_one,NULL,one_th,(void*)0)) != 0){
      perror("one thread create error");
  }
  if((err = pthread_create(&thread_two,NULL,two_th,(void*)0)) != 0){
      perror("one thread create error");
  }
  if((err = pthread_create(&thread_three,NULL,three_th,(void*)0)) != 0){
      perror("one thread create error");
  }
  pthread_join(thread_one,NULL);
  pthread_join(thread_two,NULL);
  pthread_join(thread_three,NULL);
  //信号量的销毁函数
  sem_destroy(&sem_one);
  sem_destroy(&sem_two);
  return 0;
}

互斥

概述

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

互斥锁

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

互斥锁机制主要包括以下基本函数:

● 互斥锁初始化:pthread_mutex_init()

● 互斥锁上锁:pthread_mutex_lock()

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

● 互斥锁解锁:pthread_mutex_unlock()

● 消除互斥锁:pthread_mutex_destroy()

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

互斥锁的操作流程如下:

1)在访问共享资源后临界区域前,对互斥锁进行加锁。

2)在访问完成后释放互斥锁导上的锁。

3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放。

#include <stdio.h>  
#include <pthread.h>  
#include <unistd.h>  
pthread_mutex_t mutex; //互斥锁  
// 打印机  
void printer(char *str)  
{  
    pthread_mutex_lock(&mutex); //上锁  
    while(*str!='\0')  
    {  
        putchar(*str);    
        fflush(stdout);  
        str++;  
        sleep(1);  
    }  
    printf("\n");   
    pthread_mutex_unlock(&mutex); //解锁  
}  
// 线程一  
void *thread_fun_1(void *arg)  
{  
    char *str = "hello";  
    printer(str); //打印  
}  
// 线程二  
void *thread_fun_2(void *arg)  
{  
    char *str = "world";  
    printer(str); //打印  
}  
  
int main(void)  
{  
    pthread_t tid1, tid2;       
    pthread_mutex_init(&mutex, NULL); //初始化互斥锁     
    // 创建 2 个线程  
    pthread_create(&tid1, NULL, thread_fun_1, NULL);  
    pthread_create(&tid2, NULL, thread_fun_2, NULL);  
    // 等待线程结束,回收其资源  
    pthread_join(tid1, NULL);  
    pthread_join(tid2, NULL);   
 	pthread_mutex_destroy(&mutex); //销毁互斥锁  
    return 0;  
}