Linux学习之多线程编程

Linux应用程序之多线程程序设计

1、线程介绍

前面我们介绍了Linux下进程的概念,下面我们就来说一说linux下多线程程序设计。

之所以在有了进程之后还要创建线程的概念,是因为使用线程有许多好处,

首先相比于进程来讲,一方面线程更加的节约空间,因为在Linux系统下,启动一个新的进程必须为其分配独立的地址空间,而运行于一个进程中的多个线程,他们彼此之间是共享这个进程的地址空间,共享大部分数据,启动一个线程花费的空间远远小于启动一个进程所花费的空间,而且线程间切换比进程间切换所需时间小得多。另一方面一个进程中线程的通信快捷方便,因为他们共享大部分数据,可以直接访问,而进程都是独立的地址空间,多个进程间需要通过一定的通信机制才能进行通信。

当然线程还有其它的好处,在网上可以找到更详细的总结,在此就不一一列出了,我们直接开始我们的主题---Linux下多线程程序设计。

Linux系统下的多线程遵循POSIX线程接口,POSIX接口指的是可移植操作系统接口,编写Linux下的多线程程序时需要添加头文件pthread.h,而且由于编译Linux下多线程时需要使用库libpthread.a,因此在编译时我们需要加入-lpthread选项,否则系统找不到相关的函数。下面我们就来介绍一下Linux系统下多线程编程的相关函数。

(1)创建一个线程
int  pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg);
tid:线程标识符,类似于进程ID,其类型为pthread_t,当调用pthread_create成功时,通过*tid指针返回。
*attr:指定创建线程的属性,如线程优先级、初始栈大小、是否为守护进程等。我们一般使用NULL来指定默认值。
func: 函数指针,指定当新的线程创建之后,将执行该函数。
arg: 前面func函数的参数,如果想传递多个参数,可以将他们封装在结构体中。
该函数用于创建一个线程,成功返回0,否则返回一个任意正数。
 
(2)等待某个进程退出
int pthread_join(pthread_t tid , void **status)
pthread_tid :指定要等待的线程ID
status该参数如果不为NULL,那么线程的返回值存储在status指向的空间中。
该函数是一个线程阻塞函数,调用它的函数将一直等待到被等待的线程结束为止。
(3)终止线程函数
void pthread_exit(void *status);
status:线程终止的返回值。
该函数用于终止线程,可以指定返回值,以便其它线程通过pthread_join函数获取该线程的返回值。顺便说一下,我们退出还有其它方式,比方说return,exit等,但是这些退出方式不能乱用,pthread_exit()用于线程退出,可以指定返回值。return是函数返回,只有线程函数return,线程才会退出。exit是进程退出,如果在线程函数中调用exit进程中的所有函数都会退出。
有了这三个函数,我们就可以编写一个简单的多线程例程了:
thread_join.c
#include <stdio.h>
#include <pthread.h>
 
int g_Flag = 0;
void *Thread1(void *arg)
{
         int *temp = (int *)arg;
         *temp = 1;
        printf("this is thread1 and the g_Flag = %d\n" , g_Flag);  
        pthread_exit(0);
}
int main()
{
     pthread_t thread1;
     int err;
     //创建一个线程,并向其传入一个值g_Flag
     err = pthread_create(&thread1 , NULL ,Thread1 , &g_Flag);
     if(err!=0)
        {
           printf("thread2 create failure:%s\n",strerror(err));   
        }
      printf("main:the g_Flag = %d\n" , g_Flag);
      sleep(3);
      printf("main:the g_Flag = %d\n" , g_Flag);
 
      exit(0);                 
}
这个程序的功能就是在主线程中创建一个线程,然后在这个线程中改变我们的g_Flag值,然后在主线程中打印。我们可以在linux中运行这个程序,得到的结果如下:
main:the g_Flag = 0
This is thread1 and the g_Flag = 1
main:the g_Flag = 1
上面就是我们Linux中线程的基本操作了,下面我们就来介绍一些更加详细的知识。
 
2、线程同步
类似于我们的进程一样,多线程编程时也面临着共享数据的保护问题,因为当多个线程都可以修改共享变量时,如果不加以保护,就有可能出现数据的错误,因此在多线程编程中,我们需要采取一些措施来保证线程间数据的同步操作。常见的线程同步属性包括互斥锁、条件变量和信号量。
(1)       互斥锁
互斥锁用来保证一段时间内只有一个线程在执行一段代码。其涉及相关函数、类型比较多,在此简单介绍一些常用的。
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mpth);
这两个函数一个为加锁函数,一个为解锁函数,其参数为pthread_mutex_t类型的变量。下面在用一个例子来简单看看多线程中互斥锁的使用方法:
thread_com.c
#include <stdio.h>
#include <pthread.h>
 
