Linux C编程实战之线程控制学习记录

1、线程概述

线程是计算机中独立运行的最小单位,运行时占用很少的系统资源。在用户看来,多个线程是同时执行,但从操作系统调度来看,各个线程是交替执行。系统不停的在各个线程之间切换,每个线程只有在系统分配给它的时间片内才能取得CPU的控制权,执行线程中的代码。(对于单CPU单核的情况)

那么为什么在支持多进程的情况下又引入多线程呢?

  • 节约资源,节约时间。与每个进程都有独立的地址空间不同,同一进程内的线程共享进程的地址空间,故创建新线程花费时间少,线程间的切换速度也比进程快。
  • 可以提高应用程序的响应速度。
  • 可以提高多处理器的效率
  • 可以改善程序的结构

虽然线程在进程内部共享地址空间,打开的文件描述符等资源,但线程也有其私有的数据信息,包括:

  • 线程号(thread ID)
  • 寄存器 [程序计数器,堆栈指针]
  • 堆栈
  • 信号掩码
  • 优先级
  • 线程的私有存储空间

2、创建线程

线程的创建通过函数 pthread_create 来完成,该函数的声明如下:

#incldue<pthread.h>
int pthread_creat(pthred_t *thread,pthread_attr_t *attr,
                  void *(*start_routine)(void *), void arg);

其作用是:创建线程号为thread,线程属性为attr,执行参数为arg的start_routine函数的线程。

新创建的线程去运行指针指向的函数,而原线程继续运行。

创建线程其他系统函数:

函数说明
pthread_t pthread_self(void)类似于getpid(),获取线程自身线程ID
int pthread_equal(pthread_t thread1,pthread_t thread2)判断两个进程是否为同一进程
int pthread_once(pthread_once_t * once_control,void(*int_routine)(void))保证该函数仅执行一次

下面来看看如何创建进程,如 createThread.c

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

int *get_thid(void);

int main(int argc,char **argv)
{
    pthread_t thid;                     //声明进程ID变量

    printf("parent pthread is me,my thid is %lu\n",pthread_self( ));
    if(pthread_create(&thid,NULL,(void *)get_thid,NULL) != 0)
    {
        printf("Error!\n");                 //调用函数进行进程的创建
        return 0;
    }
    sleep(1);
    return 0;
}
int *get_thid(void)                      //创建进程时,被调用的函数
{
    pthread_t thid;

    thid = pthread_self( );
    if(thid < 0)
    {
        printf("Error!\n");
        exit(0);
    }
    printf("I'm child pthread,my thid is %lu\n",thid);
    return NULL;
}

运行结果如下:

image-20210213172539505

由于pthread库不是标准linux库, 需在编译命令后面添加 -lpthread

3、线程终止

在Linux环境下,有两种方式实现线程的终止

  • 调用return函数,实现线程终止
  • 使用POSIX标准的接口API,pthread_exit函数

这两个函数主要的区别之处在于在主线程中调用的区别:

在主线程中调用return/exit,会使主线程结束,进而整个线程结束,全部线程消亡

如果是调用pthread_exit( )函数,则主线程消亡后,其他线程并不会受到影响,知道所有线程结束,进程才会结束

线程的终止时最重要的问题就是关于资源的释放问题,特别是一些临界资源

临界资源在同一时间只能被其中一个线程所使用,如若被多个线程使用,则会导致资源混乱。而如果临界资源给一个线程所使用,该线程退出时没有释放临界资源,则其他线程会一直认为该临界资源还在被其他线程所占用,就会导致死锁问题的出现。死锁问题的出现,在程序设计的过程中,往往是灾难性的,所以为了妥善处理线程结束时临界资源的释放问题,Linux系统提供了一对函数:pthread_cleanup_push()、pthread_cleanup_pop()用于自动释放资源。

#include<pthread.h>
#define pthread_cleanup_push(routine ,arg) \
{
    struct _pthread_cleanup_buffer buffer; \
          _pthread_cleanup_push(&buffer,(routine),(arg));
#dedine pthread_cleanup_pop  \
         _pthread_clean_pop(&buffer,(exeute));
}

线程终止时另外一个要注意的问题是线程间的同步问题

一般情况下,进程中各个线程的运行是相互独立的,线程的终止并不会相互通知,终止的线程资源仍归线程独有。所以资源的同步十分重要,同进程中的wait函数,在线程中所使用的是pthread_join( )函数,其声明如下:

#include<pthred.h>
void phread_exit(void *retval);
int pthread_join(pthread_t thid,void *thread_return);
int pthread_detach(pthread_t thid);

函数pthread_join用来使调用者挂起等待thid线程的结束

注意一个线程只能被另一个线程所等待,若被多个线程等待,其中一个线程恢复恢复就绪状态后,其他线程便进入了死锁,并且被等待的线程必须处于可join的状态,即它不能被设定为DETACHED(处于DETACHED状态的线程是指内核不关心线程返回值,线程结束后,内核自动回收的分离模式)所以,为了防止内存泄漏,并且完成线程同步,所有的线程结束时,都要设定为DETACHED或者 pthread_join( )等待

