首页 > 其他分享 >操作系统-线程

操作系统-线程

时间:2024-08-22 20:05:26浏览次数:7  
标签:attr int queue mutex pthread 线程 操作系统

一、线程介绍

  • 线程是操作系统能内够进行运算、执行的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
    总结:线程是进程的一部分,是进程内负责执行的单位,进程是由资源单位(内存资源、信号处理方案、文件表)+执行单位组成,默认情况下进程内只有一个线程,但进程可以有多个。

线程的发展简史:

​ 60年代,在操作系统中能拥有资源和独立运行的基本单位是进程。
​ 随着计算机技术的发展,进程出现了很多弊端:
​ 一是由于进程是资源拥有者,创建、撤消与切换存在较大的时间开销,因此需要引入轻型进程;
​ 二是由于对称多处理机出现,可以满足多个运行单位,而多个进程并行开销过大。
​ 因此在80年代,出现了能独立运行的基本单位——线程(Threads)。

线程的调度策略:

​ 线程是独立调度和分派的基本单位,有三种不同的调试策略:

  1. 线程可以为操作系统内核调度的内核线程,如Win32线程;
  2. 由用户进行自行调度的用户线程,如Linux、UNIX平台的POSIX Thread;
  3. 由内核与用户进程进行混合调度,如Windows 7的线程。

多线程适用的范围:

​ 一个进程可以有很多线程,每条线程并行执行不同的任务。
​ 在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。
​ 在单CPU单核的计算机上,使用多线程技术,可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,原因就是线程占用的资源少,被阻塞时不浪费资源。

线程的特点:

1、轻型实体
​ 线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。线程的实体包括用于指示被执行指令序列的程序计数器、局部变量、状态参数和返回地址。

​ 线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述,包括以下信息:

  1. 线程状态
  2. 当线程不运行时,被保存的现场资源。
  3. 一组执行堆栈
  4. 存放每个线程的局部变量主存区
  5. 访问同一个进程中的主存和其它资源

2、独立调度和分派的基本单位:在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

3、可并发执行:​在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了CPU与外围设备并行工作的能力。

4、共享进程资源:在同一进程中的各个线程,都可以访问该进程的用户空间,此外,还可以访问进程所拥有的已打开文件、定时器、信号量等,线程可以共享该进程所拥有的资源。所以线程之间互相通信不必调用内核。

二、线程与进程的区别(多进程与多线程)

1、资源
​ 进程采用虚拟空间+用户态/内核态机制,所以就导致进程与进程之间是互相独立的,各自的资源不可见。
​ 在同一进程中的各个线程都可以共享该进程所拥有的资源。
​ 多进程之间资源是独立的,多线程之间资源是共享的。
2、通信
​ 由于进程之间是互相独立的,需要使用各种IPC通信机制,保障多个进程协同工作。
​ 同一进程中的各个线程共享该进程所拥有的资源,线程间可以直接读写进程数据段来进行通信,但需要线程同步和互斥手段的辅助,以保证数据的一致性。
​ 多进程之间资源是独立的,所以需要通信,多线程之间资源是共享的,所以需要同步和互斥。
3、调度:无论系统采用什么样的线程调试策略,线程上下文切换都比进程上下文切换要快得多。
4、身份:进程是个资源单位,线程是个执行单位,并且线程是进程的一部分,线程需要进程安身立命,进程也需要线程当牛做马。

三、POSIX线程库

POSIX线程库介绍:

​ POSIX线程(POSIX Threads,常被缩写为pthread)是POSIX的线程标准,定义了创建和操纵线程的一套API。
​ 实现POSIX 线程标准的库常被称作pthread,一般用于Unix-likePOSIX 系统,如Linux、Solaris。但是Microsoft Windows上的实现也存在,例如直接使用Windows API实现的第三方库pthread-w32。

API具体内容:

​ pthread定义了一套C语言的类型、函数与常量,它以pthread.h头文件和一个接口库libpthread.so,gcc和g++编译器没有默认链接该库,需要程序员使用 -l pthread 参数进行手动链接。

​ pthread API中大致共有100个函数调用,全都以"pthread_"开头,并可以分为四类:

​ 1、线程管理,如创建线程,等待线程,查询线程状态等。

​ 2、互斥锁,有创建、摧毁、锁定、解锁、设置属性等操作

​ 3、条件变量,有创建、摧毁、等待、通知、设置与查询属性等操作

​ 4、使用了互斥锁的线程间的同步管理。

四、创建线程

int pthread_create (pthread_t* thread,
                    const pthread_attr_t* attr,
                    void* (*start_routine) (void*),
                    void* arg);

thread        - 线程ID,输出型参数。我们目前使用的Linux中pthread_t即unsigned long int
attr          - 线程属性,NULL表示缺省属性,如果没有特殊需求,一般写NULL即可
start_routine - 线程入口函数指针,参数和返回值的类型都是void*
				启动线程本质上就是调用一个函数,只不过是在一个独立的线程中调用的,函数返回即线程结束
arg           - 传递给线程过程函数的参数
返回值:成功返回0,失败返回错误码,但不会修改全局的错误变量,也就是无法使用perror获取错误原因。    
 
注意:
	1、restrict: C99引入的编译优化指示符,提高重复解引用同一个指针的效率。
	2、应设法保证在线程过程函数执行期间,其参数所指向的目标持久有效。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* run(void* arg)
{
	for(;;)
	{
		printf("#");
		fflush(stdout);
		sleep(1);
	}
}