pthread_mutex_t mutex; 
int g_Flag;
 
void *thread1(void *arg)
 {
       int i;
       printf("this is thread1\n");
       for(i = 0 ; i < 5 ; i++)
         {
             printf("thread1: g_Flag = %d\n", g_Flag);
             pthread_mutex_lock(&mutex);
                  g_Flag++;
             pthread_mutex_unlock(&mutex);                 
             sleep(2);                    
         }
         pthread_exit(NULL);
 }
void *thread2(void *arg)
 {
       int i;
       printf("this is thread2\n");
       for(i = 0 ; i < 5 ; i++)
         {
             printf("thread2: g_Flag = %d\n", g_Flag);
             pthread_mutex_lock(&mutex);
                  g_Flag++;
             pthread_mutex_unlock(&mutex);                        
             sleep(2);
         }
          pthread_exit(NULL);
 }
 
void main()
{
      pthread_t thread_id[2];
      int err;
      
      g_Flag = 0;
      pthread_mutex_init(&mutex,NULL);
      err = pthread_create(&thread_id[0],NULL,thread1 , NULL);
      if(err!=0)
              {
                  printf("the thread1 create failure\n");
                  exit(0); 
              }
      err = pthread_create(&thread_id[1],NULL,thread2 , NULL);
      if(err!=0)
              {
                  printf("the thread1 create failure\n");
                  exit(0); 
              }
              
             pthread_join(thread_id[0], NULL);
             pthread_join(thread_id[1] ,NULL);
        
      exit(0);     
}
在这个程序中,我们在main函数中创建了两个线程,然后在这两个线程中都对g_Flag变量进行递增,同时采用互斥锁对其进行相应的保护。打印输出的结果如下:

 
 
需要说明的是,如果有多个互斥锁,我们必须在使用的时候非常小心,不然很容易造成死锁,这时我们可以试用函数pthrea_mutex_trylock,它是函数pthread_mutex_lock的非阻塞版本,当它发现死锁不可避免时,它会返回相应信息。
(2)       条件变量
上面说了互斥锁来实现线程间数据的通信,但是互斥锁太单一,只有锁定和非锁定两种状态,因此我们需要使用条件变量,条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。
条件变量本身由互斥量来保护,在使用之前我们必须对其进行初始化,可以使用
 int pthread_cond_init(pthread_cond_t *cond , pthread_condattr_t *attr);
这个函数中cond是一个指向结构pthread_cond_t的指针,attr是一个指向结构pthread_condattr_t的指针,该结构为条件变量的属性结构,创建一个默认属性的条件变量时我们可以把它设为NULL。
另外我们还可以使用函数
int pthread_cond_destroy(pthread_cond_t *cond);
释放一个条件变量。
函数
int pthread_cond_wait(pthread_cond_t *cond , pthread_mutex_t *mutex);
使线程阻塞在一个条件变量上,传递给该函数的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁。
还有一个阻塞线程的函数是
pthrea_muutex_cond_timedwait
这个函数比上面的函数多一个时间参数,表示愿意阻塞的时间,时间一过,阻塞就被解除。
另外还有两个函数用于通知线程条件已经满足:
int pthread_cond_signal(pthread_cond_t *cond);
  int pthread_cond_broadcast(pthread cond_t *cond);
第一个函数将唤醒等待该条件的某个线程,第二个函数将唤醒等待该条件的所有线程,调用这两个函数也称为向线程发送信号。需要注意的是一定要在改变条件状态以后再给线程发送信号。说了太多理论的东西感觉很难理解,下面我们就以一个实例来看看具体的用法:
#include <stdio.h>
#include <pthread.h>
 
int count = 0;
pthread_mutex_t mutex;
pthread_cond_t  cond;
 
void *decrease()
{
      printf("this is decrease-thread: head\n");
      pthread_mutex_lock(&mutex);
         while(count == 0)
            {
               pthread_cond_wait(&cond , &mutex);                        
            }  
         // count--;   
      pthread_mutex_unlock(&mutex);
      printf("this is decrease-thread: tail\n");
      pthread_exit(NULL);
}
 
