一、线程同步
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据。如果当某个线程可以去修改变量,而其他线程也可以去读取或者修改这个变量的时候,就需要对这些线程进行同步控制,以确保它们在访问变量的存储内容时不会访问到无效的数值。
同步:多进程或者多线程访问临界资源时,必须进行同步控制。多进程或者多线程的执行并不是完全绝对的并行运行,有可能主线程需要等待函数线程的某些条件的发生。
多线程之间有几个特殊的临界资源:全局数据、堆区数据、文件描述符 多线程之间公用
线程间同步控制的方式:
(1)信号量
头文件: #include <semaphore.h>
获取:int sem_init(sem_t *sem, int shared, int value);
sem:是一个 sem_t 类型指针,指向信号量对象
shared:表示是否能在多个进程之间共享,Linux 下不支持,为 0
value:信号量的初始值
该函数成功时返回为 0,失败时返回为 -1
P 操作:int sem_wait(sem_t *sem); //阻塞运行
V 操作:int sem_post(sem_t *sem);
删除:int sem_destroy(sem_t *sem);
练习:主线程循环获取用户输入,函数线程统计用户输入的字符个数(统计一次需要 1 秒)
代码如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <assert.h> #include <pthread.h> char buffer[128] = {0}; pthread_mutex_t mutex; void *pthread_count(void *arg) { while (1) { pthread_mutex_lock(&mutex); if (strncmp(buffer, "OK", 2) == 0) { break; } printf("count = %d\n", strlen(buffer)); pthread_mutex_unlock(&mutex); sleep(1); } pthread_exit(NULL); } void main() { pthread_t id; pthread_mutex_init(&mutex, NULL); int res = pthread_create(&id, NULL, pthread_count, NULL); assert(res == 0); printf("Please input: \n"); while (1) { pthread_mutex_lock(&mutex); fgets(buffer, 127, stdin); buffer[strlen(buffer)-1] = 0; pthread_mutex_unlock(&mutex); if (strncmp(buffer, "end", 3) == 0) { exit(0); } sleep(1); } pthread_exit(NULL); }
运行结果如下:
(2)互斥锁
假设线程 A 读取变量然后给这个变量赋予一个新的值,但写操作需要两个存储器周期。当线程 B 在这两个存储器写周期中间读取这个相同的变量时,它就会得到不一致的值。为了解决这个问题,线程不得不使用锁,在同一时间只允许一个线程访问该变量。如果线程 B 希望读取变量,它首先要获取锁;同样的,当线程 A 更新变量时,也需要获取这把同样的锁。因而线程 B 在线程 A 释放以前不能读取变量。
可以通过使用 pthread 的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源之前对互斥量进行加锁,在访问完成之后释放互斥量的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量进行加锁的线程将会被阻塞值到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到
概念:完全控制临界资源,如果一个线程完成加锁操作,则其他线程无论如何都无法再完成加锁,也就无法对临界资源进行访问
初始化:int pthread_mutex_init(pthread_mutex_t *mutex, pyhread_mutex_attr_t *attr);
加锁:int pthread_mutex_lock(pthread_mutex_t *mutex); //阻塞运行
int pthread_mutex_trylock(pthread_mutex_t *mutex); //非阻塞版本
解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);
释放:int pthread_mutex_destroy(pthread_mutex_t *mutex);
(3)条件变量
二、线程安全 ----> 可重入函数
有些库函数会使用线程间共享的数据,如果没有同步控制,线程操作就是不安全的,所以,我么们使用这样一些函数时,就必须使用其安全的版本 -----> 可重入函数
三、线程中 fork 的使用,锁的变化
在线程中调用 fork 函数,子进程只会启用调用 fork 函数的那条线程,其他线程不会启用。
子进程会继承其父进程的锁以及其锁的状态,但是父进程和子进程使用的不是同意把锁,父进程解锁并不会影响到子进程的锁,所以子进程有可能死锁!!!
pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
pthread_atfork() 函数在 fork() 之前调用,当调用 fork() 时,内部创建子进程前在父进程中会调用 prepare,内部创建子进程后,父进程会调用 parent,子进程会调用 child。
指定在 fork 调用之后,创建子进程之前,调用 prepare 函数,获取所有的锁,然后创建子进程,子进程创建以后,父进程环境中调用 parent 解所有的锁,子进程环境中调用 child 解所有的锁,然后 fork 函数再返回。这样保证了在 fork 之后,子进程拿到的所都是解锁状态,从而避免死锁。