线程和线程同步

本文深入探讨了线程的概念,线程与进程的关系,以及线程间的资源共享情况。重点介绍了线程的创建、终止、同步和互斥量的使用,包括pthread库中的关键函数,如pthread_create、pthread_join、pthread_mutex_lock等。通过实例展示了如何在多线程环境中避免死锁和实现有效的资源访问控制。此外,还讨论了条件变量在解决线程同步问题中的作用。
摘要由CSDN通过智能技术生成

线程

认识线程

什么是线程

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程也就是一个轻量级进程,每个线程都有自己的线程控制块,即一个进程至少有一个轻量级进程,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在线程组里面,所有的线程都是对等的关系,没有父线程的概念。

线程与进程关系

  • 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  • 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
  • 进程可以蜕变成线程
  • 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

线程间资源共享情况

共享资源:

  • 文件描述符表
  • 每种信号的处理方式
  • 当前工作目录
  • 用户ID和组ID
  • 内存地址空间

非共享资源:

  • 线程id
  • 处理器现场和栈指针(内核栈)
  • 独立的栈空间(用户空间栈)
  • errno变量
  • 信号屏蔽字
  • 调度优先级

线程优缺点

优点:①提高程序的并发性;②开销小,不用重新分配内存;③通信和共享数据方便。

缺点:①线程不稳定(库函数实现);②线程调试比较困难(gdb支持不好);③线程无法使用unix经典事件,例如信号;

线程原语

pthread_create

#include <pthread.h>

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

/*参数释义:
pthread_t *thread:传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
const pthread_attr_t *attr:线程属性设置,如使用默认属性,则传NULL
void *(*start_routine) (void *):函数指针,指向新线程应该加载执行的函数模块
void *arg:指定线程将要加载调用的那个函数的参数
返回值:成功返回0,失败返回错误号。以前学过的系统函数都是成功返回0,失败返回-1,而错误号保存在全局变量errno中,而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。*/
//注(1):创建线程时,没什么特殊情况我们都是使用默认属性的,不过有时候需要做一些特殊处理,碧如调整优先级啊这些的。

pthread_self

获取调用线程tid

#include<pthread.h>

pthread_t pthread_self(void);

