【Linux开发-多线程】

一,线程认识

1,线程概念

  • 线程是在进程中产生的一个执行单元,是CPU调度和分配的最小单元,其在同一个进程中与其他线程并行运行,他们可以共享进程内的资源,比如内存、地址空间、打开的文件等等。
    • 线程是CPU调度和分派的基本单位,
    • 进程是分配资源的基本单位
  • 进程:正在运行的程序(狭义)
    是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称,从操作系统核心角度来说,进程是操作系统调度除CPU时间片外进行的资源分配和保护的基本单位,它有一个独立的虚拟地址空间,用来容纳进程映像(如与进程关联的程序与数据),并以进程为单位对各种资源实施保护,如受保护地访问处理器、文件、外部设备及其他进程(进程间通信)
  • 进程由操作系统控制,线程由CPU控制,一个进程至少有一个线程。

2,为什么使用多线程

- **避免阻塞**

大家知道,单个进程只有一个主线程,当主线程阻塞的时候,整个进程也就阻塞了,无法再去做其它的一些功能了。
- 避免CPU空转
应用程序经常会涉及到RPC,数据库访问,磁盘IO等操作,这些操作的速度比CPU慢很多,而在等待这些响应时,CPU却不能去处理新的请求,导致这种单线程的应用程序性能很差。
- 提升效率
一个进程要独立拥有4GB的虚拟地址空间,而多个线程可以共享同一地址空
间,线程的切换比进程的切换要快得多
- 上下文切换
在这里插入图片描述

二,线程创建与运行

1,API函数

头文件:#include<pthread.h>

1,pthread_create函数

线程具有单独的执行流,因此需要单独定义线程的main函数,还需要请求操作系统在单独的执行流中执行该函数,完成该功能的函数如下。

//线程创建→成功时返回 0,失败时返回其他值。
int pthread_create(
	pthread_t*restrict thread, 
	const pthread_attr_t * restrict attr,
	void *(* start_routine)(void *),
	void*restrict arg 
);

●thread
保存新创建线程ID的变量地址值。线程与进程相同,也需要用于区分不同线程的ID。
●attr
用于传递线程属性的参数,传递NULL时,创建默认属性的线程。
●start_routine
相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针)。
● arg
通过第三个参数传递调用函数时包含传递参数信息的变量地址值。

2,pthread_join函数

调用pthread_join函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终止为止。而且可以得到线程的main函数返回值,所以该函数比较有用。下面通过示例了解该函数的功能。

# include<pthread.h>
//线程等待→成功时返回线程的int,失败时返回其他值。
int pthread_join(pthread_t thread, void ** status);
  • thread 该参数值ID的线程终止后才会从该函数返回。status保存线程的main函数返回值的指针变量地址值。

2,代码案例

1,需要引入thread库

项目-属性-链接器-输入-库依赖项里添加pthread库
在这里插入图片描述

2,示例代码

#include <pthread.h>

void* threadEntry(void* arg)
{
    
    
    const char* msg = "I am from thread!";
    for (int i = 0; i < 3; i++)
    {
    
    
        printf("%s(%d):%s thread begin=%s!\r\n", __FILE__, __LINE__, __FUNCTION__, arg);

    }
    return (void*)msg;
}

void thread_func()
{
    
    
    pthread_t tid;
    const char* pInfo = "hello thread!";
    int ret = pthread_create(&tid, NULL, threadEntry, (void*)pInfo);//编译时需要加上 -lpthread,Linux系统默认已有,window下需要自己下载
    if (ret != -1)
    {
    
    
        void* result = NULL;
        pthread_join(tid, &result);//线程等待,等待tid线程结束再执行下面内容

        printf("%s(%d):%s from thread=%s!\r\n", __FILE__, __LINE__, __FUNCTION__, result);

    }
}

在这里插入图片描述

三,线程同步

在这里插入图片描述

  • 线程同步用于解决线程访问顺序引发的问题。需要同步的情况可以从如下两方面考虑。

    • 1、同时访问同一内存空间时发生的情况。
    • 2、需要指定访问同一内存空间的线程执行顺序的情况。
  • "控制(Control)线程执行顺序"的相关内容。假设有A、B两个线程,线程A负责向指定内存空间写人(保存)数据,线程B负责取走该数据。这种情况下,线程A首先应该访问约定的内存空间并保存数据。万一线程B先访问并取走数据,将导致错误结果。像这种需要控制执行顺序的情况也需要使用同步技术。

1,互斥量

1,创建与销毁(pthread_metex_init,pthread_mutex_destroy)

#include<pthread.h>
int pthread_mutex_init(pthread mutex_t*mutex, const pthread_mutexattr_t* attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);
//→成功时返回0,失败时返回其他值。
  • mutex 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。attr传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL。

2,开始与结束(pthread_mutex_lock,pthreadmutex_unlock)

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread mutex_t* mutex);
//→成功时返回0,失败时返回其他值。
  • 使用方法:
    pthread_mutex_lock(&mutex);
    //临界区的开始
    //…
    // 临界区的结束
    pthreadmutex_unlock(&mutex);

简言之,就是利用lock和unlock函数围住临界区的两端。此时互斥量相当于一把锁,阻止多个线程同时访问。还有一点需要注意,线程退出临界区时,如果忘了调用pthread mutex_unlock 函数,那么其他为了进入临界区而调用pthread mutexlock函数的线程就无法摆脱阻塞状态。这就是死锁状态。

