C语言--Linux多线程pthread

简介

Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序example1.c。

数据类型:

pthread_t:线程句柄
pthread_attr_t:线程属性

线程操纵函数(简介起见,省略参数):

pthread_create():创建一个线程
pthread_exit():终止当前线程
pthread_cancel():中断另外一个线程的运行
pthread_join():阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init():初始化线程的属性
pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate():获取脱离状态的属性
pthread_attr_destroy():删除线程的属性
pthread_kill():向线程发送一个信号

同步函数:

用于 mutex 和条件变量
pthread_mutex_init() 初始化互斥锁
pthread_mutex_destroy() 删除互斥锁
pthread_mutex_lock():占有互斥锁(阻塞操作)
pthread_mutex_trylock():试图占有互斥锁(不阻塞操作)。当互斥锁空闲时将占有该锁;否则立即返回
pthread_mutex_unlock(): 释放互斥锁
pthread_cond_init():初始化条件变量
pthread_cond_destroy():销毁条件变量
pthread_cond_wait(): 等待条件变量的特殊条件发生
pthread_cond_signal(): 唤醒第一个调用pthread_cond_wait()而进入睡眠的线程
Thread-local storage(或者以Pthreads术语,称作 线程特有数据):
pthread_key_create(): 分配用于标识进程中线程特定数据的键
pthread_setspecific(): 为指定线程特定数据键设置线程特定绑定
pthread_getspecific(): 获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中
pthread_key_delete(): 销毁现有线程特定数据键

与一起工作的工具函数:

pthread_equal(): 对两个线程的线程标识号进行比较
pthread_detach(): 分离线程
pthread_self(): 查询线程自身线程标识号

1、实例

/* example.c*/
#include <stdio.h>
#include <pthread.h>

void thread(void) 
{ 
  int i; 
  for(i=0;i<3;i++) 
    printf("This is a pthread.\n"); 
}

 int main(void) 
{ 
  pthread_t id;
  int i,ret;

    // 成功返回0,错误返回错误编号
  ret=pthread_create(&id,NULL,(void *)thread,NULL); 
  if(ret!=0){ 
    printf ("Create pthread error!\n"); 
    exit (1); 
  } 

  for(i=0;i<3;i++) 
    printf("This is the main process.\n"); 
    
  pthread_join(id,NULL); 
  return (0); 

} 

我们编译此程序:
gcc example1.c -lpthread -o example1

说明:由于pthread库不是标准linux库, 在编译时需加上 -lpthread 。
运行example1,我们得到如下结果:

 This is the main process. 
 This is a pthread. 
 This is the main process. 
 This is the main process. 
 This is a pthread. 
 This is a pthread. 
 再次运行,我们可能得到如下结果: 
 This is a pthread. 
 This is the main process. 
 This is a pthread. 
 This is the main process. 
 This is a pthread. 
 This is the main process.

前后两次结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数pthread_create和pthread_join,并声明了一个pthread_t型的变量

1, pthread_t

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

 typedef unsigned long int pthread_t;

它是一个线程的标识符。

2, pthread_create

函数pthread_create用来创建一个线程,它的原型为:

  extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
  void *(*__start_routine) (void *), void *__arg));

第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。

3, pthread_join; pthread_exit

函数pthread_join用来等待一个线程的结束。函数原型为:

  extern int pthread_join __P ((pthread_t __th, void **__thread_return));

第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是像我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为:

  extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));

唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给 thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create,pthread_join和pthread_exit。下面,我们来了解线程的一些常用属性以及如何设置这些属性。

2、互斥锁mutex

互斥锁用来保证一段时间内只有一个线程在执行一段代码。示例:

#include <pthread.h>
#include <stdio.h>
#include <sys/time.h>
#include <string.h>
#define MAX 10

pthread_t thread[2];
pthread_mutex_t mut;
int number=0, i;

