Linux 多线程

一、线程理论基础

1、定义

线程( thread )技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的Unix也支持线程的概念,但是在一个进程( process )中只允许有一个线程,这样多线程就意味着多进程.现在,多线程技术已经被许多操作系统所支持,包括Windows/NT、Linux( thread )。

2、优点

(1)(与进程相比)

  • ①和进程相比,它是一种非常“节俭”的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需的时间也远远小于进程间切换所需要的时间.据统计,一个进程的开销大约是一个线程开销的30倍左右
  • ②线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便

内核(招标公司)

进程(建筑公司)——资源由内核分配

线程(建筑队)——资源来自进程

(2)多线程程序作为一种多任务、并发的工作方式,自身有如下优点:

  • ①使多CPU系统更加有效.操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上.
  • ②改善程序结构.一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改.

二、多线程程序设计

1、头文件

  • Linux系统下的多线程遵循POSIX线程接口,称为pthread。
  • 编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用libpthread.a

2、步骤图解

Linux 多线程_第1张图片

 

3、步骤

(1)创建

#include

int pthread_create(pthread_t * tidp,const pthread_attr_t*attr,void*(*start_rtn)(void),void*arg)

  • tidp∶线程id
  • attr :线程属性(通常为空)
  • start_rtn:线程要执行的函数
  • arg : start_rtn的参数

(2)终止线程

如果进程中任何一个线程中调用exit或_exit,那么整个进程都会终止。

线程的正常退出方式有∶

  • (1)线程从启动例程中返回(对应函数return)
  • (2)线程可以被另一个进程终止(pthread_cance(id));
  • (3)线程自己调用pthread_exit函数(自己退出)
    • #include
    • void pthread_exit(void *rval_ptr)
      • 功能:终止调用线程
      • rval_ptr:线程退出返回值的指针

(3)线程等待

#include

int pthread_join(pthread t_tid,void **rval_ptr)

  • 功能:阻塞调用线程,直到指定的线程终止。
  • Tid :等待退出的线程id
  • rval_ptr:线程退出的返回值的指针
#include 
#include 
#include 
#include 

char message[]="hello world";

//调用的函数
void *tid_work(void * arg)
{
    printf("pthread function in running!Argument is %s\n",(char *)arg);
    sleep(3);
    //复制函数,将Bye复制到message函数内容中去
    strcpy(message,"Bye");
    pthread_exit("Thank you for your CPU time!\n");    
}

int main()
{
    pthread_t tid;
    int ret;
    void *pthread_val;

    ret=pthread_create(&tid,NULL,tid_work,(void *) message);//取线程id的地址,线程属性为空,线程要执行的函数为tid_work,函数的参数为message

    if(ret != 0)//返回值部位0时,创建线程失败,返回exit(-1)
    {
        printf("create pthread error!\n");
        exit(-1);
    }

    //等待第一个线程结束
    printf("Waiting for pthread to finish...\n");

    //返回值为线程等待的返回值
    ret = pthread_join(tid,&pthread_val);

    //当返回值不为0时,等待失败,返回exit(-1)
    if(ret != 0)
    {
        perror("pthread join error!");
        exit(-1);
    }
    printf("pthread joined,it returned %s",(char *)pthread_val);

    printf("message is now:%s\n",message);

    return 0;
}

运行结果:

Linux 多线程_第2张图片

三、线程同步

1、方法

进行多线程编程,因为无法知道哪个线程会在哪个时候对共享资源进行操作,因此让如何保护共享资源变得复杂,通过下面这些技术的使用,可以解决线程之间对资源的竞争:

  • 互斥量Mutex2
  • 信号灯Semaphore3
  • 条件变量Conditions

2、互斥量Mutex

  • Item * p =queue_list;
  • Queue_list=queue_list->next;
  • process job(p);
  • free(p);
  • 需要互斥量的原因:保证线程对资源的独占,使用的完整性。
  • 当线程1处理完Item *p=queue_list后,系统停止线程1的运行,改而运行线程2。线程2照样取出头节点,然后进行处理,最后释放了该节点。过了段时间,线程1重新得到运行。而这个时候,p所指向的节点已经被线程2释放掉,而线程1对此毫无知晓。他会接着运行process job(p)。而这将导致无法预料的后果
  • 对于这种情况,系统给我们提供了互斥量。线程在取出头节点前必须要等待互斥量,如果此时有其他线程已经获得该互斥量,那么该线程将会阻塞在这里.只有等到其他线程释放掉该互斥量后,该线程才有可能得到该互斥量。互斥量从本质上说就是一把锁,提供对共享资源的保护访问

步骤:

  • (创建) 在Linux中,互斥量使用类型pthread_mutex_t表示.在使用前,要对它进行初始化:
    • #include
    • int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t mutexattr)
    • 参数
      • 参数1:传出参数,调用时应传&mutex
      • 参数2:互斥属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享).
    • int pthread_mutex_destroy(pthread_mutex_tmutex)
  • (初始化)对于静态分配的互斥量,可以把它设置为默认的mutex对象pthread_mutex_initializer
        • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