int main(int argc,const char* argv[])
{
	pthread_t tid;
	int ret = pthread_create(&tid,NULL,run,NULL);
	printf("%d %lu\n",ret,tid);

	for(;;)
	{
		printf("*");
		fflush(stdout);
		sleep(1);
	}
		
	return 0;
}

五、线程回收

int pthread_join (pthread_t thread, void** retval);
功能:等待thread参数所标识的线程结束,并回收相关资源,如果thread线程没有结束则阻塞
retval:获得线程正常结束时的返回值,是输出型的参数,用于获取线程入口函数的返回值。
返回值:成功返回0,失败返回错误码
    
从线程过程函数中返回值的方法:
	1、线程过程函数将所需返回的内容放在一块内存中,返回该内存块的首地址,保证这块内存在函数返回,即线程结束,以后依然有效;
	2、若retval参数非NULL,则pthread_join函数将线程入口函数所返回的指针,拷贝到该参数所指向的内存中;
	3、线程入口函数所返回的指针指向text、data、bss内存段的数据,如果指向heap内存段,则还需保证在用过该内存之后释放之。

六、获取线程ID、判断线程ID

pthread_t pthread_self (void);
成功返回调用线程的ID,不会失败。

int pthread_equal (pthread_t t1, pthread_t t2);
功能:若参数t1和t2所标识的线程ID相等,则返回非零,否则返回0。

注意:某些实现的pthread_t不是unsigned long int类型,可能是结构体类型,无法通过“==”判断其相等性。

练习:在一个多线程的进程中,设计一个函数,该函数只能由主线程调用,其它线程如果调用了该函数要立即结束执行。

七、终止线程

方法1:从线程入口函数中return,主线程除外。

方法2:调用pthread_exit函数。

void pthread_exit (void* retval);
retval - 和线程过程函数的返回值语义相同。

注意:在任何线程中调用exit函数都将终止整个进程。

问题:主线程结束,子线程是否会跟着一起结束?

主线程结束,并不会导致子线程跟着一起结束,它们之间没有必然联系。

但是,主线程如果执行到最后一行,会执行return 0或隐藏的return 0,而在main函数中执行return 0就相当于执行exit(0),然后当前进程就会结束,有两种方法可以避免这种情况:

方法1:

​ 等待所有子线程结束,主线程再执行return 0;

​ 子线程在一定时间内会结束,侧使用pthread_join。

方法2:

​ 立即结束主线程,不要让它执行return 0;

​ 当子线程的结束时间不确定,则使用pthread_exit。

​ 注意:这种情况会产生新的问题,子线程的资源没有办法回收。

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

void* run(void* arg)
{
	for(int i=0; ;i++)
	{
		printf("子线程:%lu %d\n",pthread_self(),i);
		sleep(1);
	}
}

int main(void)
{
	pthread_t tid;
	pthread_create(&tid,NULL,run,NULL);

	for(int i=0; i<3; i++)
	{
		printf("我是主线程,我要结束了,倒计时:%d\n",3-i);
		sleep(1);
	}
	
	exit
}

八、线程分离

同步方式(非分离状态):创建线程之后主线程调用pthread_join函数等待其终止,并释放线程资源。

异步方式(分离状态):无需创建者等待,线程终止后自行释放资源。

int pthread_detach (pthread_t thread);
功能:使thread参数所标识的线程进入分离(DETACHED)状态。
返回值:成功返回0,失败返回错误码。

注意:如果若干个子线程需要长时间执行,不知道什么时候能结束,为了避免它父线程陷入无尽的等待,可提前给子线程设置分离状态。

九、取消线程

向发送取消请求:

int pthread_cancel (pthread_t thread);
功能:该函数只是向线程发出取消请求,并不等待线程终止。

缺省情况下,线程在收到取消请求以后,并不会立即终止,而是仍继续运行,直到其达到某个取消点。
在取消点处,线程检查其自身是否已被取消了,并做出相应动作。

设置可取消状态:

int pthread_setcancelstate (int state,int* oldstate);
成功返回0,并通过oldstate参数输出原可取消状态(若非NULL),失败返回错误码。

state取值:
   PTHREAD_CANCEL_ENABLE  - 接受取消请求(缺省)。
   PTHREAD_CANCEL_DISABLE - 忽略取消请求。

设置可取消类型:

int pthread_setcanceltype (int type, int* oldtype);

成功返回0,并通过oldtype参数输出原可取消类型
(若非NULL),失败返回错误码。

type取值:
   PTHREAD_CANCEL_DEFERRED     - 延迟取消(缺省)。
      被取消线程在接收到取消请求之后并不立即响应,
       而是一直等到执行了特定的函数(取消点)之后再响应该请求。
   PTHREAD_CANCEL_ASYNCHRONOUS - 异步取消。
      被取消线程可以在任意时间取消,不是非得遇到取消点才能被取消。
      但是操作系统并不能保证这一点。

十、线程属性

int pthread_create (pthread_t* restrict thread,
                    const pthread_attr_t* restrict attr,
                    void* (*start_routine) (void*),
                    void* restrict arg);

