线程-线程同步

1.线程同步

同步即协同步调,按预定的先后次序运行。

线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。


举个例子:内存中100字节,线程T1欲填入全1, 线程T2欲填入全0。但如果T1执行了50个字节失去cpuT2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0

产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。

“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。

因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。

产生这种错误的原因:

1.资源共享(独享资源则不会)

2. 调度随机(意味着数据访问会出现竞争)

3. 线程间缺乏必要的同步机制。

以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。

所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

2.互斥量mutex

Linux中提供一把互斥锁mutex(也称之为互斥量)。是一个变量

每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

资源还是共享的,线程间也还是竞争的,

但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。

但,应注意:同一时刻,只能有一个线程持有该锁。

A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。

因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

锁的这种机制其实是对于访问共享数据的一种规则和建议,如果你不按照规则取做事也是可以行的通,只不过问题是没法得到解决的。

3.主要应用函数:

pthread_mutex_init函数

pthread_mutex_destroy函数

pthread_mutex_lock函数

pthread_mutex_trylock函数

pthread_mutex_unlock函数

以上5个函数的返回值都是:成功返回0, 失败返回错误号。

pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。

pthread_mutex_t mutex; 变量mutex只有两种取值10

4.pthread_mutex_init函数

初始化一个互斥锁(互斥量) ---> 初值可看作1

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

1:传出参数,调用时应传&mutex

restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改

2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。 参APUE.12.4同步属性

1. 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g.  pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;

2. 动态初始化:局部变量应采用动态初始化。e.g.  pthread_mutex_init(&mutex, NULL)

5.pthread_mutex_destroy函数

销毁一个互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);


6.pthread_mutex_lock函数

加锁。可理解为将mutex--(或-1)

int pthread_mutex_lock(pthread_mutex_t *mutex);

7.pthread_mutex_unlock函数

解锁。可理解为将mutex ++(或+1

int pthread_mutex_unlock(pthread_mutex_t *mutex);

8.pthread_mutex_trylock函数

尝试加锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);

9.加锁与解锁

lockunlock

lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。

unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。

例如:T1 T2 T3 T4 使用一把mutex锁。T1加锁成功,其他线程均阻塞,直至T1解锁。T1解锁后,T2 T3 T4均被唤醒,并自动再次尝试加锁。

可假想mutexinit成功初值为1 lock功能是将mutex-- unlockmutex++

10.locktrylock

lock加锁失败会阻塞,等待锁释放。

trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。


在访问共享资源前加锁,访问结束后立即解锁。锁的“粒度”应越小越好。

11.死锁

1. 线程试图对同一个互斥量A加锁两次。

2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A


对于第二种死锁我们可以在第二次锁的时候调用trylock函数,假如线程1获取锁b失败则释放自己的锁,同理线程2,这样就解决了死锁的问题


//

2.读写锁

与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。

读写锁状态:

一把读写锁具备三种状态

1. 读模式下加锁状态 (读锁)

2. 写模式下加锁状态 (写锁)

3. 不加锁状态

读写锁特性:

1. 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。

2. 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。

3. 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高

读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。

读写锁非常适合于对数据结构读的次数远大于写的情况。


主要应用函数:

pthread_rwlock_init函数

pthread_rwlock_destroy函数

pthread_rwlock_rdlock函数  

pthread_rwlock_wrlock函数

pthread_rwlock_tryrdlock函数

pthread_rwlock_trywrlock函数

pthread_rwlock_unlock函数

以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。

pthread_rwlock_t类型 用于定义一个读写锁变量。

pthread_rwlock_t rwlock;

1.pthread_rwlock_init函数

初始化一把读写锁

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

2attr表读写锁属性,通常使用默认属性NULL即可

2.pthread_rwlock_destroy函数

销毁一把读写锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

3.pthread_rwlock_rdlock函数

以读方式请求读写锁。(常简称为:请求读锁)

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

4.pthread_rwlock_wrlock函数

以写方式请求读写锁。(常简称为:请求写锁)

    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

5.pthread_rwlock_unlock函数

解锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

6.pthread_rwlock_tryrdlock函数

非阻塞以读方式请求读写锁(非阻塞请求读锁)

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

7.pthread_rwlock_trywrlock函数

非阻塞以写方式请求读写锁(非阻塞请求写锁)

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

///

3.条件变量:

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。

主要应用函数:

pthread_cond_init函数

pthread_cond_destroy函数

pthread_cond_wait函数

pthread_cond_timedwait函数

pthread_cond_signal函数

pthread_cond_broadcast函数

以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。

pthread_cond_t类型 用于定义条件变量

pthread_cond_t cond;

1.pthread_cond_wait函数

阻塞等待一个条件变量

    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

函数作用:

1. 阻塞等待条件变量cond(参1)满足

2. 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);

 1.2.两步为一个原子操作。

3. 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);

2.pthread_cond_signal函数

