线程和线程同步

线程

认识线程

什么是线程

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

线程与进程关系

  • 轻量级进程(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
······

猜你喜欢

转载自blog.csdn.net/qq_46485161/article/details/115191048
今日推荐