//创建线程函数的第二个参数即为线程属性,传空指针表示使用缺省属性。
typedef struct {
    // 分离状态
    int detachstate;
       // PTHREAD_CREATE_DETACHED - 分离线程。
       // PTHREAD_CREATE_JOINABLE(缺省) - 可汇合线程。

    // 竞争范围
    int scope;
       // PTHREAD_SCOPE_SYSTEM - 在系统范围内竞争资源(时间片)。
       // PTHREAD_SCOPE_PROCESS(Linux不支持) - 在进程范围内竞争资源。
    

    // 继承特性
    int inheritsched;
       // PTHREAD_INHERIT_SCHED(缺省) - 调度属性自创建者线程继承。
       // PTHREAD_EXPLICIT_SCHED - 调度属性由后面两个成员确定。
    

    // 调度策略
    nt schedpolicy;
        // SCHED_FIFO - 先进先出策略。
            // 没有时间片。
            // 一个FIFO线程会持续运行,直到阻塞或有高优先级线程就绪。
            // 当FIFO线程阻塞时,系统将其移出就绪队列,待其恢复时再加到同优先级就绪队列的末尾。
            // 当FIFO线程被高优先级线程抢占时,它在就绪队列中的位置不变。
            // 因此一旦高优先级线程终止或阻塞,被抢占的FIFO线程将会立即继续运行。
        // SCHED_RR - 轮转策略。
            // 给每个RR线程分配一个时间片,一但RR线程的时间片耗尽,系统即将移到就绪队列的末尾。
        // SCHED_OTHER(缺省) - 普通策略。
            // 静态优先级为0。任何就绪的FIFO线程或RR线程,都会抢占此类线程。    

    // 调度参数
    struct sched_param schedparam;
        // struct sched_param {
        //     int sched_priority; /* 静态优先级 */
        // };
    

    // 栈尾警戒区大小(字节)  缺省一页(4096字节)。
    size_t guardsize;

    // 栈地址
    void* stackaddr;

    // 栈大小(字节)
    size_t stacksize;
} pthread_attr_t;

注意:不要手动读写该结构体,而应调用pthread_attr_set/get函数设置/获取具体属性项。

设置线程属性:

初始化线程属性结构体:
pthread_attr_t attr = {}; // 不要使用这种方式
int pthread_attr_init (pthread_attr_t* attr);
设置具体线程属性项:
int pthread_attr_setdetachstate (pthread_attr_t* attr,int detachstate);
int pthread_attr_setscope (pthread_attr_t* attr,int scope);
int pthread_attr_setinheritsched (pthread_attr_t* attr,int inheritsched);
int pthread_attr_setschedpolicy (pthread_attr_t* attr,int policy);
int pthread_attr_setschedparam (pthread_attr_t* attr,const struct sched_param* param);
int pthread_attr_setguardsize (pthread_attr_t* attr,size_t guardsize);
int pthread_attr_setstackaddr (pthread_attr_t* attr,void* stackaddr);
int pthread_attr_setstacksize (pthread_attr_t* attr,size_t stacksize);
int pthread_attr_setstack (pthread_attr_t* attr,void* stackaddr, size_t stacksize);
以设置好的线程属性结构体为参数创建线程:
int pthread_create (pthread_t* restrict thread,
                    const pthread_attr_t* testrict attr,
                    void* (*start_routine) (void*),
                    void* restrict arg);
销毁线程属性结构体:
int pthread_attr_destroy (pthread_attr_t* attr);

获取线程属性:

获取线程属性结构体:
int pthread_getattr_np (pthread_t thread,pthread_attr_t* attr);
获取具体线程属性项:
int pthread_attr_getdetachstate (pthread_attr_t* attr,int* detachstate);
int pthread_attr_getscope (pthread_attr_t* attr,int* scope);
int pthread_attr_getinheritsched (pthread_attr_t* attr,int* inheritsched);
int pthread_attr_getschedpolicy (pthread_attr_t* attr,int* policy);
int pthread_attr_getschedparam (pthread_attr_t* attr,struct sched_param* param);
int pthread_attr_getguardsize (pthread_attr_t* attr,size_t* guardsize);
int pthread_attr_getstackaddr (pthread_attr_t* attr,void** stackaddr);
int pthread_attr_getstacksize (pthread_attr_t* attr,size_t* stacksize);
int pthread_attr_getstack (pthread_attr_t* attr,void** stackaddr, size_t* stacksize);
以上所有函数成功返回0,失败返回错误码。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define __USE_GNU
#include <pthread.h>

