Linux C 多线程编程

进程&&线程


什么是进程
  • 程序的一个执行实例
  • 资源分配、调度运行的基本单位
什么是线程
  • “一个进程内部的控制序列”
  • 程序执行(CPU调度)的基本单位
进程&&线程联系
  • 一个进程的所有信息对该进程的所有线程都是共享的,包括:可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符
  • 一个进程中可以包含多个线程(至少一个),并且线程共享进程的资源
  • 创建进程和线程的底层函数都是clone()
  • 进程结束后它所拥有的所有线程都将销毁;而线程的结束不会影响同个进程中的其他进程的结束
  • 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,操作系统中的执行功能都是创建线程去完成的
进程&&线程函数调用
进程 线程
进程ID在整个系统中是唯一的 线程ID只有在它所属的进程上下文中才有意义
pid_t:进程ID的类型 pthread_t:线程ID的类型
fork():创建一个新进程(由父进程创建子进程) pthread_create():创建一个新的线程(任何一个线程都可以创建新的线程)
waitpid():父进程等待子进程中断或结束 pthread_join():等待一个线程的结束
signal(SIG_IGN):避免僵尸进程问题,子进程结束后资源由内核回收 pthread_detach():分离线程,线程结束后资源由系统回收
exit():结束当前进程 pthread_exit():结束调用它的线程(自杀)
kill():杀死进程 pthread_cancel():终止同一线程中的另一线程(他杀)
getpid():获得进程号 pthread_self():获得线程自己的线程号

注意:pthread_cancel()调用成功返回0,失败返回非0值;成功并不代表线程会结束。若是在整个程序退出时要终止各个线程,应该在成功发送cancel指令后,使用使用 pthread_join() 函数,等待指定的线程已经完全退出以后,再继续执行;否则,很容易产生 “段错误”。

线程的简单编程


创建新线程
//函数原型
int pthread_create
(pthread_t *thread, //线程ID,pthread_create()函数成功返回时,新创建线程的ID会被设置成thread所指向的内存单元
const pthread_attr_t *attr, //线程属性(常用的有8个,可设置为NULL)
void *(*start_routine) (void *),//线程处理函数
void *arg //线程处理函数的参数
);

//线程函数创建时,并不能确定哪个线程先运行:是新创建的线程还是调用的线程。

//创建一个新的线程(需要引<pthread.h>头文件)
void* rout(void* arg)
{
    while(1)
    {
        printf("the thread 1\n");
        sleep(1);
    }
}