如以下线程终止实例 jointhread.c

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

void test(void);

int main(int argc,char **argv)
{
    pthread_t thid;
    int status;

    pthread_create(&thid,NULL,(void *)test,NULL);
    pthread_join(thid,(void *)&status);                                     //使主线程进行阻塞,等待子线程结束
    printf("I  (%lu)  have waited for a long time  %d",pthread_self( ) ,status);        

    return 0;
}

void test(void)
{
    printf("I am for test !\n");
    sleep(20);                                                  //用sleep来延时函数
    printf("I have achieved!\n");

    pthread_exit(0) ;
}

运行结果如下:

image-20210213213237481

以上结果即可看出,调用函数对目标函数完成了挂起等待。

4、私有数据

区别于之前提到的私有的数据信息,此处私有数据指的是多个线程中操作不同的数据。不同的线程对私有数据的访问对彼此之间是不可见的,操作互不影响,即键同名且全局但访问内存空间不同。

在这里举一个特殊的的例子:errno全局变量,它返回标准的出错代码。理论上errno应该是任何线程都够访问的全局变量,但是如若errno中保存的值还没有被使用,便被其他线程更改了其中的值,同样也会影响使用。像这种全局变量,即是我们此处要讨论的私有数据,即都能访问的全局变量,但是在各个线程中又是不一样的值。

私有数据的实现方式借用了:一键多值。对这个键可以理解为:一个数据管理器,在各个线程中,调用时,键会被告诉在此线程中应该使用什么值。

操作线程私有数据的函数的声明如下:

#include<pthread.h>
int pthread_key_creat (pthread_key_t *key,void (*destr funcation) (void *));
int pthread_setspecific (pthred_key_t *key,const void *pointer);
void *pthread_getspecific (pthread_key_t key);
int pthread_key_delete (pthread_key_t key);
  • Creat函数是用来创建键的
  • setspecific函数用来将线程的私有数据与键绑定,在线程自身中调用
  • getspecific函数用来获取键值中绑定的私有数据
  • delete函数用来销毁键

注意:在pthread_key_creat函数中使用了析构函数。所谓析构函数指的是用来在键值使用完成之后

清除并释放与键值绑定的私有数据所占的内存空间。键值对与私有数据所占用的并不是相同的数据空间,所以要分开进行释放。一旦在键值对释放时,未释放私有数据所占据的空间,则会导致内存泄漏,灾难性的后果。所以调用析构函数有其一定的必要性,当为NULL,会调用内核自身的清理函数。

一般情况下,线程调用malloc为私有数据分配内存空间

示例 tsd.c

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

pthread_key_t key;                //定义全局变量库--键

void *thread1(void *arg);          //线程1

void *thread2(void *arg);          //线程2

int main(void)
{
    pthread_t tid;                 //线程ID

    printf("main thread begins running!\n");
    pthread_key_create(&key,NULL);                        //参数为键地址,以及析构函数(用于私有数据的内存清理),如果为NULL,则调用系统的清理函数
    pthread_create(&tid,NULL,thread1,NULL);               //四个参数依次是线程ID,线程属性,调用函数,函数参数
    sleep(10);                                            //睡眠以使主线程等待
    pthread_key_delete(key);                              //销毁键,私有数据的销毁必须在其之前,不然会内存泄漏
    printf("mian pthread ends \n");

    return 0;
}

void *thread1(void *arg)
{
    int tsd = 5;                                          //pthread中的私有数据
    pthread_t thid_1;                                     //分配新的线程号

    printf("pthread 1  %lu is running!\n",pthread_self(  ));
    pthread_setspecific(key,(void *)tsd);                        //使键与私有数据绑定
    pthread_create(&thid_1,NULL,thread2,NULL);            //创建新线程
    printf("thread1 %lu ends,pthread's tsd is %d\n",pthread_self(  ),pthread_getspecific(key));
    sleep(5);                                            //睡眠以等待新线程结束

}

void *thread2(void *arg)
{
    int tsd = 0;

    printf("pthread 2 %lu is running\n",pthread_self(  ));
    pthread_setspecific(key,(void *)tsd);                       //绑定键值与私有数据
    printf("Thread %lu ends,thread's tsd is %d\n",pthread_self(  ),pthread_getspecific(key));

}

运行结果如下:

image-20210213232526858

5、线程同步

线程最大的特点是资源的共享性,其中的同步问题十分重要。以下是Linux中处理同步问题的常用方式。

5.1、互斥锁

互斥锁通过锁机制来实现线程间的同步,在同一个时刻它通常只允许一个线程执行一个关键部分的代码。

1、使用互斥锁前必须先进行初始化操作。初始化有两种方式,一种是静态赋值法,将宏结构常量赋给互斥锁,另外一种方式是通过pthread_mutex_init函数初始化互斥锁。