int printattrs (pthread_attr_t* attr) 
{
	printf("------- 线程属性 -------\n");

	int detachstate;
	int error = pthread_attr_getdetachstate (attr, &detachstate);
	if (error) 
	{
		fprintf (stderr, "pthread_attr_getdetachstate: %s\n",strerror (error));
		return -1;
	}
	printf("分离状态:  %s\n",
		(detachstate == PTHREAD_CREATE_DETACHED) ? "分离线程" :
		(detachstate == PTHREAD_CREATE_JOINABLE) ? "可汇合线程" :
		"未知");

	int scope;
	if ((error = pthread_attr_getscope (attr, &scope)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getscope: %s\n",
			strerror (error));
		return -1;
	}
	printf ("竞争范围:  %s\n",
		(scope == PTHREAD_SCOPE_SYSTEM)  ? "系统级竞争" :
		(scope == PTHREAD_SCOPE_PROCESS) ? "进程级竞争" : "未知");

	int inheritsched;
	if ((error = pthread_attr_getinheritsched (attr,
		&inheritsched)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getinheritsched: %s\n",
			strerror (error));
		return -1;
	}
	printf ("继承特性:  %s\n",
		(inheritsched == PTHREAD_INHERIT_SCHED)  ? "继承调用属性" :
		(inheritsched == PTHREAD_EXPLICIT_SCHED) ? "显式调用属性" :
		"未知");

	int schedpolicy;
	if ((error = pthread_attr_getschedpolicy(attr,&schedpolicy)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getschedpolicy: %s\n",strerror (error));
		return -1;
	}
	printf ("调度策略:  %s\n",
		(schedpolicy == SCHED_OTHER) ? "普通" :
		(schedpolicy == SCHED_FIFO)  ? "先进先出" :
		(schedpolicy == SCHED_RR)    ? "轮转" : "未知");

	struct sched_param schedparam;
	if ((error = pthread_attr_getschedparam (attr, &schedparam)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getschedparam: %s\n",strerror (error));
		return -1;
	}
	printf ("调度优先级:%d\n", schedparam.sched_priority);

	size_t guardsize;
	if ((error = pthread_attr_getguardsize(attr, &guardsize)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getguardsize: %s\n",strerror (error));
		return -1;
	}
	printf ("栈尾警戒区:%u字节\n", guardsize);
	/*
	void* stackaddr;
	if ((error = pthread_attr_getstackaddr (attr, &stackaddr)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getstackaddr: %s\n",strerror (error));
		return -1;
	}
	printf ("栈地址:    %p\n", stackaddr);

	size_t stacksize;
	if ((error = pthread_attr_getstacksize (attr, &stacksize)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getstacksize: %s\n",strerror (error));
		return -1;
	}
	printf ("栈大小:    %u字节\n", stacksize);
	*/
	void* stackaddr;
	size_t stacksize;
	if ((error = pthread_attr_getstack (attr, &stackaddr,&stacksize)) != 0) 
	{
		fprintf (stderr, "pthread_attr_getstack: %s\n",strerror (error));
		return -1;
	}
	printf ("栈地址:    %p\n", stackaddr);
	printf ("栈大小:    %u字节\n", stacksize);

	printf("------------------------\n");

	return 0;
}

void* thread_proc (void* arg) 
{
	pthread_attr_t attr;
	int error = pthread_getattr_np (pthread_self (), &attr);
	if (error) 
	{
		fprintf (stderr, "pthread_getattr_np: %s\n", strerror (error));
		exit (EXIT_FAILURE);
	}

	if (printattrs (&attr) < 0)
		exit (EXIT_FAILURE);

	exit (EXIT_SUCCESS);

	return NULL;
}

int main (int argc, char* argv[]) 
{
	int error;
	pthread_attr_t attr, *pattr = NULL;

	if (argc > 1) 
	{
		if (strcmp (argv[1], "-s")) 
		{
			fprintf (stderr, "用法:%s [-s]\n", argv[0]);
			return -1;
		}

		if ((error = pthread_attr_init (&attr)) != 0) 
		{
			fprintf (stderr, "pthread_attr_init: %s\n",strerror (error));
			return -1;
		}

		if ((error = pthread_attr_setdetachstate (&attr,PTHREAD_CREATE_DETACHED)) != 0) 
		{
			fprintf (stderr, "pthread_attr_setdetachstate: %s\n",strerror (error));
			return -1;
		}

		if ((error = pthread_attr_setinheritsched (&attr,PTHREAD_EXPLICIT_SCHED)) != 0) 
		{
			fprintf (stderr, "pthread_attr_setinheritsched: %s\n",strerror (error));
			return -1;
		}

		if ((error = pthread_attr_setstacksize (&attr, 4096*10)) != 0) 
		{
			fprintf (stderr, "pthread_attr_setstack: %s\n",strerror (error));
			return -1;
		}

		pattr = &attr;
	}

	pthread_t tid;
	if ((error = pthread_create (&tid, pattr, thread_proc,NULL)) != 0) 
	{
		fprintf (stderr, "pthread_create: %s\n", strerror (error));
		return -1;
	}

	if (pattr)
	{
		if ((error = pthread_attr_destroy (pattr)) != 0) 
		{
			fprintf (stderr, "pthread_attr_destroy: %s\n",strerror (error));
			return -1;
		}
	}

	pause ();
	return 0;
}

注意:如果man手册查不到线程的相关函数,安装完整版gnu手册:sudo apt-get install glibc-doc。

练习:实现大文件的多线程cp拷贝,对比系统cp命令,哪个速度更快,为什么?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>

typedef struct Task
{
	char* src;
	char* dest;
	size_t start;
	size_t end;
}Task;

void* run(void* arg)
{
	Task* task = arg;

	// 打开源文件和目标文件
	FILE* src_fp = fopen(task->src,"r");
	FILE* dest_fp = fopen(task->dest,"a");
	if(NULL == src_fp || NULL == dest_fp)
	{
		perror("fopen");
		return NULL;
	}

	// 调整文件的位置指针
	fseek(src_fp,task->start,SEEK_SET);
	fseek(dest_fp,task->start,SEEK_SET);

	// 创建缓冲区
	char buf[1024];
	size_t buf_size = sizeof(buf);

	for(int i=task->start; i<task->end; i+=buf_size)
	{
		int ret = fread(buf,1,buf_size,src_fp);
		if(0 >= ret)
			break;
		fwrite(buf,1,ret,dest_fp);
	}

	fclose(src_fp);
	fclose(dest_fp);
	free(task);
}

int main(int argc,const char* argv[])
{
	if(3 != argc)
	{
		puts("Use:./cp <src> <dest>");
		return 0;
	}

	// 获取到文件的大小
	struct stat buf;
	if(stat(argv[1],&buf))
	{
		perror("stat");
		return -1;
	}

	// 创建出目标文件
	if(NULL == fopen(argv[2],"w"))
	{
		perror("fopen");
		return -2;
	}

	// 计算需要的线程数量,以100M为单位
	size_t pthread_cnt = buf.st_size/(1024*1024*100)+1;

	// 分配任务
	pthread_t tid;
	for(int i=0; i<pthread_cnt; i++)
	{
		Task* task = malloc(sizeof(Task));
		task->src = (char*)argv[1];
		task->dest = (char*)argv[2];
		task->start = i*1024*1024*100;
		task->end = (i+1)*1024*1024*100;

		// 创建子线程并分配任务
		pthread_create(&tid,NULL,run,task);

		// 分享子线程
		pthread_detach(tid);
	}
	
	// 结束主线程
	pthread_exit(NULL);
}

多线程并不能提高运行速度,反而可能会降低,所以多线程不适合解决运算密集性问题,而是适合解决等待、阻塞的问题,如果使用进程去等待,会浪费大量资源,所以使用更轻量的线程去等待,节约资源。

一、线程同步

​ 同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合。

​ 如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A,A再继续操作。

​ 在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

注意:同一个进程内存的多个线程之间,除了栈内存是独立的,其他资源全部共享。

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

int num = 0;
void* run(void* arg)
{
    for(int i=0; i<1000000; i++)
    {
        // 加锁
        num++;
        // 解锁
    }    
}

int main(int argc,const char* argv[])
{
    pthread_t tid1,tid2;
    pthread_create(&tid1,NULL,run,NULL);
    pthread_create(&tid2,NULL,run,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("%d\n",num);
}

线程A 线程A

读取

运算 读取

回写 运算

​ 回写

二、互斥锁

注意:如果man手册中查不到这系列函数,可以安装以下内容:
    sudo apt-get install glibc-doc
    sudo apt-get install manpages-posix-dev
    
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
功能:定义并初始化互斥锁
    
int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);
功能:初始化一互斥锁,会被初始化为非锁定状态

int pthread_mutex_lock (pthread_mutex_t* mutex);
功能:加锁,当互斥锁已经是锁定状态时,调用者会阻塞,直到互斥被解开,当前线程才会加锁成功并返回。

int pthread_mutex_unlock (pthread_mutex_t* mutex);
功能:解锁,解锁后等待加锁的线程才能加锁成功。

int pthread_mutex_destroy (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 abs_timeout);
功能:倒计时加锁,如果超时还不加上则立即返回。
struct timespec{
	time_t tv_sec;        /* Seconds.  */
	long int tv_nsec;     /* Nanoseconds.*/ 1秒= 1000000000 纳秒
};
#include <stdio.h>
#include <pthread.h>
/*
执行流程:
	1、互斥锁被初始化为非锁定状态
	2、线程1调用pthread_mutex_lock函数,立即返回,互斥量呈锁定状态;
	3、线程2调用pthread_mutex_lock函数,阻塞等待;
	4、线程1调用pthread_mutex_unlock函数,互斥量呈非锁定状态;
	5、线程2被唤醒,从pthread_mutex_lock函数中返回,互斥量呈锁定状态
*/

pthread_mutex_t mutex;
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int num = 0;
void* run(void* arg)
{
    for(int i=0; i<1000000; i++)
    {
        pthread_mutex_lock(&mutex);
        num++;
        pthread_mutex_unlock(&mutex);
    }
}

int main(int argc,const char* argv[])
{
    pthread_mutex_init(&mutex,NULL);
    pthread_t pid1,pid2;
    pthread_create(&pid1,NULL,run,NULL);
    pthread_create(&pid2,NULL,run,NULL);
    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
    pthread_mutex_destroy(&mutex);
    printf("%d\n",num);
}

三、读写锁

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
功能:定义并初始化读写锁

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_tryrdlock(pthread_rwlock_t *rwlock);
功能:尝试加读锁,如果不能加则立即返回

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
功能:尝试加写锁,如果不能加则立即返回

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
           const struct timespec *restrict abstime);
功能:带倒计时加读锁,超时则立即返回
    
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
           const struct timespec *restrict abstime);
功能:带倒计时加写锁,超时则立即返回

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:销毁读写锁
    
使用读写锁的线程应根据后续的操作进行加锁,如果只对数据进行读取则只加读锁即可,只有对数据进行修改时才应该加写锁,与互斥锁的区别是,它能让只读的线程加上锁,使用原理与文件锁一样。
    线程A		线程B
    读锁		读锁 	OK
    读锁		写锁	NO
    写锁		读锁	NO
    写锁		写锁	NO
    

练习:使用读写锁来解决同步问题。

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

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

int num = 0;
void* run(void* arg)
{
    for(int i=0; i<1000000; i++)
    {
		pthread_rwlock_wrlock(&rwlock);
        num++;
		pthread_rwlock_unlock(&rwlock);
    }    
}

int main(int argc,const char* argv[])
{
    pthread_t tid1,tid2;
    pthread_create(&tid1,NULL,run,NULL);
    pthread_create(&tid2,NULL,run,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
	pthread_rwlock_destroy(&rwlock);
    printf("%d\n",num);
}

四、死锁问题

什么是死锁:

​ 多个线程互相等待对方资源,在得到所需要的资源之前都不会释放自己的资源,然后造成循环等待的现象,称为死锁。

死锁产生四大必要条件:

​ 1、资源互斥

​ 2、占有且等待

​ 3、资源不可剥夺

​ 4、环路等待

​ 以上四个条件缺一不可,只要有一个不满足就不能构成死锁。

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

// 创建三个互斥锁并初始化
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;

void* run1(void* arg)
{
    pthread_mutex_lock(&mutex1);
    usleep(100);
    pthread_mutex_lock(&mutex2);
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}
void* run2(void* arg)
{
    pthread_mutex_lock(&mutex2);
    usleep(100);
    pthread_mutex_lock(&mutex3);
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex3);
    pthread_mutex_unlock(&mutex2);
}
void* run3(void* arg)
{
    pthread_mutex_lock(&mutex3);
    usleep(100);
    pthread_mutex_lock(&mutex1);
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex3);
}