代码示例:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_t ntid;
void printids(const char *s)
{
	pid_t pid;
	pthread_t tid;
	pid = getpid();
	tid = pthread_self();
	printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
	(unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg)
{
	printids(arg);
	return NULL;
}
int main()
{
	int err;
	pthread_t ntid;
	err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
	if (err != 0) 
	{
		fprintf(stderr, "can't create thread: %s\n", strerror(err));
		exit(1);
	}
	printids("main thread:");
	sleep(1);
	return 0;
}
结果:
new thread:  pid 4721 tid 3087018896 (0xb8002b90)
main thread: pid 4721 tid 3087021760 (0xb80036c0)

pthread_exit

调用线程退出函数,注意和exit函数的区别,任何线程里exit导致进程退出,其他线程
未工作结束,主控线程退出时不能return或exit。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是
用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函
数已经退出了

#include <pthread.h>

void pthread_exit(void *retval);

参数释义
retval:线程退出时传递出的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。

pthread_join

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

参数释义:
thread:回收线程的tid
retval:接收退出线程传递出的返回值
返回值:成功返回0,失败返回错误号

函数阻塞调用线程直到thread所指定的线程终止。
如果在目标线程中调用pthread_exit(),程序员可以在主线程中获得目标线程的终止状态。

连接线程只能用pthread_join()连接一次。若多次调用就会发生逻辑错误。

说了这么多为什么要使用pthread_join()?
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到pthread_join()方法了。
可以这么简单的理解:主线程等待子线程的终止。也就是想调用pthread_join()方法后面的代码,只有等到子线程结束了才能继续执行。

pthread_cancel

在进程内某个线程可以取消另一个线程

#include <pthread.h>

int pthread_cancel(pthread_t thread);

代码实例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thr_fn1(void *arg)
{
	printf("thread 1 returning\n");
	return (void *)1;
}
void *thr_fn2(void *arg)
{
	printf("thread 2 exiting\n");
	pthread_exit((void *)2);
}
void *thr_fn3(void *arg)
{
	while(1) 
	{
		printf("thread 3 writing\n");
		sleep(1);
	}
}
int main(void)
{
	pthread_t tid;
	void *tret;
	pthread_create(&tid, NULL, thr_fn1, NULL);
	pthread_join(tid, &tret);
	printf("thread 1 exit code %d\n", (int)tret);
	pthread_create(&tid, NULL, thr_fn2, NULL);
	pthread_join(tid, &tret);
	printf("thread 2 exit code %d\n", (int)tret);
	pthread_create(&tid, NULL, thr_fn3, NULL);
	sleep(3);
	pthread_cancel(tid);
	pthread_join(tid, &tret);
	printf("thread 3 exit code %d\n", (int)tret);
	return 0;
}

结果:
thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1

pthread_detach

分离线程

#include <pthread.h>

int pthread_detach(pthread_t tid);

参数释义
tid:分离线程tid
返回值:成功返回0,失败返回错误号。

注意:一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

代码实例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
void *thr_fn(void *arg)
{
	int n = 3;
	while (n--)
	{
		printf("thread count %d\n", n);
		sleep(1);
	}
	return (void *)1;
}
int main(void)
{
	pthread_t tid;
	void *tret;
	int err;
	pthread_create(&tid, NULL, thr_fn, NULL);
	//第一次运行时注释掉下面这行,第二次再打开,分析两次结果
	pthread_detach(tid);
	while (1)
	{
	err = pthread_join(tid, &tret);
	if (err != 0)
		fprintf(stderr, "thread %s\n", strerror(err));
	else
		fprintf(stderr, "thread exit code %d\n", (int)tret);
	sleep(1);
	}
	return 0;
}
1、注释:pthread_detach(tid);
结果:
thread count 2
thread count 1
thread count 0
thread exit code 1
thread No such process
thread No such process
2、不注释:pthread_detach(tid);
结果:
thread count 2
thread Invalid argument
thread count 1
thread Invalid argument
thread count 0
thread Invalid argument
thread Invalid argument

pthread_equal

比较两个线程是否相等

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

线程终止方式

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

①从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。

②一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

③线程可以调用pthread_exit终止自己。

线程同步

线程为什么要同步

  • 共享资源,多个线程都可对共享资源操作
  • 线程操作共享资源的先后顺序不确定
  • 处理器对存储器的操作一般不是原子操作

互斥量

临界区(Critical Section)

保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么 在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

临界区选定

临界区的选定因尽可能小,如果选定太大会影响程序的并行处理性能。

mutex操作原语

pthread_mutex_t mutex = PTHREAD_MUREX_INITALIZER //用于初始化互斥锁,后面简称锁

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); //初始化锁,和上面那个一个意思。
//初始化一个互斥锁(互斥量)–>初值可看做1
int pthread_mutex_destroy(pthread_mutex_t *mutex); //销毁锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //上锁
int pthread_mutex_unlok(pthread_mutex_t *mutex); //解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试上锁

扩展:

  • restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
  • 静态初始化:如果互斥锁mutex是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 动态初始化:局部变量应采用动态初始化。pthread_mutex_init(&mutex, NULL);
  • attr对象用于设置互斥量对象的属性,使用时必须声明为pthread_mutextattr_t类型,默认值可以是NULL。Pthreads标准定义了三种可选的互斥量属性:
    ①协议(Protocol): 指定了协议用于阻止互斥量的优先级改变
    ② 优先级上限(Prioceiling):指定互斥量的优先级上限
    ③进程共享(Process-shared):指定进程共享互斥量

互斥锁有什么作用?
采用互斥锁保护临界区,从而防止竞争条件。也就是说,一个进程在进入临界区时应得到锁;它在退出临界区时释放锁。

