并发-线程

1, 线程

线程(thread)也是并发 的一种形式,线程是比进程更小的活动单位,一个进程中可以有多个线程,线程是进程内部的一个执行分支。

一个进程刚开始时只有一个线程(称之为主线程),后续的代码中可以创建新的线程,可以指定新线程去执行某个函数,这个函数称之为线程函数。

进程内部的多个线程共享该进程内部的所有数据,所以线程间通信相比进程简便很多,如:直接使用全局变量就行

2,线程相关函数

2.1 创建一个新线程

复制代码
   pthread_t类型变量用来表示一个线程的id,线程id具有唯一性
   void *(*start_routine) (void *) 
   start_routine 是函数指针,指向一个返回值为void*,参数为void*的函数(这样的函数,称之为线程函数)
   
   pthread_create - create a new thread
      SYNOPSIS
      #include <pthread.h>
   int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                      void *(*start_routine) (void *), void *arg);
        thread:pthread_t类型变量的地址,该变量用来保存新线程的id
        attr:用来指定线程的属性,一般为NULL,表示采取默认属性,如果不想采用默认属性,后续会有函数来修改
                线程属性。
        start_routine:线程函数的地址。新线程创建成功后就去执行该函数
        arg:线程函数的参数
   Compile and link with -pthread.

2.2 线程结束

(1) 正常结束

该执行的指令都执行完了(线程函数执行完了)

(2) 进程结束了,进程内的所有线程都结束

(3) 在线程执行的任意时刻,调用 pthread_exit 函数,该线程就会退出/结束

复制代码
    #include <pthread.h>
    void pthread_exit(void *retval);
        retval:线程 退出码/返回值 的地址

(4) 被别人干掉

复制代码
    #include <pthread.h>
    int pthread_cancel(pthread_t thread);
        thread:需要被干掉的那个线程的id
                
    线程有一个属性,可以决定是否能被别人干掉,可以用一个函数来设置
    #include <pthread.h>
    int pthread_setcancelstate(int state, int *oldstate);
        state: 
            PTHREAD_CANCEL_ENABL3E  可以被干掉(默认属性)
            PTHREAD_CANCEL_DISABLE  不能被干掉
        oldstate:用来保存改变之前的属性,如果不关心改变之前的属性,就为NULL        
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int x = 0;
void func1()//普通函数
{
    int i;
    for(i=0;i<100;i++)
    {
        printf("主线程i=%d\n",i);
        usleep(100);
    }
}

void* func2(void *arg)//线程函数
{
    int i;
    for(i=0;i<100;i++)
    {
        printf("-----------新线程i=%d\n",i);
        usleep(100);
    }
}

void* func3(void *arg)//线程函数
{
    int i;
    printf("参数:%s\n",(char *)arg);
    for(i=0;i<100;i++)
    {
        printf("-----------新线程i=%d\n",i);
        usleep(100);
    }
}

void* func4(void *arg)//线程函数
{
    x += 100;
    int i;
    printf("参数:%d\n",*((int *)arg));
    for(i=0;i<100;i++)
    {
        printf("-----------新线程i=%d\n",i);
        usleep(100);
    }

}

int main()
{
    //func1();//func1是普通函数,直接调用,并没有并发,先执行完func函数再往下执行
    pthread_t tid;//用来保存新线程的id
    //pthread_create(&tid,NULL,func2,NULL);//创建新线程执行 func2函数,并且把参数NULL传递给func2
    //char buf[] = "线程参数测试";
    //pthread_create(&tid,NULL,func3,(void*)buf);//创建新线程执行 func3函数,并且把参数buf传递给func3
                                                //传递字符串

    int data = 100;
    pthread_create(&tid,NULL,func4,(void*)&data);//创建新线程执行 func4函数,并且把参数&data传递给func4
                                                //传递整数
    int i;
    for(i=0;i<100;i++)
    {
        printf("主线程i=%d\n",i);
        usleep(100);
    }
    printf("x=%d\n",x);
    return 0;//进程结束了,所有线程立马都结束
    //pthread_exit(NULL);//只是退出当前线程(主线程),其他线程如果没有执行完,不会结束
}

2.3 资源回收

一个线程结束了,并不代表所有资源都被释放了,有两种方式回收线程资源:自动回收和手动回收

由一个属性决定是自动回收还是需要手动回收,该属性默认是需要手动回收资源,如果需要自动回收,调用

pthread_detach函数

复制代码
#include <pthread.h>
int pthread_detach(pthread_t thread);
    thread:线程id,把这个线程设置为自动回收资源
    失败返回-1,成功返回0
例如:
    pthread_detach(pthread_self());//设置自动回收该线程的资源
    
//等待线程结束,并手动回收资源
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
    thread:线程id
    retval:二级指针,一般是定义一个一级指针变量,把这个变量的地址作为参数传入,
            成功后,改变了保存了 退出线程的 退出码。如果不需要保存退出码,该参数为NULL
    失败返回-1,成功返回0
    
二者选一即可