对于动态分配的互斥量,在申请内存(malloc)之后,通过pthread_mutex_init进行初始化

  • pthread_mutex_init(&mutex, NULL);

在释放内存(free)前需要调用pthread_mutex_destroy

  • (加/上锁)对共享资源的访问,要使用互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_trylock(pthread_mutex_t *mutex)

返回值:成功则返回0,出错则返回错误编号。

trylock是非阻塞调用模式,如果互斥量没被锁住,trylock函数将对互斥量加锁,并获得对共享资源的访问权限;

如果互斥量被锁住了,trylock函数将不会阻塞等待而直接返回EBUSY,表示共享资源处于忙状态

  • (解锁)在操作完成后,必须给互斥量解锁,也就是前面所说的释放。这样其他等待该锁的线程才有机会获得该锁,否则其他线程将会永远阻塞。

int pthread_mutex_unlock(pthread_mutex_t *mutex)

  • (销毁)动态分配内存,在释放内存(free)前需要调用pthread_mutex_destroy
    • pthread_mutex_destroy(pthread_mutex_t *mutex);
    • 返回值
      • 若成功,返回0,否则,返回错误编号

3、条件变量

(1)条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所(共享的数据)。

(2)主要应用函数:

  • pthread_cond_init函数
  • pthread_cond_destroy函数
  • pthread_cond_wait函数
  • pthread_cond_timedwait函数
  • pthread_cond_signal函数
  • pthread_cond_broadcast函数
  • 以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。
  • pthread_cond_t类型 用于定义条件变量 pthread_cond_t cond;
  • 引入条件变量的目的:在使用互斥锁的基础上引入条件变量可以使程序的效率更高,因为条件变量的引入明显减少了线程取竞争互斥锁的次数。执行pthread_cond_wait或pthread_cond_timedwait函数的线程明显知道了条件不满足,要因此在其释放锁之后就没有必要再跟其它线程去竞争锁了,只需要阻塞等待signal或broadcast函数将其唤醒。这样提高了效率。

(3)函数

  • 1)pthread_cond_init函数
    • int pthread_cond_init(pthread_cond_t * cond, pthread_condattr_t *cond_attr);
    • 作用:初始化一个条件变量
    • 参2:attr表条件变量属性,通常为默认值,传NULL即可。
    • 也可以使用静态初始化的方法,初始化条件变量:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 2)pthread_cond_destroy函数
    • int pthread_cond_destroy(pthread_cond_t *cond);
    • 作用:销毁一个条件变量
  • 3)pthread_cond_wait函数
    • int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
    • 作用:
      • 阻塞等待条件变量cond(形参1)满足条件,且释放已经掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex)(注意这是一个原子操作,即阻塞等待的同时马上解锁,类似sigsuspend函数);
      • 当被唤醒(signal或broadcast函数),pthread_cond_wait函数返回,解除阻塞并重新申请获取互斥锁:pthread_mutex_lock(&mutex);
  • 4)pthread_cond_timedwait函数
    • int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t * mutex, const struct timespec *abstime);
    • 作用:与pthread_cond_wait函数作用相同,但其限时等待一个条件变量,即如果在规定的时间点(第三个形参)还未被唤醒时,该线程自动唤醒并解除阻塞重新申请获取互斥锁:pthread_mutex_lock(&mutex);
  • 5)pthread_cond_signal函数
    • int pthread_cond_signal(pthread_cond_t *cond);
    • 作用:唤醒至少一个阻塞在条件变量上的线程。
  • 6)pthread_cond_broadcast函数
    • int pthread_cond_broadcast(pthread_cond_t *cond);
    • 作用:唤醒全部阻塞在条件变量上的线程

(4)生产者消费者条件变量模型

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

看如下示例,使用条件变量模拟生产者、消费者问题:

#include 
#include 
#include 

#define BUFFER_SIZE 16
#define OVER -1    //不放东西了
struct prod
{
    int buffer[BUFFER_SIZE];
    pthread_mutex_t lock;
    pthread_cond_t notempty;
    pthread_cond_t notfull;

    int readpos,writepos;
};

struct prod buf;//结构体类型的变量,对应结构体内的成员,可以减少程序定义次数

void init(struct prod *b)
{
    pthread_mutex_init(&b->lock,NULL);

    pthread_cond_init(&b->notempty,NULL);

    pthread_cond_init(&b->notfull,NULL);

    b->readpos = b->writepos = 0;
}

void put(struct prod *b,int data)
{
    pthread_mutex_lock(&b->lock);

    if ((b->writepos + 1)% BUFFER_SIZE == b->readpos)
    {
        pthread_cond_wait(&b->notfull,&b->lock);

    }
    b->buffer[b->writepos]=data;
    b->writepos++;

    if (b->writepos >= BUFFER_SIZE)
    {
        b->writepos=0;
    }
    pthread_cond_signal(&b->notempty);
    pthread_mutex_unlock(&b->lock);
}