int main(int argc,const char* argv[])
{
    // 创建三个线程
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1,NULL,run1,NULL);
    pthread_create(&tid2,NULL,run2,NULL);
    pthread_create(&tid3,NULL,run3,NULL);

    // 主线程等待三个子线程结束
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    return 0;
}

如休防止出现死锁:

​ 构成死锁的四个条件只有一个不成立,就不会产生死锁了。

​ 1、破坏互斥条件,让资源能够共享使用(准备多份)。

​ 2、破坏占有且等待的条件,一次申请完成它所有需要的资源(把所有资源进行打包,用一把锁来代表,拿到这反锁就相当于拿到的所有资源),资源没有满足前不让它运行,一旦开始运行就一直归它所有, 缺点是系统资源会被浪费。

​ 3、破坏不可剥夺的条件,当已经占有了一些资源,请求新的资源而获取不到,然后就释放已经获取到的资源,缺点是实现起来比较复杂,释放已经获取到的资源可能会造成前一阶段的工作浪费。

​ 4、破坏循环等待的条件,采用顺序分配资源的方法,在系统中为资源进行编号,规定线程必须按照编号递增的顺序获取资源,缺点是资源必须相对稳定,这样就限制了资源的增加和减少。

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

// 创建三个互斥锁并初始化
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;