void thread1()
{
        printf ("thread1 : I'm thread 1,starting...\n");

        for (i = 0; i < MAX; i++)
        {
                printf("thread1 : number = %d\n",number);
                pthread_mutex_lock(&mut);
                        number++;
                pthread_mutex_unlock(&mut);
                sleep(2);
        }

        pthread_exit(NULL);
}

void thread2()
{
        printf("thread2 : I'm thread 2,starting...\n");

        for (i = 0; i < MAX; i++)
        {
                printf("thread2 : number = %d\n",number);
                pthread_mutex_lock(&mut);
                        number++;
                pthread_mutex_unlock(&mut);
                sleep(3);
        }

        pthread_exit(NULL);
}

void thread_create(void)
{
        int temp;
        memset(&thread, 0, sizeof(thread));     //comment1
        /*创建线程*/
        if((temp = pthread_create(&thread[0], NULL, (void *)thread1, NULL)) != 0)  //comment2     
                printf("create thread1 failed!\n");
        else
                printf("create thread1 successed!\n");

        if((temp = pthread_create(&thread[1], NULL, (void *)thread2, NULL)) != 0)  //comment3
                printf("create thread failed!\n");
        else
                printf("create thread2 successed!\n");
}

void thread_wait(void)
{
        /*等待线程结束*/
        if(thread[0] !=0)
           {             
                //comment4    
                pthread_join(thread[0],NULL);
                printf("thread1 end !\n");
          }
        if(thread[1] !=0) 
           {  
                //comment5
                pthread_join(thread[1],NULL);
                printf("thread2 end !\n");
         }
}

int main()
{
        /*用默认属性初始化互斥锁*/
        pthread_mutex_init(&mut,NULL);
        thread_create();
        thread_wait();

        return 0;
}

一 pthread_mutex_init

函数pthread_mutex_init用来生成一个互斥锁。NULL表明使用默认属性。如需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数 pthread_mutexattr_settype用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值, PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_ PRIVATE。后者用来设置互斥锁类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、 PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,一般情况下,选用最后一个默认属性。

**二 pthread_mutex_lock;pthread_mutex_unlock **

pthread_mutex_lock声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。

注意:

1 需要说明的是,上面的两处sleep不光是为了演示的需要,也是为了让线程睡眠一段时间,让线程释放互斥锁,等待另一个线程使用此锁。因为在linux下好像没有pthread_delay_np那个函数(我试了一下,提示没有定义该函数的引用),所以我用了sleep来代替,好像也可以通过pthread_cond_timedwait来代替。

2 请千万要注意里头的注释comment1-5。如果没有comment1和comment4,comment5,将导致在pthread_join的时候出现段错误,另外,上面的comment2和comment3是根源所在,所以千万要记得写全代码。因为上面的线程可能没有创建成功,导致下面不可能等到那个线程结束,而在用pthread_join的时候出现段错误(访问了未知的内存区)。另外,在使用memset的时候,需要包含string.h头文件哦

上述执行并输出如下:

这里写图片描述

3、条件变量 pthread_cond_wait

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

1. 创建和注销

条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER

动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)

2. 等待和激发

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 struct timespec *abstime)

等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。

无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。

激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。

下面的例子用队列和条件变量构造一个类似生产者消费者模型。这个模型虽然简单,但是很强大

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
 
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 
struct node {
    int n_number;
    struct node *n_next;
} *head = NULL;
 
/*[thread_func]*/
static void cleanup_handler(void *arg)
{
    printf("thread 2:cleanup handler.\n");
    free(arg);
    (void)pthread_mutex_unlock(&mtx);
}