void * producer(void)
{
    int n;

    for(n=0;n<20;n++)
    {
        printf("%2d-----> |\n",n+1);
        put(&buf,n+1);
    }
    put(&buf,OVER);

    return NULL;
}

int get(struct prod *b)
{
    int data;

    pthread_mutex_lock(&b->lock);

    if (b->readpos==b->writepos)
    {
        pthread_cond_wait(&b->notempty,&b->lock);
    }

    data=b->buffer[b->readpos];
    
    b->readpos =(b->readpos+1) % BUFFER_SIZE;
    pthread_cond_signal(&b->notfull);
    pthread_mutex_unlock(&b->lock);
}


void * customer(void)
{
    int d;

    while(1)
    {
        d=get(&buf);

        if(OVER==d)
        {
            break;
        }
        printf("|----->%2d\n");
    }
    return NULL;
}



int main()
{
    pthread_t th_p,th_c;

    init(&buf);

    pthread_create(&th_p,NULL,(void *)producer,NULL);
    pthread_create(&th_c,NULL,(void *)customer,NULL);

    pthread_join(th_p,NULL);
    pthread_join(th_c,NULL);

    return 0;
}

4、信号灯

(1)定义

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。

信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。

信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为 sem_t ,对应的头文件为

#include sem_t sem;

(2)主要函数

1)sem_init函数

#include // 初始化信号量/信号灯 int sem_init(sem_t *sem, int pshared, unsigned int value); // 资源释放, 线程销毁之后调用这个函数即可 // 参数 sem 就是 sem_init() 的第一个参数 int sem_destroy(sem_t *sem);

  • 参数:
    • sem:信号量变量地址
    • pshared:
      • 0:线程同步
      • 非 0:进程同步
    • value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。

2)sem_wait函数

// 参数 sem 就是 sem_init() 的第一个参数 // 函数被调用sem中的资源就会被消耗1个, 资源数-1 int sem_wait(sem_t *sem);

  • 当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。

3)sem_trywait函数

// 参数 sem 就是 sem_init() 的第一个参数 // 函数被调用sem中的资源就会被消耗1个, 资源数-1 int sem_trywait(sem_t *sem);

  • 当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。

4)struct timespec

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示 struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0 .. 999999999] */ }; // 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。 // abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的 int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

  • 该函数的参数 abs_timeout 和 pthread_cond_timedwait 的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。

5)sem_post函数

// 调用该函数给sem中的资源数+1 int sem_post(sem_t *sem);

  • 调用该函数会将 sem 中的资源数 +1,如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。

6)sem_getvalue函数

// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中 // sval是一个传出参数 int sem_getvalue(sem_t *sem, int *sval);

通过这个函数可以查看 sem 中现在拥有的资源个数,通过第二个参数 sval 将数据传出,也就是说第二个参数的作用和返回值是一样的。

(3)生产者与消费者

场景描述:使用信号量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点。

1)总资源数为1

如果生产者和消费者线程使用的信号量对应的总资源数为 1,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了。

#include 
#include 
#include 
#include 
#include 
#include 

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        // 生产者拿一个信号灯
        sem_wait(&psem);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());

        // 通知消费者消费, 给消费者加信号灯
        sem_post(&csem);
        

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        sem_wait(&csem);
        // 取出链表的头结点, 将其删除
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        free(pnode);
        // 通知生产者生成, 给生产者加信号灯
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化信号量
    // 生产者和消费者拥有的信号灯的总和为1
    sem_init(&psem, 0, 1);  // 生成者线程一共有1个信号灯
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    sem_destroy(&psem);
    sem_destroy(&csem);

    return 0;
}

结论:如果生产者和消费者使用的信号量总资源数为 1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。

2)总资源数大于1

如果生产者和消费者线程使用的信号量对应的总资源数为大于 1,这种场景下出现的情况就比较多了:

  • 多个生产者线程同时生产
  • 多个消费者同时消费
  • 生产者线程和消费者线程同时生产和消费

以上不管哪一种情况都可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行线程同步,处理代码如下:

#include 
#include 
#include 
#include 
#include 
#include 

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 生产者线程信号量
sem_t psem;
// 消费者线程信号量
sem_t csem;

// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        // 生产者拿一个信号灯
        sem_wait(&psem);
        // 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
        pthread_mutex_lock(&mutex);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
        pthread_mutex_unlock(&mutex);

        // 通知消费者消费
        sem_post(&csem);
        
        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        // 取出链表的头结点, 将其删除
        free(pnode);
        pthread_mutex_unlock(&mutex);
        // 通知生产者生成, 给生产者加信号灯
        sem_post(&psem);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化信号量
    sem_init(&psem, 0, 5);  // 生成者线程一共有5个信号灯
    sem_init(&csem, 0, 0);  // 消费者线程一共有0个信号灯
    // 初始化互斥锁
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    sem_destroy(&psem);
    sem_destroy(&csem);
    pthread_mutex_destroy(&mutex);

    return 0;
}

你可能感兴趣的:(Linux,C,linux,unix)