互斥量实例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NLOOP 5
int counter; /* incremented by threads */
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
void *doit(void *);
int main(int argc, char **argv)
{
	pthread_t tidA, tidB;
	pthread_create(&tidA, NULL, doit, NULL);
	pthread_create(&tidB, NULL, doit, NULL);
	/* wait for both threads to terminate */
	pthread_join(tidA, NULL);
	pthread_join(tidB, NULL);
	return 0;
}
void *doit(void *vptr)
{
	int i, val;
	for (i = 0; i < NLOOP; i++) 
	{
		pthread_mutex_lock(&counter_mutex);
		val = counter;
		printf("%x: %d\n", (unsigned int)pthread_self(), val + 1);
		counter = val + 1;
		pthread_mutex_unlock(&counter_mutex);
	}
	return NULL;
}
//从运行结果不难看出,线程tidA首先进行循环打印,等到tidA完成释放资源后才轮到线程tidB
运行结果:
b7f80b90: 1
b7f80b90: 2
b7f80b90: 3
b7f80b90: 4
b7f80b90: 5
b757fb90: 6
b757fb90: 7
b757fb90: 8
b757fb90: 9
b757fb90: 10

死锁

死锁问题是多线程特有的问题,它可以被认为是线程间切换消耗系统性能的一种极端情况。在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。死锁问题是在多线程开发中应该坚决避免和杜绝的问题。

线程死锁的原因

(1)互斥条件:一个资源每次只能被一个线程使用。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

解决方法:如果发生了死锁,那么只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。

条件变量

  • 条件变量提供了另一种同步的方式。互斥量通过控制对数据的访问实现了同步,而条件变量允许根据实际的数据值来实现同步。
  • 没有条件变量,程序员就必须使用线程去轮询(可能在临界区),查看条件是否满足。这样比较消耗资源,因为线程连续繁忙工作。条件变量是一种可以实现这种轮询的方式。
  • 条件变量往往和互斥一起使用

条件变量控制原语

//初始化条件变量:
//方法一:静态初始化
pthread_cont_t cont = PTHREAD_COND_INITIALIZER;
//方法二:动态初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//参数释义:cond:用于接收初始化成功管道条件变量
//attr:通常为NULL,且被忽略

//销毁
int pthread_cond_destroy(pthread_cond_t *cond);

//等待
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);  //计时等待

//等待唤醒
int pthread_cond_signal(pthread_cond_t *cptr); //唤醒一个等待该条件的线程。存在多个线程是按照其队列入队顺序唤醒其中一个
int pthread_cond_broadcast(pthread_cond_t * cptr); //广播,唤醒所有等待线程

实例:

生产者消费者模型:

#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
struct msg 
{
	struct msg *next;
	int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
	struct msg *mp;
	for(;;) 
	{
		pthread_mutex_lock(&lock);
		while (head == NULL)
			pthread_cond_wait(&has_product, &lock);
		mp = head;
		head = mp->next;
		pthread_mutex_unlock(&lock);
		printf("Consume %d\n", mp->num);
		free(mp);
		sleep(rand() % 5);
	} 
}
void *producer(void *p)
{
	struct msg *mp;
	for (;;) 
	{
		mp = malloc(sizeof(struct msg));
		mp->num = rand() % 1000 + 1;
		printf("Produce %d\n", mp->num);
		pthread_mutex_lock(&lock);
		mp->next = head;
		head = mp;
		pthread_mutex_unlock(&lock);
		pthread_cond_signal(&has_product);
		sleep(rand() % 5);
	}
}
int main(int argc, char *argv[])
{
	pthread_t pid, cid;
	srand(time(NULL));
	pthread_create(&pid, NULL, producer, NULL);
	pthread_create(&cid, NULL, consumer, NULL);
	pthread_join(pid, NULL);
	pthread_join(cid, NULL);
	return 0;
}

运行结果:
Produce 543
Consume 543
Produce 465
Consume 465
······
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

别呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值