void* run1(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex1);
        usleep(100);
        if(0 == pthread_mutex_trylock(&mutex2))
            break;
        pthread_mutex_unlock(&mutex1);

    }

    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}
void* run2(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex2);
        usleep(100);
        if(0 == pthread_mutex_trylock(&mutex3))
            break;
        pthread_mutex_unlock(&mutex2);
    }
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex3);
    pthread_mutex_unlock(&mutex2);
}
void* run3(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex3);
        usleep(100);
        if(0 == pthread_mutex_trylock(&mutex1))
            break;
        pthread_mutex_unlock(&mutex3);
    }
    printf("没有构成死锁!!!\n");
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex3);
}


int main(int argc,const char* argv[])
{
    // 创建三个线程
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1,NULL,run1,NULL);
    pthread_create(&tid2,NULL,run2,NULL);
    pthread_create(&tid3,NULL,run3,NULL);

    // 主线程等待三个子线程结束
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);
    return 0;
}

检测死锁的方法:

​ 总体思路:观察+分析

​ 方法1:阅读代码,分析各线程的加锁步骤。

​ 方法2:使用strace追踪程序的执行流程。

​ 方法3:查看日志观察程序的业务执行过程。

​ 方法4:使用gdb调试,查看各线程的执行情况。

1、把断点打在线程创建完毕后
2、run
3、info threads 查看所有线程
4、thread n 进程指定的线程
5、bt 查看线程堆栈信息
6、配合s/n单步调试

什么是死锁?

构成死锁的4个必要条件?

如何避免死锁?

如何判断程序是否陷入死锁?

五、原子操作

​ 所谓的原子操作就是不可被拆分的操作,对于多线程对全局变量进行操作时,就再也不用再线程锁了,和pthread_mutex_t保护作用是一样的,也是线程安全的,有些编译器在使用时需要加-march=i686编译参数。

type __sync_fetch_and_add (type *ptr, type value);	// +
type __sync_fetch_and_sub (type *ptr, type value);	// -
type __sync_fetch_and_and (type *ptr, type value);	// &
type __sync_fetch_and_or (type *ptr, type value);	// |
type __sync_fetch_and_nand (type *ptr, type value);	// ~
type __sync_fetch_and_xor (type *ptr, type value);	// ^
功能:以上操作返回的是*ptr的旧值

type __sync_add_and_fetch (type *ptr, type value); 	// +
type __sync_sub_and_fetch (type *ptr, type value);	// -
type __sync_and_and_fetch (type *ptr, type value);	// &
type __sync_or_and_fetch (type *ptr, type value);	// |
type __sync_nand_and_fetch (type *ptr, type value);	// ~
type __sync_xor_and_fetch (type *ptr, type value);	// ^
功能:以上操作返回的是*ptr与value计算后的值
    
type __sync_lock_test_and_set (type *ptr, type value);
功能:把value赋值给*ptr,并返回*ptr的旧值
    
__sync_lock_release(type *ptr);
功能:将*ptr赋值为0
#include <stdio.h>
#include <pthread.h>

int num = 0;
void* run(void* arg)
{
    for(int i=0; i<100000000; i++)
    {
		__sync_fetch_and_add(&num,1);
    }
}

int main(int argc,const char* argv[])
{
    pthread_t pid1,pid2;
    pthread_create(&pid1,NULL,run,NULL);
    pthread_create(&pid2,NULL,run,NULL);
    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
    printf("%d\n",num);
}

原子操作的优点:

​ 1、速度贼快

​ 2、不会产生死锁

原子操作的缺点:

​ 1、该功能并不通用,有些编译器不支持。

​ 2、type只能是整数相关的类型,浮点型和自定义类型无法使用。

练习1:

​ 使用读写锁或互斥锁实现一个线程安全队列。

#include "queue.h"
#include <stdlib.h>

Node* create_node(TYPE data)
{
	Node* node = malloc(sizeof(Node));
	node->data = data;
	node->next = NULL;
	return node;
}

Queue* create_queue(void)
{
	Queue* queue = malloc(sizeof(Queue));
	pthread_rwlock_init(&queue->lock,NULL);
	queue->front = NULL;
	queue->rear = NULL;
	return queue;
}

bool empty_queue(Queue* queue)
{
	pthread_rwlock_rdlock(&queue->lock);
	bool flag = NULL == queue->front;
	pthread_rwlock_unlock(&queue->lock);
	return flag;
}

void push_queue(Queue* queue,TYPE data)
{
	Node* node = create_node(data);
	if(empty_queue(queue))
	{
		pthread_rwlock_wrlock(&queue->lock);
		queue->front = node;
		queue->rear = node;
	}
	else
	{
		pthread_rwlock_wrlock(&queue->lock);
		queue->rear->next = node;
		queue->rear = node;
	}
	pthread_rwlock_unlock(&queue->lock);
}

bool pop_queue(Queue* queue)
{
	if(empty_queue(queue))
		return false;

	pthread_rwlock_wrlock(&queue->lock);
	Node* tmp = queue->front;
	queue->front = tmp->next;
	pthread_rwlock_unlock(&queue->lock);

	free(tmp);
	return true;
}

TYPE top_queue(Queue* queue)
{
	pthread_rwlock_rdlock(&queue->lock);
	TYPE data = queue->front->data;
	pthread_rwlock_unlock(&queue->lock);
	return data;
}

void destroy_queue(Queue* queue)
{
	while(!empty_queue(queue))
		pop_queue(queue);

	pthread_rwlock_destroy(&queue->lock);
	free(queue);
}

int main(void)
{
	Queue* queue = create_queue();
	for(int i=0; i<10; i++)
	{
		push_queue(queue,i);
		printf("push %d\n",i);
	}

	while(!empty_queue(queue))
	{
		printf("top %d\n",top_queue(queue));
		pop_queue(queue);
	}
}

练习2:

​ 使用原子操作实现一个线程安全的无锁队列。

//queue->rear = (queue->rear+1)%queue->cap;
if(queue->rear == queue->cap)
{
    queue->rear = 0;
}
else
{
    __sync_fetch_and_add(&queue->rear,1);
}

queue->front = (queue->front+1)%queue->cap;
if(queue->front == queue->cap)
{
    queue->front = 0;
}
else
{
    __sync_fetch_and_add(&queue->front,1);
}

六、生产者与消费者模型

img

生产者:生产数据的线程,这类的线程负责从用户端、客户端接收数据,然后把数据Push到存储中介。

消费者:负责消耗数据的线程,对生产者线程生产的数据进行(判断、筛选、使用、响应、存储)处理。

存储中介:也叫数据仓库,是生产者线程与消费者线程之间的数据缓冲区,用于平衡二者之间的生产速度与消耗速度不均衡的问题,通过缓冲区隔离生产者和消费者,与二者直连相比,避免相互等待,提高运行效率。

问题1:生产快于消费,缓冲区满,撑死。

解决方法:负责生产的线程通知负责消费的线程全速消费,然后进入休眠。

问题2:消费快于生产,缓冲区空,饿死。

解决方法:负责消费的线程通知负责生产的线程全速生产,然后进入休眠。

七、条件变量

​ 条件变量是利用线程间共享的"全局变量"进行同步的一种机制,主要包括两个动作:

​ 1、线程等待"条件变量的条件成立"而休眠;

​ 2、等"条件成立"叫醒休眠的线程。

​ 为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起,一般线程睡入条件变量,伴随着解锁动作,而线程从条件变量醒来时,伴随着加锁动作,如果加锁失败线程进入阻塞状态,而不是睡眠。

// 定义或创建条件变量
pthread_cond_t cond;

// 初始化条件变量
int pthread_cond_init (pthread_cond_t* cond,const pthread_condattr_t* attr);
//亦可pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 使调用线程睡入条件变量cond,同时释放互斥锁mutex
int pthread_cond_wait (pthread_cond_t* cond,pthread_mutex_t* mutex);

// 带倒计时的睡眠,时间到了会自动醒来
int pthread_cond_timedwait (pthread_cond_t* cond,
    pthread_mutex_t* mutex,
    const struct timespec* abstime);

struct timespec {
    time_t tv_sec;  // Seconds
    long   tv_nsec; // Nanoseconds [0 - 999999999]
};

// 从条件变量cond中叫醒一个线程,令其重新获得原先的互斥锁
int pthread_cond_signal (pthread_cond_t* cond);
注意:被唤出的线程此刻将从pthread_cond_wait函数中返回,
但如果该线程无法获得原先的锁,则会继续阻塞在加锁上。

// 从条件变量cond中唤醒所有线程
int pthread_cond_broadcast (pthread_cond_t* cond);

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

