Linux 多线程编程

前言

Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用libpthread.a。Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork。

基础知识点和代码实现

1.运行一个进程中的多个线程,彼此之间使用相同的地址空间,共享大部分数据。

2.启动一个线程所花费的空间远远小于启动一个进程所话费的空间。

3.线程间切换所需要的时间远远小于进程间切换所需要的时间。

4.不同进程具有独立的数据空间,数据的传递只能通过通信的方式。--费时、不方便

统一进程下的线程之间共享数据空间,一个线程数据可以直接为其他线程所用。--快捷、方便

5.编写多线程需要注意的地方:

有的变量不能同时被两个线程所修改

子程序中声明为static的数据可能为多线程带来灾难性打击

6.多线程优点:

1)提高应用程序相响应,将耗时长的操作置于一个新的线程,避免等待。

2)使CPU多核系统更加高效。

3)改善程序结构。将长而复杂的进程可以分为多个线程。

7. 进程是资源分配的基本单位,线程没什么资源。共享进程资源

8. volatile的作用是: 作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.

简单地说就是防止编译器对代码进行优化.比如如下程序:

XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;

对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,则编译器会逐一的进行编译并产生相应的机器代码(产生四条代码).

9.线程相关操作

9.1线程的标识符 :pthread_t

pthread_t 在头文件/usr/include/bits/pthreadtypes.h定义如下:

    typedef unsigned long int pthread_t;

9.2创建线程:pthread_creat

原型为:

 extern int pthread_create _P((pthread_t *_thread,_const pthread_attr_t *_attr,void *(*_start_routine) (void *),void *_arg));

第一个参数:指向线程标识符的指针 第二个参数:设置线程属性 第三个参数:线程运行函数的起始地址 最后一个参数:运行函数的参数

线程创建成功,返回0,不为0,创建失败。

常见错误返回代码:EAGAIN:系统限制创建新的线程 EINVAL:线程的属性值非法

线程创建成功,新创建线程运行参数三和参数四确定的函数。

9.3等待线程结束pthread_join和线程结束pthread_exit

pythread_join函数原型:

   extern int pthread_join_P((pthread_t _th,void**_thread_return));

第一个参数:被等待的线程标识符 第二个参数:一个用户定义的指针,可以用来存储被等待线程的返回值

这个函数是线程阻塞函数,调用它的函数一直等待到被等待线程结束为止,函数返回时,被等待线程的资源被收回。

线程结束有两种途径:函数结束,调用它的线程结束;另一种方式通过函数pthread_exit来实现。

pthread_exit函数原型:

exter void pthread_exit_P((void*_retval)_attribute_((_noreturn_));

唯一的参数是函数的返回代码:只要pthread_join中的第二个参数thread_return不是NULL,该值传递给thread_return。

一个线程不能被多个线程等待,否则第一个接受到的信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH.

10.线程的同步

虽然线程本地存储可以避免线程访问共享数据,但是线程之间的大部分数据始终还是共享的。在涉及到对共享数据进行读写操作时,就必须使用同步机制,否则就会造成线程们哄抢共享数据的结果,这会把你的数据弄的七零八落理不清头绪。

Linux提供的线程同步机制主要有互斥锁和条件变量。

10.1互斥锁

所谓的互斥就是线程之间互相排斥,获得资源的线程排斥其它没有获得资源的线程。Linux使用互斥锁来实现这种机制。

既然叫锁,就有加锁和解锁的概念。当线程获得了加锁的资格,那么它将独享这个锁,其它线程一旦试图去碰触这个锁就立即被系统“拍晕”。当加锁的线程解开并放弃了这个锁之后,那些被“拍晕”的线程会被系统唤醒,然后继续去争抢这个锁。至于谁能抢到,只有天知道。但是总有一个能抢到。于是其它来凑热闹的线程又被系统给“拍晕”了……如此反复。

从互斥锁的这种行为看,线程加锁和解锁之间的代码相当于一个独木桥,同意时刻只有一个线程能执行。从全局上看,在这个地方,所有并行运行的线程都变成了排队运行了。比较专业的叫法是同步执行,这段代码区域叫临界区。同步执行就破坏了线程并行性的初衷了,临界区越大破坏得越厉害。所以在实际应用中,应该尽量避免有临界区出现。实在不行,临界区也要尽量的小

Linux初始化和销毁互斥锁的接口是pthread_mutex_init()和pthead_mutex_destroy(),对于加锁和解锁则有pthread_mutex_lock()、pthread_mutex_trylock()和pthread_mutex_unlock()。这些接口的完整定义如下:

从这些定义中可以看到,互斥锁也是有属性的。只不过这个属性在绝大多数情况下都不需要改动,所以使用默认的属性就行。方法就是给它传递NULL。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destory(pthread_mutex_t *mutex );
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

phtread_mutex_trylock()比较特别,用它试图加锁的线程永远都不会被系统“拍晕”,只是通过返回EBUSY来告诉程序员这个锁已经有人用了。至于是否继续“强闯”临界区,则由程序员决定。系统提供这个接口的目的可不是让线程“强闯”临界区的。它的根本目的还是为了提高并行性,留着这个线程去干点其它有意义的事情。当然,如果很幸运恰巧这个时候还没有人拥有这把锁,那么自然也会取得临界区的使用权。

互斥锁在同一个线程内,没有互斥的特性。也就是说,线程不能利用互斥锁让系统将自己“拍晕”。解释这个现象的一个很好的理由就是,拥有锁的线程把自己“拍晕”了,谁还能再拥有这把锁呢?但是另外情况需要避免,就是两个线程已经各自拥有一把锁了,但是还想得到对方的锁,这个时候两个线程都会被“拍晕”。一旦这种情况发生,就谁都不能获得这个锁了,这种情况还有一个著名的名字——死锁。死锁是永远都要避免的事情,因为这是严重损人不利己的行为。

10.2 条件变量

条件变量关键点在“变量”上。与锁的不同之处就是,当线程遇到这个“变量”,并不是类似锁那样的被系统给“拍晕”,而是根据“条件”来选择是否在那里等待。等待什么呢?等待允许通过的“信号”。这个“信号”是系统控制的吗?显然不是!它是由另外一个线程来控制的。

条件变量是一种事件机制。由一类线程来控制“事件”的发生,另外一类线程等待“事件”的发生。为了实现这种机制,条件变量必须是共享于线程之间的全局变量。而且,条件变量也需要与互斥锁同时使用。

初始化和销毁条件变量的接口是pthread_cond_init()和pthread_cond_destory();控制“事件”发生的接口是pthread_cond_signal()或pthread_cond_broadcast();等待“事件”发生的接口是pthead_cond_wait()或pthread_cond_timedwait()。它们的完整定义如下:

int pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *attr);
int pthread_cond_destory(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex,
const timespec *abstime);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