多线程并发也会有和多进程并发一样的问题:访问共享资源时被打断,而造成不可预知的后果

所以多线程并发时,也需要PV操作,可以用之前学过的信号量,但是有更好的方法:线程互斥锁

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void * func1(void *arg)
{
    //pthread_detach(pthread_self());//设置自动回收该线程的资源
    int i;
    for(i=0;i<100;i++)
    {
        printf("-----------新线程i=%d\n",i);
        usleep(100);
    } 
    int *p = (int*)malloc(4);
    *p = 200;//退出码
    pthread_exit((void*)p);
}


int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,func1,NULL);
    int i;
    for(i=0;i<100;i++)
    {
        printf("主线程i=%d\n",i);
        usleep(100);
    }
    int *p;
    int r = pthread_join(tid,(void*)&p);//等待tid线程结束并 手动回收tid线程的资源
    if(-1==r)
    {
        perror("pthread_join失败");
    }
    printf("线程退出码:%d\n",*p);
    free(p);
    return 0;//进程结束了,所有线程立马都结束
}

3,线程互斥锁

pthread_mutex_t类的变量就是线程互斥锁

3.1 初始化线程互斥锁

复制代码
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
        const pthread_mutexattr_t *restrict attr);  
     mutex:线程互斥锁的地址
     attr:互斥锁的属性,一般为NULL,表示默认属性(如:初始化之后为解锁状态)
   失败返回-1,成功返回0 

3.2 P操作

复制代码
   #include <pthread.h>
​
   int pthread_mutex_lock(pthread_mutex_t *mutex);//如果是死锁状态,一直等
   int pthread_mutex_trylock(pthread_mutex_t *mutex);//尝试获取锁资源
   int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
           const struct timespec *restrict abstime);//限时等待

3.3 V操作

复制代码
    int pthread_mutex_unlock(pthread_mutex_t *mutex);

3.4 销毁互斥锁

复制代码
   #include <pthread.h>
   int pthread_mutex_destroy(pthread_mutex_t *mutex);
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <string.h>
#include <semaphore.h>

pthread_mutex_t mutex;//线程互斥锁

//多线程并发时,也需要PV操作,可以用之前学过的信号量,但是有更好的方法:线程互斥锁
int data = 0;//共享资源

//信号量
void *func1(void *arg)
{
    sem_t *s = (sem_t *)arg;
    int i;
    for(i=0;i<1000000;i++)
    {
        sem_wait(s);
        data++;
        sem_post(s);
    }
}