void *increase()
{
      printf("this is increase-thread: head\n");  
      
      pthread_mutex_lock(&mutex);      
          count++;
      pthread_cond_signal(&cond);  
      pthread_mutex_unlock(&mutex);
       
      printf("this is increase-thread: tail\n"); 
      pthread_exit(NULL);  
}
 
void main()
{
    pthread_t thread_id[2];
    int err;
    count = 0;
    
    err = pthread_create(&thread_id[0],NULL,decrease,NULL);
    if(err!=0)
     {
        printf("create thread1 failure\n");      
        exit(0);
     }
    err = pthread_create(&thread_id[1],NULL,increase,NULL);
    if(err!=0)
            {
                     printf("create thread2 failure\n");
                     exit(0);
            }    
    sleep(5);
    printf("main: count = %d\n" , count);
    exit(0);       
}
在这段代码中,我们在主线程中创建了两个线程,分别是decrease和increase,在decrease线程中,我们采用了条件变量的等待函数,需要注意的是,我们在此没有对count这个变量做减一操作,是为了在我们运行时结果明显。在increase中我们对count采用加一操作。运行程序所得结果如下:

如此便可以看出我们条件变量的用法了。
 
(3)信号量
接下来就来介绍一下信号量,信号量在linux进程通信时我们已经有所了解,线程中的信号量与其有些类似,都是用来保护一些公共资源。信号量本质上是一个非负的整数计数器,调用函数sem_post()函数时增加信号量。当信号量值大于0时,才能使用公共资源。我们还可以采用sem_wait()函数来减少信号量,用sem_init()函数来初始化信号量。下面我们就给出一个程序实例来看看我们信号量的用法:
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
sem_t sem; //定义一个信号量
int fd;    //定义一个文件标识符
 
void delay_ms(int n)
{
               while(n--);              
}
void *write_one(void *arg)
{
           char *buf = (char *)arg;
           sem_wait(&sem);        
     write(fd ,buf , 6);        
     delay_ms(1000);
     sem_post(&sem);
     
     pthread_exit(NULL);                
}
 
void *write_two(void *arg)
{    
    sem_wait(&sem);
    write(fd,",world belongs " , 15);  
    delay_ms(1000);
    write(fd ,"to you" , 6);
    sem_post(&sem);
    pthread_exit(NULL);                                  
}
 
void *write_three(void *arg)
{
       sem_wait(&sem);
       write(fd ,",come on" ,8);                
       delay_ms(1000);
    sem_post(&sem);   
    pthread_exit(NULL);   
}
 
void main()
{
    //int fd;
    pthread_t thread_id[3];
    int ret;
    
    //打开一个文件,并初始化信号量
    fd = open("/home/share/fun_call/lesson2/data.txt",O_RDWR , O_APPEND);
    sem_init(&sem , 0 , 1);    
    
    ret = pthread_create(&thread_id[0] , NULL , write_one ,(void *)"Hi,boy" );
    if(ret!=0)
            {
                printf("create write_one fail\n");
                exit(0);         
            }
    
    ret = pthread_create(&thread_id[1] , NULL , write_two , NULL);
    if(ret!=0)
            {
                printf("create write_two fail\n");
                exit(0);         
            }
            
    ret = pthread_create(&thread_id[2] , NULL , write_three , NULL);
    if(ret!=0)
            {
                printf("create write_three fail\n");
                exit(0);         
            }           
            
     pthread_join(thread_id[0] , NULL); 
     pthread_join(thread_id[1] , NULL);
     pthread_join(thread_id[2] , NULL);
                        
     exit(0);
}
该程序其实是一个二值信号量的情况,我们将信号量的初始值定位1,然后通过主线程创建三个线程,分别向我们的公共资源data.txt中写入信息,在每个线程中,我们通过上面的函数分别获取信号量和释放信号量,由此达到对公共资源的保护,运行程序后,我们可以发现我们的data.txt中写入了正确的结果,但是有一点说明,在我电脑上,线程的运行顺序为(不包括主线程)
thread_three->three_two->three_one
因此打印输出就是“,come on,world belongs to you Hi,boy”
在你的电脑上可能就是另外的顺序了,这和CPU的调度有关,但是我所谓的正确打印,主要是指线程thread_two中,我写入了两次信息,这写入的两次在data.txt中必须是连在一起显示的,如果在文件中没有连在一起,说明我们的信号量没有保护我们的临界资源,中途有其它线程插入了,说明我们的程序有问题,那我们就要好好看看我们的额程序了。
    以上就是线程的一些基本知识了,希望得到大家的指正补全。
 
 
 
 
 
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值