唤醒至少一个阻塞在条件变量上的线程

int pthread_cond_signal(pthread_cond_t *cond);

3.pthread_cond_broadcast函数

唤醒全部阻塞在条件变量上的线程

int pthread_cond_broadcast(pthread_cond_t *cond);

4.pthread_cond_timedwait函数

限时等待一个条件变量

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

3 参看man sem_timedwait函数,查看struct timespec结构体。

struct timespec {

time_t tv_sec; /* seconds */

long   tv_nsec; /* nanosecondes*/ 纳秒

}

形参abstime:绝对时间。

如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。

struct timespec t = {1, 0};

pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 19701100:00:01(早已经过去) 

正确用法:

time_t cur = time(NULL); 获取当前时间

struct timespec t; 定义timespec 结构体变量t

t.tv_sec = cur+1; 定时1

pthread_cond_timedwait (&cond, &mutex, &t); 传参APUE.11.6线程同步条件变量小节

在讲解setitimer函数时我们还提到另外一种时间类型:

        struct timeval {

             time_t      tv_sec;  /* seconds */ 秒

             suseconds_t tv_usec; /* microseconds */ 微秒

        };

生产者消费者条件变量模型

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。

相关代码已经上传至github:https://github.com/itverson/Linux-System-programming

//上述为条件变量的生产者消费者模型

条件变量的优点:

相较于mutex而言,条件变量可以减少竞争。

如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。

/

信号量