3,互斥量代码示例

  • 互斥量是"Mutual Exclusion"的简写,表示不允许多个线程同时访问。互斥量主要用于解决线程同步访问的问题。
  • 线程中为了保护临界区需要套用锁机制。线程同步中同样需要锁。互斥量就是一把优秀的锁,接下来介绍互斥量的创建及销毁函数。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREAD	100

void * thread_inc(void * arg);
void * thread_des(void * arg);

long long num=0;
pthread_mutex_t mutex;

int main(int argc, char *argv[]) 
{
    
    
	pthread_t thread_id[NUM_THREAD];
	int i;
	
	pthread_mutex_init(&mutex, NULL);

	for(i=0; i<NUM_THREAD; i++)
	{
    
    
		if(i%2)
			pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
		else
			pthread_create(&(thread_id[i]), NULL, thread_des, NULL);	
	}	

	for(i=0; i<NUM_THREAD; i++)
		pthread_join(thread_id[i], NULL);

	printf("result: %lld \n", num);
	pthread_mutex_destroy(&mutex);
	return 0;
}

void * thread_inc(void * arg) 
{
    
    
	int i;
	pthread_mutex_lock(&mutex);
	for(i=0; i<50000000; i++)
		num+=1;
	pthread_mutex_unlock(&mutex);
	return NULL;
}
void * thread_des(void * arg)
{
    
    
	int i;
	for(i=0; i<50000000; i++)
	{
    
    
		pthread_mutex_lock(&mutex);
		num-=1;
		pthread_mutex_unlock(&mutex);
	}
	return NULL;
}

2,信号量

1,创建与销毁(sem_init,sem_destory)

信号量创建及销毁方法如下:

#include <semaphore.h>
/*→成功时返回0,失败时返回其他值。*/
//创建
int sem_init(sem_t* sem, int pshared, unsigned int value);
//销毁
int sem_destroy(sem_t* sem);

  • Sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
  • pshared :传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。我们需要完成同一进程内的线程同步,所以传递0。
  • Value:指定新创建的信号量初始值。

上述函数的pshared参数默认向其传递0。接下来介绍信号量中相当于互斥量lock、unlock的函数。

#include<semaphore.h>
int sem_post(sem_t*sem);
int sem_wait(sem_t * sem);
//→成功时返回0,失败时返回其他值。
  • Sem:传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1。

调用sem init函数时,操作系统将创建信号量对象,此对象中记录着"信号量值"整数。该值在调用sem post函数时增1,调用sem_wait函数时减1。但信号量的值不能小于0,因此,在信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态(因为函数未返回)。
当然,此时如果有其他线程调用sem_post函数,信号量的值将变为1,而原本阻塞的线程可以将该信号量重新减为0并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为1)。

2,开始与结束(sem_wait,sem_post)

sem_wait(&sem);//信号量变为0…
// 临界区的开始
//…
//临界区的结束
sem_post(&sem);// 信号量变为1…

  • 上面的代码结构中,调用sem_wait函数进入临界区的线程在调用sem post函数前不允许其他线程进入临界区。
  • 信号量的值在0和1之间跳转,因此,具有这种特性的机制称为"二进制信号量"
  • “线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出总和并退出程序。”
    按照要求构建程序,应按照线程A、线程B的顺序访问变量num,且需要线程同步。

3,信号量代码示例

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

void * read(void * arg);
void * accu(void * arg);
static sem_t sem_one;
static sem_t sem_two;
static int num;

int main(int argc, char *argv[])
{
    
    
	pthread_t id_t1, id_t2;
	sem_init(&sem_one, 0, 0);//是否完成输入
	sem_init(&sem_two, 0, 1);//是否完成计算

	pthread_create(&id_t1, NULL, read, (void) 5);
	pthread_create(&id_t2, NULL, accu,  (void) 5);

	pthread_join(id_t1, NULL);
	pthread_join(id_t2, NULL);

	sem_destroy(&sem_one);
	sem_destroy(&sem_two);
	return 0;
}

void * read(void * arg)
{
    
    
	int i;
	int count = (int)arg;
	for(i=0; i<count; i++)
	{
    
    
		fputs("Input num: ", stdout);

		sem_wait(&sem_two);
		scanf("%d", &num);
		sem_post(&sem_one);
	}
	return NULL;	
}
void * accu(void * arg)
{
    
    
	int sum=0, i;
	int count = (int)arg;
	for(i=0; i<count; i++)
	{
    
    
		sem_wait(&sem_one);
		sum+=num;
		sem_post(&sem_two);
	}
	printf("Result: %d \n", sum);
	return NULL;
}

四,线程销毁

销毁线程(pthread_join,pthread_detach)的区别

  • Linux线程并不是在首次调用的线程main函数返回时自动销毁,所以用如下2种方法之一加以明确。否则由线程创建的内存空间将一直存在。
    • 1、调用pthread_join函数。
    • 2、调用pthread_detach函数。
  • 之前调用过pthread_join函数。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。因此,通常通过如下函数调用引导线程销毁。
#include <pthread.h>
int pthread_detach(pthread_t thread);
//→成功时返回,失败时返回其他值。
  • thread 终止的同时需要销毁的线程ID。
    调用 pthread_detach函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。
    调用该函数后不能再针对相应线程调用pthread_join函数,这需要格外注意。虽然还有方法在创建线程时可以指定销毁时机,但与pthread_detach方式相比,结果上没有太大差异.

  • 此外可以通过 线程函数:
    pthread_detach(pthread_self());

    pthread_exit(0) ;

    来进行自动销毁,无需外部的操作

猜你喜欢

转载自blog.csdn.net/MOON_YZM/article/details/130892086