int main(void)
{
    pthread_t tid
    int ret;
    if((ret=pthread_create(&tid,NULL,rout,NULL))!=0)
    {
        fprintf(stderr,"pthread_create : %s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }

   while(1)
    {
        printf("the main thread\n");
        sleep(1);
    }
}
  • 程序运行结果如下所示:
the main thread
the thread 1
the main thread
the thread 1
the main thread
the thread 1
……
获取线程ID
pthread_t pthread_self(void);

利用主线程创建新线程,打印进程ID和线程ID,代码如下:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
pthread_t ntid;

void print(char *s) 
{
    pthread_t tid;
    pid_t pid;
    tid=pthread_self();
    pid=getpid();
    printf("%s pid %lu tid %lu\n",s,(unsigned long)pid,(unsigned long)tid);           
}


void *rout(void *arg)
{
    print("new thread:");
    return ((void *)0);
}
int main(void)
{
    int ret;
    if((ret=pthread_create(&ntid,NULL,rout,NULL))!=0)
    {
        fprintf(stderr,"pthread_create:%s\n",strerror(ret));
        exit(EXIT_FAILURE);
    }
    print("main thread:");
    sleep(1);
}               

运行结果如下:

main thread: pid 18360 tid 3078538944
new thread: pid 18360 tid 3078536048
线程终止
  • 从线程函数return
  • 调用pthread_exit()函数终止自己
  • 一个线程可以调用pthread_cancel终止同一进程中的令一进程
//线程终止
void pthread_exit(void *retval);

注意 : return和pthread_exit()返回的指针所指向的内存单元必须是全局的or用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到之后返回指针时线程函数已经退出了

//取消同一进程中的其它进程
int pthread_cancel(pthread_t thread);
线程等待

为什么线程需要等待?

  • 已经退出的线程,其空间没有释放,任然在进程的地址空间内
  • 创建新的线程不会复用刚才退出线程的地址空间
//等待线程结束(但不获取线程的终止状态)
int pthread_join(
pthread_t thread, //线程ID
void **retval //retval指向一个指针,后者指向线程的返回值
);

//调用此函数的线程将一直阻塞,直到指定的线程调用pthread_exit(),直接从启动程中return或者取消

线程的三种终止方式以及获取各自的终止码实例

void* thread1(void*arg)
{
    printf("thread 1 returning ...\n");
    int *p=(int *)malloc(sizeof(int));
    *p=1;
    return (void*)p;
}

void* thread2(void*arg)
{                                                       
    printf("thread 2 exiting ...\n");
    int *p=(int *)malloc(sizeof(int));
    *p=2;
    pthread_exit((void*)p);
}

void* thread3(void* arg)
{
    while(1)
    {
        printf("thread 3 is running...\n");
        sleep(1);
    }
    return NULL;
}

int main(void)
{
    pthread_t tid;
    void* ret;
    //thread 1 return
    pthread_create(&tid,NULL,thread1,NULL);
    pthread_join(tid,&ret);
    printf("thread return,thread id %x,return code:%d\n",tid,*(int*)ret);
    free(ret);

    //thread 2 exit           
    pthread_create(&tid,NULL,thread2,NULL);
    pthread_join(tid,&ret);
    printf("thread return,thread id %x,return code:%d\n",tid,*(int*)ret);
    free(ret);
    //thread 3 cancel by other
    pthread_create(&tid,NULL,thread3,NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid,&ret);
    if(ret==PTHREAD_CANCELED)
        printf("thread return,thread id %x,return code:PTHREAD_CANCELED\n",tid);
    else
        printf("thread return,thread id %x,return code:NULL\n",tid);
    return 0;
 61 }                        

运行结果如下所示。可以看到,当一个线程调用pthread_exit()退出或者是直接return返回时,进程中的其他线程可以通过调用pthread_join()函数获得该进程的退出状态。

thread 1 is returning...
thread return,thread id : B779EB70,thread code : 1
thread 2 is exiting...
thread return,thread id : B779EB70,thread code: 2
thread 3 is running...
thread 3 is running...
thread 3 is running...
thread return,thread id : B779EB70,thread code:PTHREAD_CANCELRD

调用该函数的线程将挂起等待,只到ID为thread的线程终止。thread线程以不同的方法终止,pthread_join()函数得到的终止状态是不同的,总结如下:

  • 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值;
  • 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED;
  • 如果thread线程是自己调用pthread_exit()终止的,retval所指向的单元存放的是传给pthread_exit()函数的参数;
  • 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
线程分离

如果设置线程分离,那么线程的底层存储资源在线程结束时会立即回收。在线程被分离后,我们不能用pthread_join()函数等待它的终止状态,因为对分离的线程调用pthread_join()函数会产生未定义行为。

//函数原型
int pthread_detach(pthread_t thread);

线程的属性


//线程属性结构如下:  
typedef struct  
{  
    int                   etachstate;      //线程的分离状态  
    int                   schedpolicy;     //线程调度策略  
    structsched_param     schedparam;      //线程的调度参数  
    int                   inheritsched;    //线程的继承性  
    int                   scope;           //线程的作用域  
    size_t                guardsize;       //线程栈末尾的警戒缓冲区大小  
    int                   stackaddr_set;   //线程的栈设置  
    void*                 stackaddr;       //线程栈的位置  
    size_t                stacksize;       //线程栈的大小  
}pthread_attr_t;  

线程的属性不能直接设置,须用相关函数进行操作,初始化函数为pthread_attr_init(),这个函数必须在pthead_create()函数之前调用。之后必须用pthead_attr_destory()函数来释放资源。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。

线程的常用属性(三个)

线程的分离状态(datached state)

  • 非分离状态(线程默认属性)。此状态下,原有的线程等待创建的线程结束,只有当pthread_join()函数返回时,创建的线程才算结束,才能释放自己占用的系统资源。
  • 分离状态。分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
int pthread_attr_init(pthread_attr_t *attr);//初始化线程属性
int pthread_attr_destory(pthread_attr_t *attr);//销毁线程属性
int pthread_attr_setdetachstate(
pthread_attr_t *attr, //要设置分离状态的线程ID
int detachstate //可选的:PTHREAD_DETACHED(分离状态);PTHREAD_CREATE_JOINABLE(非分离线程)
);

注意 :如果一个线程设置为分离线程且这个线程运行的非常快,则它很有可能在pthread_create()函数返回之前返回,这样如果有新的线程调用pthread_create()函数就会的到错误的线程号。

线程的栈地址(stack address)

  • POSIX定义了两个常量——POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性
  • 给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测系统是否支持栈属性
  • 当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr()和pthread_attr_getstackaddr()两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)

线程的栈保护区的大小(stack duard size)

  • 在线程栈顶留出一段空间,防止栈溢出
  • 当栈指针进入这段保护区时,系统会发出错误,通常是发送信号给线程
  • 该属性默认值是PAGESIZE大小,该属性被设置是,系统会自动将该属性大小补齐为页大小的整数倍
  • 当改变栈地址属性时,占保护区大小通常清零

更多线程属性相关内容可以阅读这篇文章:http://blog.csdn.net/zsf8701/article/details/7842392

线程其他常用函数
  • 线程中获得线程ID:pthread_t pthread_self(void)
  • 线程分离:pthread_detach()
  • 比较两个线程是否相等:int pthread_equal(pthread_t t1, pthread_t t2);

线程的同步&&互斥

互斥量
  • 互斥量从本质上来说是一把锁,在访问共享资源前对互斥量进行上锁,在访问完成后释放互斥量
  • 对互斥量进行加锁之后,任何其他视图再次对互斥加锁的线程都会被阻塞直到当前线程释放该互斥量
  • 如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量进行上锁,其他线程只能继续等待
//互斥量的常用接口
1.pthread_mutex_init(&mutex,NULL)//初始化
2.int pthread_mutex_lock(pthread_mutex_t * mutex)//上锁
3.int pthread_mutex_unlock(pthread_mutex_t * mutex)//解锁
4.int pthread_mutex_destroy(pthread_mutex_t *mutex) //销毁

//模拟售票系统
##include<stdio.h>                                                                              
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
#include<sched.h>

int ticket=100;
pthread_mutex_t mutex;

void *rout(void *arg)
{
    char *id=(char*)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        if(ticket>0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n",id,ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
            //sched_yield();
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}

int main(void)
{
    pthread_t t1,t2,t3,t4;
    pthread_mutex_init(&mutex,NULL);
    pthread_create(&t1,NULL,rout,"thread 1");   
    pthread_create(&t2,NULL,rout,"thread 2");
    pthread_create(&t3,NULL,rout,"thread 3");
    pthread_create(&t4,NULL,rout,"thread 4");
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
    pthread_join(t3,NULL);
    pthread_join(t4,NULL);
    pthread_mutex_destroy(&mutex); 
    return 0;
}      

程序运行结果如下所示:

thread 4 sells ticket:100
thread 4 sells ticket:99
thread 4 sells ticket:98
……
thread 4 sells ticket:3
thread 4 sells ticket:2
thread 4 sells ticket:1

上述结果说明线程4竞争到了互斥量,但是因为该互斥量被上了锁,导致其他线程无法使用此互斥量,这时我们可以用sched_yield()让一个线程主动让出执行权。

运行结果如下:

thread 4 sells ticket:100
thread 3 sells ticket:99
thread 2 sells ticket:98
……
thread 3 sells ticket:3
thread 2 sells ticket:2
thread 1 sells ticket:1

总结:调用pthread_lock()函数时,可能会遇到下面几种情况:

  • 互斥量处于未上锁状态,该函数会将互斥量锁定,返回成功
  • 发起函数调用时,若有线程锁定互斥量,或者有多个线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock()会陷入阻塞,等待互斥量解锁
同步
  • 多个线程(或进程)为了合作完成任务,必须严格按照规定的某种先后次序来运行。例如:一个线程访问队列时,发现队列为空,则它只能等待,直到其它线程将一个节点加入到队列中,才能继续访问。这种情况就需要用到条件变量。
pthread_cond_t cond;//定义一个条件变量
//条件变量初始化
int pthread_cond_init(
    pthread_cond_t *restrict cond,//要初始化的条件变量
    const pthread_condattr_t *restrict attr//NULL
    );
//条件等待函数
int pthread_cond_wait(
    pthread_cond_t *restrict cond,//要在这个条件变量上等待
    pthread_mutex_t *restrict mutex//互斥量
    );

int pthread_cond_signal(
    pthread_cond_t *cond
    );
//销毁条件变量    
int pthread_cond_destroy(
    pthread_cond_t *cond//要销毁的条件变量
    );

条件变量的简单案例

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

pthread_cond_t cond;
pthread_mutex_t mutex;

void *r1(void *arg)
{
    pthread_cond_wait(&cond,&mutex);
    printf("activity\n");
}

void *r2(void *arg)
{
    while(1)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

int main(void)
{
    pthread_t t1,t2;
    pthread_cond_init(&cond,NULL);
    pthread_mutex_init(&mutex,NULL);

    pthread_create(&t1,NULL,r1,NULL);
    pthread_create(&t2,NULL,r2,NULL);

    pthread_join(t1,NULL);
    pthread_join(t2,NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}                    

程序运结果如下:

activity
activity
activity
activity
^C

为什么pthread_cond_wait需要互斥量?

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以不许要有一个线程通过某些操作,改变共享变量,是原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程
  • 条件不会无缘无故的突然变得满足了,必然牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据

条件变量使用规范

1. 等待条件
pthread_mutex_lock(&mutex)
while(条件为假)//如果用if,则wait信号有可能被打断而返回,所以用while再次检测
pthread_cond_wait(&cond,&mutex)
2. 修改条件
pthread_mutex_unlock(&mutex)
3. 让条件满足的代码
pthread_mutex_lock(&mutex)
条件为真
pthread_cond_signal(&cond)//signal发送信息,如果没有wait就会丢失
pthread_mutex_unlock(&mutex)

生产者-消费者模型

生产者消费者问题:两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,将信息放入缓冲区,另一个是消费者,从缓冲区中取信息。

问题1:若缓冲区已满,而此时生产者还想往其中放入一个新的数据。

解决:让生产者睡眠,待消费者从缓冲区中取出一个或多个数据时再唤醒它;

问题2:当消费者试图从缓冲区中取数据而发现缓冲区为空。

解决:让消费者睡眠,直到消费者向其中放一些数据后再将其唤醒。

上述方法可以用互斥量解决,程序代码如下:

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

#define CONSUMERS_COUNT 2
#define PRODUCERS_COUNT 2

struct msg{
    struct msg *next;
    int num;
};

struct msg *head=NULL;

pthread_cond_t cond;
pthread_mutex_t mutex;

pthread_t thread[CONSUMERS_COUNT+PRODUCERS_COUNT];

void *consumer(void *p) 
{
int num=*(int*)p;
    free(p);
    struct msg *mp;
    for(;;)
    {
        pthread_mutex_lock(&mutex);
        while(head==NULL)
        {
            printf("%d begin wait a condition....\n",num);
            pthread_cond_wait(&cond,&mutex);
        }

        printf("%d end wait a condition...\n",num);
        printf("%d begin consume product...\n",num);
        mp=head;
        head=mp->next;
        pthread_mutex_unlock(&mutex);
        printf("consume %d\n",mp->num);
        free(mp);
        printf("%d end consume product..\n",num);
        sleep(rand()%5);
    }                 
    }

void *product(void *p)
{
    struct msg *mp;
    int num=*(int*)p;
    free(p);
    for(;;)
    {
        printf("%d begin produce product...\n",num);
        mp=(struct msg*)malloc(sizeof(struct msg));
        mp->num=rand()%1000+1;
        printf("pruduce %d\n",mp->num);
        pthread_mutex_lock(&mutex);
        mp->next=head;
        head=mp;
        printf("%d end produce product...\n",num);
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        sleep(rand()%5);
    }
}
int main(void)
{
    srand(time(NULL));

    pthread_cond_init(&cond,NULL);
    pthread_mutex_init(&mutex,NULL);

    int i;
    for(i=0;i<CONSUMERS_COUNT;i++)
    {
        int *p=(int*)malloc(sizeof(int));
        *p=i;
        pthread_create(&thread[i],NULL,consumer,(void *)p);
    }

    for(i=0;i<CONSUMERS_COUNT;i++)
    {
        int *p=(int*)malloc(sizeof(int));
        *p=i;
        pthread_create(&thread[i],NULL,product,(void *)p);
    }
    for(i=0;i<CONSUMERS_COUNT+PRODUCERS_COUNT;i++)
        pthread_join(thread[i],NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

运行结果如下所示,生产者消费者进程交替运行,当产品队列中无产品时,消费者需要等待:

1 begin produce product...
pruduce 4
1 end produce product...
0 begin produce product...
pruduce 8
0 end produce product...
1 end wait a condition...
1 begin consume product...
consume 8
1 end consume product..
0 end wait a condition...
0 begin consume product...
consume 4
0 end consume product..
0 begin wait a condition....
1 begin wait a condition....
1 begin produce product...
pruduce 4
1 end produce product...
0 end wait a condition...
0 begin consume product...
consume 4
0 end consume product..

……

读写锁

  • 读写锁与互斥量类似,不过读写锁允许更高的并行性
  • 读写锁可以有三种加锁状态:读模式下加锁、写模式下加锁、不加锁
  • 一次只有一个线程可以占用写模式的读写锁,但是多个线程可以同时占有读模式的读写锁
  • 读写锁也可以叫做“共享互斥锁”。读锁–共享模式锁;写锁–互斥模式锁
int pthread_rwlock_init(
    pthread_rwlock_t *restrict rwlock,
    const pthread_rwlockattr_t *restrict attr
    );

int pthread_rwlock_rdlock(
    pthread_rwlock_t *rwlock
    );
int pthread_rwlock_wrlock(
    pthread_rwlock_t *rwlock
    );
int pthread_rwlock_unlock(
    pthread_rwlock_t *rwlock
    );

int pthread_rwlock_destroy(
    pthread_rwlock_t *rwlock
    );

使用读写锁实现四个线程读写一段程序的实例,共创建了四个新的线程,其中两个线程用来读取数据,另外两个线程用来写入数据。在任意时刻,如果有一个线程在写数据,将阻塞所有其他线程的任何操作。

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


pthread_rwlock_t rwlock;

void *read_t(void *arg)
{
    char *thread_name=(char *)arg;
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("线程%s进入临界区\n",thread_name);
        sleep(1);
        printf("线程%s离开临界区\n",thread_name);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
void *write_t(void *arg)
{
char *thread_name=(char *)arg;
    while(1)
    {
        pthread_rwlock_wrlock(&rwlock);
        printf("线程%s进入临界区\n",thread_name);
        sleep(1);
        printf("线程%s离开临界区\n",thread_name);
        pthread_rwlock_unlock(&rwlock);
        sleep(5);
    }
}

int main(void)
{
    int ret;
    pthread_t thread_read1,thread_read2,thread_write1,thread_write2;

    pthread_rwlock_init(&rwlock,NULL);

    pthread_create(&thread_read1,NULL,read_t,"read_1");
    pthread_create(&thread_read2,NULL,read_t,"read_2");
    pthread_create(&thread_write1,NULL,write_t,"write_1");
    pthread_create(&thread_write2,NULL,write_t,"write_2");

    pthread_join(thread_read1,NULL);
    pthread_join(thread_read2,NULL);
    pthread_join(thread_write1,NULL);
    pthread_join(thread_write2,NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}                

运行结果如下,可以看写锁每次只能有一个进入临界区,而多个读锁可以同时进入临界区,所以说读锁是共享模式锁,写锁是互斥模式锁:

线程write_2进入临界区......
线程write_2离开临界区......
线程write_1进入临界区......
线程write_1离开临界区......
线程read_2进入临界区......
线程read_1进入临界区......
线程read_2离开临界区......
线程read_1离开临界区......
线程write_2进入临界区......
线程write_2离开临界区......
线程write_1进入临界区......
线程write_1离开临界区......
线程read_2进入临界区......
线程read_1进入临界区......
^C

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/79609850