static void *thread_func(void *arg)
{
    struct node *p = NULL;
 
    pthread_cleanup_push(cleanup_handler, p);    // 拓展https://blog.csdn.net/longbei9029/article/details/72871714
    while (1) 
	{
		pthread_mutex_lock(&mtx);           //这个mutex主要是用来保证pthread_cond_wait的并发性
		while (head == NULL)   
		{               //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。这个时候,应该让线程继续进入pthread_cond_wait
			pthread_cond_wait(&cond, &mtx);         // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
													//用这个流程是比较清楚的/*lock-->unlock-->wait() return-->lock*/
		}
        p = head;
        head = head->n_next;
        printf("thread 2:Got %d from front of queue\n", p->n_number);
        free(p);
        pthread_mutex_unlock(&mtx);             //临界区数据操作完毕,释放互斥锁
    }
    pthread_cleanup_pop(0);
    return 0;
}
 
int main(void)
{
    pthread_t tid;
    int i;
    struct node *p;
    pthread_create(&tid, NULL, thread_func, NULL);   //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大
    /*[tx6-main]*/
    for (i = 0; i < 10; i++) {
        p = malloc(sizeof(struct node));
        p->n_number = i;
        printf("thread 1:Set %d to queue\n", p->n_number);
        pthread_mutex_lock(&mtx);             //需要操作head这个临界资源,先加锁,
        p->n_next = head;
        head = p;
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mtx);           //解锁
        sleep(1);
    }
    printf("thread 1:wanna end the line.So cancel thread 2.\n");
    pthread_cancel(tid);             //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。关于取消点的信息,有兴趣可以google,这里不多说了
    pthread_join(tid, NULL);
    printf("thread 1:all done -- exiting\n");
    return 0;
}

说明:

void pthread_cleanup_push(void (*rtn)(void *), void *arg); 
函数执行压栈清理函数的操作,

void pthread_clean_pop(int execute); 
函数执行从栈中删除清理函数的操作。

pthread_cancel(pthread_t tid); 
"线程取消函数"即线程被取消或者下面描述的情况发生时自动调用的函数。它一般用于释放一些资源,比如释放锁,以免其它的线程永远也不能获得锁,而造成死锁。

在下面三种情况下,pthread_cleanup_push()压栈的"清理函数"会被调用:
1, 线程调用pthread_exit()函数,而不是直接return.
2, 响应取消请求时,也就是有其它的线程对该线程调用pthread_cancel()函数。
3, 本线程调用pthread_cleanup_pop()函数,并且其参数非0
注意:
1.当pthread_cleanup_pop()函数的参数为0时,仅仅在线程调用pthread_exit函数或者其它线程对本线程调用pthread_cancel函数时,才在弹出"清理函数"的同时执行该"清理函数"。
2.注意pthread_exit终止线程与线程直接return终止线程的区别,调用return函数是不会在弹出"清理函数"的同时执行该"清理函数的。
3 .pthread_cleanup_push()函数与pthread_cleanup_pop()函数必须成对的出现在同一个函数中。

4、pthread_join和pthread_detach的用法

1:join

join是三种同步线程的方式之一。另外两种分别是互斥锁(mutex)和条件变量(condition variable)。 调用pthread_join()将阻塞自己,一直到要等待加入的线程运行结束。 可以用pthread_join()获取线程的返回值。 一个线程对应一个pthread_join()调用,对同一个线程进行多次pthread_join()调用是逻辑错误。

join or detach
线程分两种:一种可以join,另一种不可以。该属性在创建线程的时候指定。
joinable线程可在创建后,用pthread_detach()显式地分离。但分离后不可以再合并。该操作不可逆。 为了确保移植性,在创建线程时,最好显式指定其join或detach属性。似乎不是所有POSIX实现都是用joinable作默认。

2: pthread_detach

创建一个线程默认的状态是joinable, 如果一个线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process,即还有一部分资源没有被回收(退出状态码),所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源(类似于wait,waitpid)
但是调用pthread_join(pthread_id)后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此,比如在Web服务器中当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接),这时可以在子线程中加入代码 pthread_detach(pthread_self()) 或者父线程调用 pthread_detach(thread_id)(非阻塞,可立即返回),这将设置子线程的状态为detached, 则子线程运行结束后会自动释放所有资源。


版权声明:本文为qq_15437629原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。