//线程互斥锁
void * func2(void *arg)
{
    int i;
    for(i=0;i<1000000;i++)
    {
        pthread_mutex_lock(&mutex);
        data++;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
   
    pthread_mutex_init(&mutex,NULL);
    pthread_t tid1,tid2;
    pthread_create(&tid1,NULL,func2,NULL);
    pthread_create(&tid2,NULL,func2,NULL);

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("data=%d\n",data);
    pthread_mutex_destroy(&mutex);
    return 0;
}

练习:

模拟一个外卖店,有如下要求:

该店菜品有 辣椒炒肉,剁椒鱼头,水煮肉片,麻婆豆腐,红烧肉,糖醋排骨,空心菜,大白菜

每隔时间t1有顾客下单,下单的间隔时间t1随机生成,用 sleep/usleep模拟,下单菜品也是随机生成

两个外卖小哥接单,假设送单时间为t2,也是随机生成,用 sleep/usleep模拟

先下单的一定会被先接单,且同一个外卖小哥只能送完一单才能接下一单

该外卖店最多只接100单,完成则下班

如果累计有20单未被接单,则暂停下单

下单和接单信息打印输出

顾客 -》一个线程

每个外卖小哥 -》 一个线程

复制代码
void * func1(void *arg)//顾客线程
{
	while(1)//一直下单,间隔时间为 t1
	{
		sleep(t1);
		下单 -> 下单信息保存到队列  入队
	}
}

void *func2(void *arg)//外卖小哥线程
{
	while(1)//一直接单,送单时间为 t2
	{
		接单  -> 出队
		模拟送单 -> sleep(t2)
	}
}
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include "queue.h"
char * name[8] = {"辣椒炒肉","剁椒鱼头","水煮肉片","麻婆豆腐","红烧肉","糖醋排骨","空心菜","大白菜"};
int count = 0;//记录下单的总数量
pthread_mutex_t mutex;
pthread_cond_t cond;
pthread_cond_t cond2;
//模拟顾客下单

void * func1(void *arg)
{
    Queue * l = (Queue*)arg;
    int t1;//顾客下单的间隔时间
    int index;//顾客下单菜品的下标
    while(1)
    {
        t1 = rand() % 10 + 10;//间隔时间假设为 [1,3]秒钟
        sleep(t1);

        pthread_mutex_lock(&mutex);
        if(count >= 100)
        {
            pthread_mutex_unlock(&mutex);
            break;        
        }
        else
        {
            if(l->num < 20)
            {
                //下单,入队
                index = rand()%8;
                push(l,index);
                
                count++;
                printf("顾客下单了,这是第%d单,菜品是:%s\n",count,name[index]);  
                pthread_cond_signal(&cond);
            }  
            else
            {
                //订单>=20,暂停下单
                pthread_cond_wait(&cond2,&mutex);
            }
        }
        pthread_mutex_unlock(&mutex);

    }
}

//模拟外卖小哥接单
void* func2(void *arg)
{
    Queue * l = (Queue*)arg;
    int t2;//小哥送单的时间
    int index;
    while(1)
    {
        //接单
        pthread_mutex_lock(&mutex);
        if(is_empty(l) == 0)
        {
            index = get_front(l);//获取订单
            pop(l);
            printf("外卖小哥接单,菜品名:%s\n",name[index]);
            pthread_cond_signal(&cond2);
        }
        else
        {
            if(count >= 100)
            {
                pthread_mutex_unlock(&mutex);
                break;
            }
            pthread_cond_wait(&cond,&mutex);
            printf("一直在循环判断,但是条件一直不成立,浪费CPU\n");
        }
        pthread_mutex_unlock(&mutex);
        t2 = rand() % 1 + 1;//送单时间假设为 [1,3]秒钟
        sleep(t2);
        
    }
    
}

int main()
{
    Queue * l = init_queue();
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    pthread_cond_init(&cond2,NULL);
    
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,NULL,func1,(void*)l);
    pthread_create(&tid2,NULL,func1,(void*)l);

    pthread_create(&tid3,NULL,func2,(void*)l);
    pthread_create(&tid4,NULL,func2,(void*)l);

    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    pthread_join(tid4,NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    pthread_cond_destroy(&cond2);
    destroy_queue(l);
    return 0;
}

4,线程条件变量

前面的例子中,外卖小哥使订单减少,顾客使订单增加,是一个典型的 消费者-生产者 模型。

如果数据"已满"(超过20单),生产者应该停止生产,如果数据"空了",消费者停止消费。

问题是消费者怎么知道数据空了?生产者怎么知道数据已满?

常规的做法就是循环判断,一直不间断的进行判断,直到满足我的条件。

生产者的条件:数据没有满

消费者的条件:数据不为空

这种常规做法有一个缺点:浪费CPU资源,有时候一直循环但是都是不满足条件

-》线程条件变量

条件不满足 的时候,进行休眠(是指进入阻塞态,让出CPU);

条件满足 时,需要别人来唤醒我;(消费者休眠一般由生产者唤醒,生产者休眠一般由消费者唤醒)

4.1 初始化线程条件变量

复制代码
pthread_cond_t 类型的变量就是线程条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *cattr);
		cond:要初始化的线程条件变量的首地址
		cattr:属性,一般为NULL,表示默认属性
	失败返回-1,同时errno被设置
	成功返回0

4.2 进入阻塞状态,等待条件满足

复制代码
   #include <pthread.h>
   int pthread_cond_wait(pthread_cond_t *restrict cond,
       			pthread_mutex_t *restrict mutex);
       cond:线程条件变量地址
       mutex:线程互斥锁,在执行 pthread_cond_wait操作前,必须线对 mutex进行lock/p 操作
       		因为在pthread_cond_wait函数内部会对 mutex进行 unlock/V 操作
       
   int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);

4.3 唤醒正在阻塞的线程

复制代码
   #include <pthread.h>
   int pthread_cond_signal(pthread_cond_t *cond);
   		cond:条件变量的地址,唤醒阻塞在该条件变量上的任意一个线程
   		
   int pthread_cond_broadcast(pthread_cond_t *cond);//broadcast 广播
   		cond:条件变量的地址,唤醒阻塞在该条件变量上的所有线程

4.4 销毁线程条件变量

复制代码
int pthread_cond_destroy(pthread_cond_t *cond);
	cond:条件变量的地址
相关推荐
雪碧聊技术2 天前
多线程4:线程池、并发、并行、综合案例-抢红包游戏
java·线程池·多线程·并发·并行·复用线程
慕木沐3 天前
【JUC并发】cmpxchg和lock指令
后端·并发·juc·汇编指令
Hello-Brand8 天前
Java核心知识体系10-线程管理
java·高并发·多线程·并发·多线程模型·线程管理
许野平13 天前
Rust:启动与关闭线程
开发语言·后端·rust·线程·启动·关闭
Themberfue14 天前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
老猿讲编程17 天前
Python 线程学习知识大全
python·线程
邂逅岁月19 天前
【多线程奇妙屋】 Java 的 Thread类必会小技巧,教你如何用多种方式快速创建线程,学并发编程必备(实践篇)
java·开发语言·操作系统·线程·进程·并发编程·javaee
wangsir.19 天前
POSIX信号量
linux·c++·线程·信号处理·进程
CXDNW20 天前
【系统面试篇】进程和线程类(1)(笔记)——区别、通讯方式、同步、互斥、死锁
笔记·操作系统·线程·进程·互斥·死锁
wangsir.22 天前
linux线程池
linux·c++·线程