2、初始化后就可以给给互斥锁加锁了。加锁有两个函数:pthread_mutex_lock()pthread_mutex_trylock()。用pthread_mutex_lock()加锁的时候,如果mutex已经被锁住,当前尝试加锁的进程就会阻塞,直到互斥锁被其他线程释放,当pthread_mutex_lock函数返回时,说明互斥锁已经被当前进程成功加锁。pthread_mutex_trylock函数则不同,如果mutex已经被加锁,它将立即返回,返回的错误码为EBUSY,而不是阻塞等待。

3、用pthread_mutex_unlock函数解锁时,要满足两个条件:一是互斥锁必须处于加锁状态,二是调用本函数的线程必须是给互斥锁加锁的线程。解锁后如果有其他线程在等待互斥锁,等待队列中的第一个将获得互斥锁

4、当一个互斥锁使用完毕后,必须进行清除,清除互斥锁使用函数pthread_mutex_destroy
清除一个互斥锁意味着释放它所占用的资源。清除锁时要求当前处于开放状态,若锁处于锁定状态,函数放回EBUSY,该函数成功之行时返回0。由于在Linux中,互斥锁并不占用内存,因此pthread_mutex_destroy()除了解除互斥锁的状态外没有其他操作。

5.2、条件变量

条件变量是利用线程见共享的全局变量进行同步的一种机制。条件变量宏观上类似if语句,符合条件就能执行某段程序,否则只能等待条件成立。

使用条件变量主要包括两个动作:一个等待使用资源的线程等待”条件变量被设置为真”;另一个线程在使用完资源后”设置条件为真”,这样就可以保证线程间的同步了。这样就存在一个关键问题,这就是要保证条件变量能被正确的修改,条件变量要受到特殊的保护,实际使用中互斥锁扮演者这样一个保护者的角色。Linux也提供了一系列对条件变量操作的函数。

1、与互斥锁一样,条件变量的初始话也有两种方式,一种是静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER赋给互斥锁。另一种方式是使用函数pthread_cond_init

2、pthread_cond_wait函数释放有mutex指向的互斥锁,同时使当前线程关于cond所指向的条件变量阻塞,直到条件被信号唤醒。通常条件表达式在互斥锁的保护下求值,如果条件表达式为假,那么线程基于条件变量阻塞。当一个线程改变条件变量的同时,条件变量获得一个信号,使得条件变量的线程退出阻塞状态。
pthread_cond_timedwait函数和pthread_cond_wait函数用法类似,差别在于pthread_cond_timedwait函数将阻塞直到条件变量获得信号或者经过abstime指定的时间,也就是说,如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。

3、线程被条件变量阻塞后,可以通过函数pthread_cond_signalpthread_cond_broadcast激活。
pthread_cond_signal激活一个等待条件成立的线程,存在多个等待线程时,按入队顺序激活其中一个,而pthread_cond_broadcast则激活所有等待线程。

4、当一个条件变量不再使用时,需要将其清除。清除一个条件变量通过调用pthread_cond_destroy()实现。pthread_cond_destroy函数清除由cond指向的条件变量。注意:只有在没有线程等待该条件变量的时候才能清除这个条件变量,否则返回EBUSY。

示例代码 condition.c

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

pthread_mutex_t mutex;
pthread_cond_t cond;

void hander(void *arg)
{
    free(arg);
    (void)pthread_mutex_unlock(&mutex);
}

void *thread1(void *arg)
{
    pthread_cleanup_push(hander, &mutex);
    while (1) {
        printf("thread1 is running\n");
        pthread_mutex_lock(&mutex); //条件变量使用时配合互斥锁使用
        pthread_cond_wait(&cond,&mutex);
        printf("thread1 applied the condition\n");
        pthread_mutex_unlock(&mutex);
        sleep(4);
    }
    pthread_cleanup_pop(0);
}

void *thread2(void *arg)
{
    while (1) {
        printf("thread2 is running\n");
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        printf("thread2 applied the condition\n");
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

int main()
{
    pthread_t thid1,thid2;
    printf("condition variable study!\n");
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_create(&thid1,NULL,thread1,NULL);
    pthread_create(&thid2,NULL,thread2,NULL);

    sleep(1);
    do {
        pthread_cond_signal(&cond);
    } while(1);

    sleep(20);
    pthread_exit(0);
    return 0;
}

5.3、异步信号

在Linux系统中,线程是在内核外实现的,它不像进程那样在内核中实现,Linux线程本质上是轻量级的进程。信号可以被进程用来进行相互通信,一个进程通过信号通知另一个进程发生了某件事件,比如该进程所需要的输入数据已经就绪。线程同进程一样也可以接收和处理信号,信号也是一种线程同步的手段。

信号于任何线程都是异步的,也就是说信号到达线程的时间是不定的。如果有多个线程可以接收异步信号,则只有一个被选中,如果并发的多个同样的信号被送到一个进程,每一个将被不同的线程处理,如果所有的线程都屏蔽该信号,则这些信号将被挂起,直到有信号解除屏蔽来处理它们。其中函数pthread_kill用来向特定的线程发送信号signal,函数pthread_sigmask用来设置线程的信号屏蔽码,但对不允许屏蔽的Cancel信号和不允许相应的Restart信号进行了保护,函数sigwait用来阻塞线程。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值