注意:使用互斥锁配合条件变量实现的生产者与消费者模型,能够平衡生产与消费的时间不协调,并且可以最大限度的节约运行资源。

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* run(void* arg)
{
	int index = 1;
	for(;;)
	{
		pthread_mutex_lock(&mutex);

		if(0 == index % 10)
		{
			printf("任务已完成,即将睡眠!\n");
			pthread_cond_wait(&cond,&mutex);
		}

		printf("index = %d\n",index++);
		sleep(1);
		pthread_mutex_unlock(&mutex);
	}
}

int main(int argc,const char* argv[])
{
	pthread_t tid;
	pthread_create(&tid,NULL,run,NULL);

	printf("是否叫醒睡眠的线程?");
	for(;;)
	{
		char cmd = getchar();
		if('y' == cmd)
		{
			pthread_cond_signal(&cond);
		}
	}
	pthread_join(tid,NULL);
	return 0;
}

八、信号量

多线程使用的信号量:

#include <semaphore.h>
sem_t sem;

int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:给信号量设置初始值
pshared:信号量的使用范围
    0 线程间使用
    nonzero 进程之间使用

int sem_wait(sem_t *sem);
功能:信号量减1操作,如果信号量已经等于0,则阻塞

int sem_trywait(sem_t *sem);
功能:尝试对信号量减1操作,能减返回0成功,不能减返回-1失败,不会阻塞

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:带倒计时的对信号减1操作,能减返回0成功,不能减超时返回-1失败,阻塞abs_timeout一段时间

int sem_post(sem_t *sem);
功能:对信号量执行加1操作

int sem_getvalue(sem_t *sem, int *sval);
功能:获取信号量的值

int sem_destroy(sem_t *sem);
功能:销毁信号量

多进程使用的信号量:

sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
功能:在内核创建一个信号量对象
name:信号量的名字
oflag:
    O_CREAT	不存在则创建信号量,存在则获取
    O_EXCL	如果信号量已经存在,返回失败
mode:信号量的权限
value:信号量的初始值
    
sem_t *sem_open(const char *name, int oflag);
功能:获取信号,或相关属性
    
int sem_unlink(const char *name);
功能:删除信号量

标签:attr,int,queue,mutex,pthread,线程,操作系统
From: https://www.cnblogs.com/sleeeeeping/p/18374616

相关文章

  • 操作系统-进程
    一、进程1、进程介绍进程与程序:程序是存储在磁盘上的可执行文件,里面包含可执行的机器指令和数据的静态实体;进程是处于活跃状态的计算机程序,也就是正在运行中的程序一个运行中的程序,可能由多个进程组成,但至少要有一个进程,称为主进程,同时可以通过系统调用创建出若干个子进程同......
  • Node.js获取操作系统指标和参数
    constos=require('os');console.log("操作系统临时文件夹os.tmpdir():"+os.tmpdir());console.log("CPU的字节序os.endianness():"+os.endianness());console.log("操作系统主机名os.hostname():"+os.hostname());console.log("操作......
  • 电脑三大操作系统
    电脑需要运行,那就必须要有一个操作系统,一般情况下,电脑所装的系统是windows系统,除此之外,电脑的操作系统有很多的,windows是使用最多的一种,是微软公司的产品。下面就介绍下三大电脑操作系统,供大家参考。1、Windows使用最多MicrosoftWindows是美国微软公司研发的一套操作系统......
  • Java线程池实现原理及在美团业务中的实践
    Java线程池实现原理及在美团业务中的实践随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一......
  • Java当中有几种方式来创建线程执行任务?
    1.继承thread类publicclassyxextendsThreads{publicstaticvoidmain(String[]args)[yxthread=newyx();thread.start();}@Overridepublicvoidrun(){system.out.println("helloyx&qu......
  • Msyql导出数据只占一个线程为什么会对线上环境有影响
    mysqldump在导出数据时,虽然只占用一个线程,但它仍然可能对线上环境产生影响。这些影响主要体现在以下几个方面:1.I/O负载mysqldump在导出数据时需要从磁盘读取大量数据,并将其写入到导出的文件中。这会增加数据库服务器的磁盘I/O负载。如果服务器上同时有其他应用程序或查询......
  • 多线程和多线程同步
    多线程和多线程同步多线程编程是现代软件开发中的一项关键技术,在多线程编程中,开发者可以将复杂的任务分解为多个独立的线程,使其并行执行,从而充分利用多核处理器的优势。然而,多线程编程也带来了挑战,例如线程同步、死锁和竞态条件等问题。本篇文章将深入探讨多线程编程的基本概念(......
  • 操作系统之面试常考
    【转载】:https://www.cnblogs.com/zyf-zhaoyafei/p/4714598.html最近这段时间正在积极准备面试,复习到操作系统部分,本篇文章就介绍操作系统基础内容,参考第四版《计算机操作系统》这本文章总结了面试中常考、常用到的基本知识点,希望对准备面试的同学和想回顾操作系统知识点的程序......
  • 【OS系列】程序、进程与线程之区别大揭秘,一图读懂胜千言
    1.程序(Program)程序是一组指令的集合,它存储在磁盘上,是一个静态的实体。程序本身并不执行任何操作,它只是提供了一个执行的蓝图。例如,一个编译好的可执行文件(如Windows的.exe文件)就是一个程序。2.进程(Process)进程是程序的一次执行实例,是操作系统进行资源分配和调度的基本......
  • /* 线程读取循环队列*/
    /*线程读取循环队列*/#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<unistd.h>#defineQUEUE_SIZE5typedefstruct{intdata[QUEUE_SIZE];intfront;intrear;pthread_mutex_tlock;}CircularQueue......