进化版的互斥锁(1 --> N

由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。

信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

主要应用函数:

sem_init函数

sem_destroy函数

sem_wait函数

sem_trywait函数

sem_timedwait函数

sem_post函数

以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno(注意,它们没有pthread前缀)

sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。

sem_t sem; 规定信号量sem不能< 0。头文件<semaphore.h>

信号量基本操作:

sem_wait: 1. 信号量大于0,则信号量-- (类比pthread_mutex_lock

  | 2. 信号量等于0,造成线程阻塞

对应

  |

sem_post 将信号量++,同时唤醒阻塞在信号量上的线程 (类比pthread_mutex_unlock

但,由于sem_t的实现对用户隐藏,所以所谓的++--操作只能通过函数来实现,而不能直接++--符号。

信号量的初值,决定了占用信号量的线程的个数。

sem_init函数

初始化一个信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

1sem信号量

2pshared0用于线程间;取非0(一般为1)用于进程间

3value指定信号量初值

sem_destroy函数

销毁一个信号量

int sem_destroy(sem_t *sem);

sem_wait函数

给信号量加锁 --

int sem_wait(sem_t *sem);

sem_post函数

给信号量解锁 ++

 int sem_post(sem_t *sem);

sem_trywait函数

尝试对信号量加锁 -- (sem_wait的区别类比locktrylock)

 int sem_trywait(sem_t *sem);

sem_timedwait函数

限时尝试对信号量加锁 --

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

2abs_timeout采用的是绝对时间。

定时1秒:

time_t cur = time(NULL); 获取当前时间

struct timespec t; 定义timespec 结构体变量t

t.tv_sec = cur+1; 定时1

t.tv_nsec = t.tv_sec +100;

sem_timedwait(&sem, &t); 传参



使用信号量完成的生产者消费者模型也已经上传至github。

进程间同步

互斥量mutex

进程间也可以使用互斥锁来达到同步的目的。但应在pthread_mutex_init初始化之前修改其属性为进程间共享mutex的属性修改函数主要有以下几个。

主要应用函数:

pthread_mutexattr_t mattr 类型: 用于定义mutex锁的【属性】

pthread_mutexattr_init函数初始化一个mutex属性对象

int pthread_mutexattr_init(pthread_mutexattr_t *attr);

pthread_mutexattr_destroy函数销毁mutex属性对象 (而非销毁锁)

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

pthread_mutexattr_setpshared函数修改mutex属性。

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

2pshared取值:

线程锁:PTHREAD_PROCESS_PRIVATE (mutex的默认属性即为线程锁,进程间私有)

进程锁:PTHREAD_PROCESS_SHARED

代码上传至github了

文件锁

借助 fcntl函数来实现锁机制。 操作文件的进程没有获得锁时,可以打开,但无法执行readwrite操作。

fcntl函数: 获取、设置文件访问控制属性。

int fcntl(int fd, int cmd, ... /* arg */ );

2

F_SETLK (struct flock *) 设置文件锁(trylock

F_SETLKW (struct flock *) 设置文件锁(lockW --> wait

F_GETLK (struct flock *) 获取文件锁

3

        struct flock {

              ...

              short l_type;     锁的类型:F_RDLCK F_WRLCK F_UNLCK

              short l_whence;   偏移位置:SEEK_SETSEEK_CURSEEK_END

              off_t l_start;    起始偏移:1000

              off_t l_len;      长度:0表示整个文件加锁

              pid_t l_pid;      持有该锁的进程ID(F_GETLK only)

              ...

         };

依然遵循“读共享、写独占”特性。但!如若进程不加锁直接操作文件,依然可访问成功,但数据势必会出现混乱。

多线程中,可以使用文件锁吗?

多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量来实现的。因此,多线程中无法使用文件锁。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 您好!如果我理解正确的话,您的问题是关于JMU Java 07多线同步访问的。以下是我的回答: 多线程编程是指在一个程序中同时执行多个线程的编程技术。多线程编程在提高程序性能、增强程序交互性等方面有很大的优势,但也会引入一些问题,比如线程安全问题。 当多个线程同时访问共享资源时,可能会出现线程不安全的问题,比如数据竞争、死锁等。为了避免这些问题,我们需要使用同步机制来确保线程安全。 Java中提供了多种同步机制,比如synchronized关键字、ReentrantLock类、Semaphore类等。这些同步机制都可以用来保证多线程访问共享资源的安全性。 其中,synchronized是最常用的同步机制之一。在Java中,synchronized关键字可以用来修饰方法或代码块,以实现同步访问共享资源。synchronized可以保证在同一时间只有一个线程可以访问共享资源,从而避免了数据竞争等线程安全问题。 除了synchronized之外,Java还提供了一些其他的同步机制,比如ReentrantLock类和Semaphore类。ReentrantLock类提供了比synchronized更灵活的锁机制,可以实现可重入锁、公平锁等功能。Semaphore类可以控制同时访问共享资源的线程数量,从而避免资源被过度占用的问题。 总的来说,多线程编程是一项复杂而重要的技术,需要仔细研究和实践。在实际开发中,我们需要根据具体的需求选择合适的同步机制来确保多线程访问共享资源的安全性。 ### 回答2: 多线程编程是在当前计算机领域中最为常见的技术之一,它可以利用计算机中的多核处理器来使程序运行更加高效。但是,多线程编程中可能会出现的最大问题就是线程安全,因为线程之间可能会访问相同的资源,从而导致竞态条件。 在Java中,可以通过使用synchronized关键字来实现同步访问,从而避免线程安全问题。synchronized关键字可以用于两种不同的情形:同步方法和同步块。在同步方法中,方法是同步的,即每个线程在执行该方法时都需要获取该对象的锁,如果该锁已经被其他线程获取,则需要等待直到此锁被释放。在同步块中,需要手动指定锁,即每个线程在执行同步块时需要获取该指定锁,其他线程如果需要访问该代码块中的共享资源也需要获取该指定锁,这样就保证了该代码块中的所有共享资源的同步访问。 除了synchronized关键字外,Java还提供了其他一些同步机制来实现线程安全,如ReentrantLock类和CountDownLatch类等。ReentrantLock类可以实现更为灵活的同步访问控制,但需要手动释放锁;而CountDownLatch类则用于同步一个或多个线程,使这些线程在某个条件满足之前一直处于等待状态。 在进行多线程编程时,应该尽量避免对同步访问造成瓶颈,应该通过减小同步代码块的范围等方式来提高程序的效率。此外,多线程编程时还应该进行线程安全性的测试,以确保程序能够正确地运行。 ### 回答3: 在Java中,多线程是一种非常常见的编程方式。由于多线程的特点,对共享资源的访问会出现竞争的情况,这种竞争可能会导致数据不一致或程序异常等问题。因此,在多线程编程中,我们需要采取一些措施来保证共享资源的访问能够正确、有序地进行,这就是同步机制。 同步机制包括两种方式:锁和信号量。锁是最基本的同步机制。锁有两种类型:互斥锁(Mutex)和读写锁(ReadWriteLock)。互斥锁用于保护共享资源,保证同一时间只有一个线程可以访问它,其他线程需要等待锁释放后才能继续访问。读写锁用于读写分离场景,提高了多线程访问共享资源的并发性。读写锁支持多个线程同时读取共享资源,但只允许一个线程写入共享资源。 信号量是一种更加高级的同步机制。信号量可以用来控制并发线程数和限制访问共享资源的最大数量。在Java中,Semaphore类提供了信号量的实现。Semaphore可以控制的线程数量可以是任意的,线程可以一起执行,也可以分批执行。 除了锁和信号量,Java还提供了一些其他同步机制,比如阻塞队列、Condition等。阻塞队列是一种特殊的队列,它支持线程在插入或者删除元素时阻塞等待。Condition是一种锁的增强,它可以让线程在某个特定条件下等待或者唤醒。 在多线程编程中,使用同步机制需要注意以下几点。首先,同步机制要尽可能的保证资源访问的公平性,避免因为某些线程执行时间过长导致其他线程等待时间过长。其次,同步机制要尽可能的避免死锁的发生,尤其要注意线程之间的依赖关系。最后,同步机制的实现要尽可能地简单,避免过于复杂的代码实现带来的维护成本。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值