对于等待“事件”的接口从其名称中可以看出,一种是无限期等待,一种是限时等待。后者与互斥锁的pthread_mutex_trylock()有些类似,即当等待的“事件”经过一段时间之后依然没有发生,那就去干点别的有意义的事情去。而对于控制“事件”发生的接口则有“单播”和“广播”之说。所谓单播就是只有一个线程会得到“事件”已经发生了的“通知”,而广播就是所有线程都会得到“通知”。对于广播情况,所有被“通知”到的线程也要经过由互斥锁控制的独木桥。

这里插入一个简单多线程的例子

#include 要包含头文件pthread.h 还需要链接libpthread.so库,链接阶段应该有类似指令:gcc program.o -o program -lpthread

多线程例子:

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


void *thread(void *arg)
{
 printf("This is a  thread and arg=%d.\n",*(int *)arg);
 *(int*)arg = 0;
 return arg;
}


int main(int argc,char *argv[])
{
 pthread_t th;
 int ret;
 int arg = 10;
 int *thread_ret = NULL;


  ret = pthread_create(&th,NULL,thread,&arg);
  if(ret!=0){
  printf("create thread error!\n");
  return -1;
}
        
cloud@zw:~$ gcc thread.c -o thread -lpthread
cloud@zw:~$ ./thread
This is the main process.
This is a  thread and arg=10.
thread_ret=0.

11.线程的合并与分离

线程的合并就是回收线程资源,以免出现资源泄露的问题。

当一个进程或线程调用了针对其它线程的pthread_join()接口,就是线程合并了。这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。

当被合并线程结束,pthread_join()接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。

与线程合并相对应的另外一种线程资源回收机制是线程分离,调用接口是pthread_detach()。线程分离是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得pthread_detach()接口只要拥有一个参数就行了,那就是被分离线程句柄。

线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕。

12.两个多线程编程的例子和实现

12.1) Linux里编写成多线程序,输出各线程号。

设计思路:今天看了老师的程序,明白了关于线程号输出函数pthread_self().很简单,我主要把多线程程序的主线程和子线程号进行输出。在过程中碰到问题,主线程号输出了,子线程号没有输出。仔细一想,主线程执行完了,子线程还没来得及执行就结束了。怎么办呢?sleep函数,让主线程等待10s,最终达到预期目的。

程序如下:

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

void  * thread(void *arg)
{
        printf("thread id is %lu.\n",pthread_self());
        return NULL;
}

int main()
{
pthread_t id;
printf("Main thread id is %lu \n",pthread_self());
if(!pthread_create(&id,NULL,(void *)thread,NULL))
{
printf("succeed!\n");
sleep(10);
return 0;
}
else
{printf("Fail to Create Thread");
return -1;
}
}

运行结果:

注:编译的时候要链接库:gcc -o thread1 thread1.c -lpthread

12.2) 利用多线程计算pi值。

设计思路:这里利用中值积分定理来计算π值。我的思路就是把线程的执行任务分成四段,,每个线程执行一段,期间遇到的问题就是,线程传递的参数,有些没计算完,来不及传递过去,最终线程结束,我的做法就是让每个线程创建的时候,等待1s.最终达到我的预期结果。

程序如下:

#include <stdio.h>  
#include<pthread.h>  
static long num_steps=100000;   
const int numThreads = 4;  
double step, pi;  
pthread_mutex_t mut; 
double sum = 0.0;  
  
  
void * thread(void *pArg)   
{    
   double x;  
   int  i ;  
   int temp = *((int *)pArg);  
   int start = temp*(num_steps/4);  
   int end = start + num_steps/4;  
   printf("%d %d %d\n",temp,start,end);  //测试程序,可以去掉
   for (i=start; i<end; i++){  

      pthread_mutex_lock(&mut);
      x = (i+0.5)*step;  
      sum = sum + 4.0/(1.0 + x*x); 
      pthread_mutex_unlock(&mut); 
   }  
     
   return 0;   
}  
  
void main()  
{    
   int i;  
   pthread_t  hThread[numThreads];  
   pthread_mutex_init(&mut,NULL);
   step = 1.0/(double) num_steps;  
       
   for(i=0;i<numThreads;i++)  
   {  
      hThread[i] = pthread_create(&hThread[i] ,NULL,thread, &i);
      sleep(1);    
   }  
   
//  for(i=0;i<numThreads;i++){

  // pthread_join(hThread[i],NULL);
   
 //  }
  
   pi = step * sum;
   printf("Pi = %12.9f\n",pi);
   
     
}  

运行结果:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券