Python With Threading Lock
Python is a versatile programming language that provides developers with various tools and libraries to build efficient and concurrent applications. One such tool is threading lock, which allows synchronization of threads to ensure safe access to shared resources. In this article, we will explore the importance of using threading lock in Python, its benefits, types, basic usage, handling deadlocks and race conditions, and best practices. Additionally, we will provide examples and cover frequently asked questions about threading lock in Python.
Why Use Threading Lock in Python?
Multithreading is a powerful technique to improve the performance of Python programs by executing multiple tasks simultaneously. However, when multiple threads try to access and modify shared resources concurrently, problems like race conditions and data corruption may occur. Threading lock helps to address these issues by synchronizing the access to shared resources. It allows only one thread to hold the lock at a time, ensuring that no other thread can access the locked resource until it is released.
Benefits of Using Threading Lock
1. Synchronization: Threading lock provides a mechanism for preventing multiple threads from accessing shared resources simultaneously, avoiding race conditions and maintaining data integrity.
2. Resource Management: It helps in managing access to resources that can only be used by one thread at a time, such as file handles or network connections.
3. Performance Optimization: By synchronizing the access to shared resources, threading lock reduces the overhead of unnecessary context switching and improves the overall performance of the program.
4. Deadlock Prevention: Threading locks can help prevent deadlocks, which occur when two or more threads are blocked indefinitely, waiting for each other to release resources.
Understanding Threading Lock in Python
In Python, the threading module provides a built-in class called Lock to implement threading lock. A lock object can be acquired by a thread using the acquire() method. Once a thread acquires the lock, no other thread can acquire it until it is released using the release() method. It follows a “mutex” (Mutual Exclusion) pattern, allowing only one thread to proceed while other threads are blocked.
Types of Threading Lock in Python
Python provides two types of threading locks:
1. Normal Lock: It is a basic lock object that can be acquired and released by a single thread.
2. RLock (Reentrant Lock): It is a reentrant lock object that can be acquired multiple times by the same thread. It allows a thread to acquire the same lock repeatedly without causing a deadlock. However, the lock must be released the same number of times it was acquired.
Basic Usage of Threading Lock in Python
To use threading lock in Python, follow these steps:
1. Import the threading module: `import threading`
2. Create a lock object: `lock = threading.Lock()` or `lock = threading.RLock()`
3. Acquire the lock: `lock.acquire()`
4. Perform operations on shared resources.
5. Release the lock: `lock.release()`
Using Threading Lock to Synchronize Access to Shared Resources
When multiple threads need to access shared resources, synchronization is required to prevent data corruption or race conditions. Here’s an example that demonstrates the usage of threading lock to synchronize access to a shared counter variable:
counter = 0
lock = threading.Lock()
for _ in range(100000):
counter += 1
# Create 5 threads
threads = 
for _ in range(5):
thread = threading.Thread(target=increment)
# Wait for all threads to finish
for thread in threads:
print(“Counter:”, counter) # Expected output: 500000
In the above example, the lock ensures that only one thread can increment the counter at a time. By acquiring the lock before modifying the counter and releasing it afterwards, each thread can safely and sequentially increment the counter without causing conflicts.
Implementing Locking Strategies with Threading Lock in Python
Threading lock can be used to implement various locking strategies, depending on the specific requirements of your application. Some commonly used strategies include:
1. Exclusive Lock: Only one thread can acquire the lock at a time, ensuring exclusive access to a shared resource.
2. Shared Lock: Multiple threads can acquire a shared lock simultaneously, allowing concurrent read access to a shared resource but restricting write access to one thread at a time.
3. Read-Write Lock: It combines both exclusive and shared locking mechanisms, allowing multiple threads to acquire the lock simultaneously for read operations but only one thread for write operations.
Handling Deadlocks and Race Conditions with Threading Lock
Deadlocks and race conditions are common concurrency issues in multithreaded programming. While threading lock helps in preventing race conditions, it is important to be aware of potential deadlocks. Deadlocks occur when two or more threads are indefinitely waiting for each other to release resources, causing the program to halt. To avoid deadlocks, follow these best practices:
1. Acquire locks in a fixed order: If multiple locks are involved, always acquire them in the same order to prevent circular dependencies.
2. Use timeout values: Specify a timeout value while acquiring locks to avoid waiting indefinitely.
3. Limit lock holding time: Release locks as soon as they are no longer required to minimize the chance of deadlocks.
4. Avoid nested locking: If possible, avoid acquiring the same lock multiple times within the same thread to prevent deadlocks caused by reentrant locks.
5. Use synchronization primitives wisely: Consider using higher-level synchronization primitives like Condition or Semaphore when appropriate, as they may simplify thread coordination and minimize the chances of deadlocks.
Best Practices for Working with Threading Lock in Python
To make the best use of threading lock in Python, consider the following best practices:
1. Identify critical sections: Determine the portions of your code that require synchronization and apply locks only to those critical sections.
2. Limit lock scope: Acquire and release locks within the smallest possible scope to reduce lock contention and improve performance.
3. Avoid excessive locking: Avoid acquiring locks unnecessarily, as it can lead to decreased parallelism and increased contention.
4. Test for deadlocks: Regularly test and analyze your multithreaded programs for potential deadlocks using appropriate debugging tools and techniques.
5. Review and optimize: Periodically review your locking strategy and optimize it based on the specific needs and characteristics of your application.
FAQs (Frequently Asked Questions)
Q1. What is a threading lock in Python?
A threading lock is a synchronization mechanism in Python that allows multiple threads to coordinate and ensure safe access to shared resources by providing mutual exclusion.
Q2. How does threading lock prevent race conditions?
Threading lock prevents race conditions by allowing only one thread to hold the lock at a time. Other threads that attempt to acquire the lock are blocked until it is released, ensuring sequential access to shared resources.
Q3. How to install the threading module in Python?
The threading module is part of the standard library in Python, so there is no need to install it separately. It can be imported using the statement `import threading`.
Q4. How to create a lock object in Python?
A lock object can be created using the threading module. For example, `lock = threading.Lock()` creates a normal lock object, while `lock = threading.RLock()` creates a reentrant lock object.
Q5. How to stop a thread in Python?
Threads cannot be stopped forcibly in Python. Instead, it is recommended to design the thread logic in a way that it can be terminated gracefully using flags or other termination conditions.
Q6. What is the difference between Lock and RLock in Python?
The main difference between Lock and RLock in Python is that an RLock (Reentrant Lock) can be acquired multiple times by the same thread without causing a deadlock, whereas a Lock can only be acquired and released by a single thread.
Q7. How to use threading.timer in Python?
The threading.timer class is a subclass of threading.Thread that represents a timer that calls a specific function after a specified delay. It can be used to schedule the execution of a function in a separate thread after a certain period of time.
Q8. What is the “with” statement in Python threading lock?
The “with” statement in Python can be used with a threading lock to automatically acquire and release the lock within a specific code block. It ensures that the lock is always released, even if an exception occurs within the block.
Threading lock plays a crucial role in concurrent programming with Python, ensuring safe and efficient access to shared resources. By preventing race conditions and managing resource access, threading lock helps in building reliable and high-performance multithreaded applications. Understanding the types, usage, and best practices associated with threading lock empowers developers to leverage the full potential of Python’s threading capabilities while minimizing potential issues like deadlocks and race conditions.
Locking \U0026 Synchronizing Threads In Python
What Is Threading Lock () In Python?
Threading is an important concept in Python programming that allows multiple tasks to run simultaneously. When multiple threads access a shared resource concurrently, issues like resource contention and data inconsistency can arise. One common approach to address these issues is by using the threading lock() function.
The threading lock() function provides a way to synchronize access to shared resources, ensuring that only one thread can access the resource at a given time. It acts as a simple binary flag that allows a thread to either acquire or release the lock.
The basic workflow of using threading lock() involves acquiring the lock before accessing the shared resource, performing the necessary operations, and then releasing the lock. This guarantees that only one thread can access the resource at a time and prevents other threads from interfering with it.
The lock() function is part of the threading module in Python’s standard library. One common way to use it is by creating an instance of the Lock class, like this:
# Create a new lock instance
lock = threading.Lock()
Once the lock object is created, it can be used to protect critical sections of code by acquiring and releasing the lock as needed. The lock can be acquired using the `acquire()` method and released using the `release()` method.
# Acquire the lock
# Access shared resource
# perform necessary operations
# Release the lock
By using the lock() function, you can ensure that only one thread can enter the critical section at a time. If multiple threads attempt to acquire the lock simultaneously, they will be blocked until the lock is released by the currently holding thread.
The lock() function can be further customized by providing arguments to its constructor. For instance, the `blocking` argument specifies whether a thread should wait to acquire the lock or continue executing if the lock is currently held by another thread. By default, blocking is set to `True`, which means the thread will wait until the lock is available. If blocking is set to `False`, the thread will continue execution and won’t wait for the lock.
Another useful argument is `timeout`, which specifies the maximum amount of time a thread should wait to acquire the lock. If the lock is not released within the specified timeout period, the thread will move on. If no timeout value is given, the thread waits indefinitely until the lock is acquired.
Threading locks also support the context manager protocol in Python, allowing you to use them with the `with` statement. This simplifies the code and ensures that the lock is automatically released regardless of exceptions raised within the block.
# Access shared resource
# perform necessary operations
Using the lock in a `with` statement ensures that the lock is acquired at the beginning of the block and released at the end, even if an exception occurs within the block.
Q: Why do we need threading lock() in Python?
A: Threading lock() is essential to prevent concurrent access to shared resources by multiple threads. Without a lock, threads can interfere with each other, leading to data inconsistency and other issues. Locks ensure that only one thread can access the resource at a given time, avoiding such problems.
Q: What happens if a thread tries to acquire a lock that is already held by another thread?
A: If a lock is already held by another thread, the thread trying to acquire it will be blocked until the lock is released. It will wait for the lock to become available before proceeding further.
Q: Is it necessary to release a lock explicitly?
A: Yes, it is crucial to release the lock explicitly using the `release()` method. Failing to do so can cause a deadlock, where threads are stuck waiting for a lock that will never be released. Releasing the lock allows other threads to acquire it and continue execution.
Q: Can a thread acquire the same lock multiple times?
A: Yes, a thread can acquire the lock multiple times consecutively. However, it must also release the lock the same number of times it acquired it. Otherwise, other threads will be blocked from acquiring the lock.
Q: Are threading locks only necessary for multithreaded programs?
A: Threading locks are primarily used in multithreaded programs, where multiple threads access shared resources. In single-threaded programs, there is generally no need for locks as there is no concurrent access to worry about. However, they can still be used if synchronization is required for any reason.
In conclusion, threading lock() in Python is a powerful mechanism to ensure synchronization and prevent concurrent access to shared resources by multiple threads. By acquiring and releasing locks, you can control the access to critical sections of your code and avoid data inconsistencies. Understanding and correctly implementing threading locks is crucial for building reliable and efficient multithreaded programs in Python.
Is Python Lock Thread-Safe?
Python is known for its simplicity and ease of use, making it a popular programming language for a variety of applications. When it comes to multi-threaded programming, however, there are certain considerations that need to be made to ensure thread safety and avoid issues such as race conditions and deadlocks.
One of the mechanisms Python provides to manage thread safety is the “lock” object from the threading module. A lock can be used to synchronize access to shared resources, ensuring that only one thread can access the resource at a time. But the question remains, is Python lock thread-safe itself?
To understand this, let’s first clarify what “thread-safety” means. In the context of multi-threaded programming, an operation or an object is considered thread-safe if it can be used correctly by multiple threads without causing unexpected behavior or corrupting shared data.
Python’s lock object, the threading.Lock class, is designed to provide mutual exclusion, ensuring that only one thread can acquire the lock at a time. This prevents concurrent access to shared resources and helps avoid race conditions. When a thread attempts to acquire the lock and another thread already holds it, the thread will block until the lock becomes available.
The implementation of the lock object in Python is based on the underlying operating system’s primitives, such as mutexes or semaphores. This means that the behavior of the lock may vary depending on the platform or the Python interpreter being used. However, in general, the lock object provided by Python is indeed thread-safe.
The threading.Lock class in Python provides a reliable and efficient way to protect critical sections of code or shared resources. It guarantees that only one thread can execute the code protected by the lock at any given time.
Now, let’s address some frequently asked questions regarding Python lock and its thread safety:
Q: Can I use the same lock object across multiple threads?
A: Yes, the same lock object can be used across multiple threads. It provides the necessary synchronization to ensure that only one thread can acquire the lock at a time.
Q: What happens if a thread tries to acquire a lock it already holds?
A: If a thread attempts to acquire a lock that it already holds, it will successfully acquire the lock again. However, it’s important to release the lock the same number of times it was acquired to avoid deadlocks.
Q: Can I use a lock to protect multiple shared resources?
A: Yes, a lock can be used to protect multiple shared resources. However, it’s important to ensure that the same lock object is used consistently to synchronize access to all the shared resources it intends to protect.
Q: What happens if a thread forgets to release the lock?
A: If a thread forgets to release the lock, it can lead to deadlock situations where other threads are unable to acquire the lock, resulting in a program freeze. It’s important to always include a finally block to ensure the lock is released, even in the presence of exceptions.
Q: Are there any alternatives to locks in Python for thread synchronization?
A: Yes, Python provides other synchronization primitives such as Semaphores, Event objects, Condition variables, and RLock (re-entrant lock). These can be used as alternatives to locks based on the specific requirements of the multi-threaded program.
In conclusion, Python’s lock object, the threading.Lock class, is indeed thread-safe. It provides mutual exclusion and synchronization to manage access to shared resources in multi-threaded programs. However, it’s important to use locks correctly, ensuring they are acquired and released in the appropriate places to avoid race conditions and deadlocks. Python also offers other synchronization primitives as alternatives to locks, depending on the specific needs of the program.
Keywords searched by users: python with threading lock Python threading lock example, Pip install threading, Lock thread Python, Threading Python, Join thread Python, Stop thread Python, threading.timer python, With lock Python
Categories: Top 55 Python With Threading Lock
See more here: nhanvietluanvan.com
Python Threading Lock Example
Python is a versatile programming language that allows for concurrent execution of different sections of code, enabling developers to make the most of available system resources. However, without proper management, simultaneous access to shared resources can lead to data corruption and inconsistent results. To avoid such issues, Python provides various synchronization primitives, such as locks, that can be used to control access to critical sections of code. In this article, we will delve into the concept of locking and explore a practical example of using locks in Python threading.
Understanding locks in Python threading
A lock is a synchronization primitive that allows only one thread to access a given resource at a time. It provides a simple mechanism to prevent concurrent access, ensuring that only one thread can execute a critical section of code at any given time. This prevents race conditions and other related issues that might arise when multiple threads try to modify the same resource simultaneously.
Locks in Python are implemented using the `threading.Lock` class from the `threading` module. Creating a lock is as simple as instantiating an object of this class, like so:
lock = threading.Lock()
Using locks in Python threading
Once we have obtained a lock, we can use it to protect a critical section of code by acquiring and releasing the lock appropriately. To acquire a lock, we can use the `acquire` method, followed by the desired operations on the shared resource. Once the critical section is executed, we should release the lock using the `release` method to allow other threads to access the locked resource. The general structure of using locks in Python looks like this:
# Critical section
# Access shared resource
Now let’s look at an example to understand how locks work in practice.
Example: Bank Account Balance
Consider a scenario where multiple threads attempt to deposit money into a common bank account. Without proper synchronization, the account balance might become inconsistent due to simultaneous deposits. Using locks, we can ensure that only one thread accesses the account balance at a time, preventing any data corruption.
balance = 0
lock = threading.Lock()
balance += 100
print(“Deposit successful. Balance:”, balance)
# Create multiple threads
threads = 
for _ in range(5):
t = threading.Thread(target=deposit)
# Wait for all threads to finish
for t in threads:
In this example, a lock `lock` is created to control access to the `balance` variable. Each thread, when executed, acquires the lock, increases the balance by 100, and releases the lock. By using locks, we ensure that only one thread can perform the balance update at a time, preventing any inconsistencies in the final result.
FAQs about Python threading locks
Q: Can multiple threads acquire the same lock simultaneously?
A: No, a lock ensures that only one thread at a time can acquire it. If a thread tries to acquire a lock that is already held by another thread, it will be blocked until the lock is released.
Q: What happens if a thread forgets to release a lock?
A: Forgetting to release a lock can cause a deadlock, where multiple threads become permanently blocked and cannot make any progress. To avoid this, it is crucial to ensure that locks are always released, preferably using a `try`-`finally` block that guarantees lock release even if an exception occurs.
Q: Are locks the only way to synchronize threads in Python?
A: No, Python provides several other synchronization primitives, such as semaphores, conditions, and events, that can be used based on specific requirements. Locks are generally the simplest and most widely used form of synchronization.
Q: Can locks be used across different processes?
A: No, locks in Python’s threading module are limited to synchronizing threads within a single process. If inter-process synchronization is required, you should consider using other mechanisms, such as multiprocessing or third-party libraries.
Q: Are locks always necessary for thread safety?
A: No, locks are only required when there is shared mutable state accessed by multiple threads. If threads operate on independent data or immutable objects, locks may not be necessary. It is important to analyze the specific requirements of each scenario to determine the need for locks.
In conclusion, Python threading locks provide a simple yet effective way to control access to shared resources and maintain thread safety. By understanding the concepts and proper usage of locks, developers can confidently write concurrent programs that avoid race conditions and ensure data consistency.
Pip Install Threading
The world of computer programming constantly evolves with new tools and libraries emerging to make developers’ lives easier. One such library is threading, which allows Python programmers to achieve parallelism by executing multiple threads simultaneously. In this article, we will explore the intricacies of threading and its installation process using pip, a Python package installer.
So, let’s dive into the exciting world of threading!
Understanding Threading in Python
Threading is a technique that allows multiple threads of execution within a single process. In simple terms, it enables different parts of a program to run concurrently, increasing efficiency and performance. As Python is an interpretive language, it traditionally uses a Global Interpreter Lock (GIL) to ensure thread safety and prevent simultaneous access to shared objects.
Although the GIL can limit the full potential of threading, it’s still useful in certain scenarios. These include tasks that involve I/O operations, where the GIL is released, allowing other threads to execute while one thread is waiting for the I/O operation to complete. This makes threading an ideal choice when dealing with network requests, file operations, and handling GUI events.
Pip: The Python Package Installer
Before we proceed, let’s understand the role of pip and how it simplifies the installation of Python packages, including threading. Pip, which stands for “pip installs packages,” is the default package installer for Python. With a simple pip command, developers can effortlessly download and install packages from the Python Package Index (PyPI) or any other source.
Installing Threading with Pip
To install the threading library using pip, follow these steps:
Step 1: Open your command prompt or terminal window.
Step 2: Type the following command and hit enter:
`pip install threading`
Pip will automatically download and install the threading library along with any necessary dependencies.
Frequently Asked Questions (FAQs)
1. Why should I use threading in Python?
Using threading in Python can significantly improve the execution time of programs that involve tasks with a significant amount of waiting, such as network I/O, file operations, or waiting for user input. It allows you to write more efficient and responsive programs by effectively utilizing system resources.
2. Can threading be used for CPU-bound tasks?
Threading in Python may not provide the desired performance improvement for CPU-bound tasks. Due to the GIL, only one thread can be executed at a time, limiting the potential benefits of threading in scenarios where the tasks mostly involve intensive CPU operations. In such cases, using multiprocessing or asynchronous techniques like asyncio might be more suitable.
3. How do I create a thread in Python?
Python’s threading module provides a Thread class that allows you to create and manage threads. To create a thread, you can simply subclass the Thread class and override the run() method with the desired functionality. Alternatively, you can use the Thread(target=func) constructor to specify a target function for the thread.
4. Can I share data between threads?
Yes, threads can share data by using shared objects. However, when multiple threads access shared data simultaneously, you need to ensure thread-safety or use synchronization mechanisms like locks to prevent data corruption. Python provides various synchronization primitives, such as Locks, Semaphores, and Conditions.
5. What are some common pitfalls of using threading in Python?
One common pitfall is the use of global variables, which can lead to unexpected behavior when accessed by different threads simultaneously. Another issue is race conditions, which occur when the outcome of a program depends on the relative timing of events between multiple threads. Proper synchronization and careful handling of shared data can help mitigate these pitfalls.
6. Can I terminate a thread?
Python’s threading module does not provide an explicit way to terminate threads. However, you can gracefully stop a thread by using flags or shared variables to control the execution of the thread. Alternatively, you can set the thread as a daemon thread, which will automatically terminate when the main program exits.
Threading is a powerful technique in Python that allows developers to write efficient, concurrent programs. By utilizing the pip installer, you can easily install the threading library and explore its capabilities firsthand. Whether it’s for I/O-bound tasks or handling GUI events, threading can make your Python programs faster and more responsive. However, it’s important to be aware of the limitations imposed by the Global Interpreter Lock and choose appropriate parallelism techniques like multiprocessing for CPU-bound operations.
Lock Thread Python
Threads are lightweight and independent units of execution within a single process. They allow concurrent execution of multiple tasks, which can increase the efficiency and responsiveness of an application. However, when multiple threads access shared resources simultaneously, it can lead to data corruption or inconsistent results. This is where threading synchronization techniques, such as locks, come into play.
A thread lock, often simply called a lock, is a synchronization primitive used to control access to a shared resource. It allows multiple threads to take turns accessing the resource exclusively, thus preventing conflicts and data corruption. In Python, the threading module provides the Lock class, which can be used to implement thread locking functionality.
To use the Lock class, you first need to import the threading module:
Next, you can create an instance of the Lock class:
lock = threading.Lock()
Once you have a lock object, you can make use of its methods to control access to the shared resource. The two most commonly used methods are `acquire()` and `release()`. The `acquire()` method is used to acquire a lock, blocking the thread if the lock is already held by another thread. The `release()` method is used to release the lock, allowing other threads to acquire it.
Here’s an example that demonstrates how to use a lock:
lock = threading.Lock()
shared_resource = 0
shared_resource += 1
shared_resource -= 1
# Create threads
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=decrement)
# Start threads
# Wait for threads to finish
print(shared_resource) # Output: 0
In the above example, two functions, `increment()` and `decrement()`, are defined. Both functions are trying to modify the `shared_resource` variable. To achieve thread safety, a lock is used to synchronize access to the `shared_resource` variable. The `with` statement is used to acquire and release the lock automatically, ensuring that the lock is always released, even in the case of an exception.
Now let’s move on to the FAQs section to address some common questions related to locking threads in Python:
**Q: Why do we need to lock threads?**
A: Locking threads ensures that only one thread can access a shared resource at a time, preventing data corruption and inconsistent results. It allows for thread safety and avoids race conditions.
**Q: How does a lock work?**
A: A lock object has two states: locked and unlocked. When a thread wants to access a shared resource, it attempts to acquire the lock. If the lock is in the unlocked state, the thread acquires the lock and continues its execution. If the lock is in the locked state, the thread blocks and waits until the lock is released by another thread.
**Q: What is the difference between a lock and a semaphore?**
A: While both locks and semaphores are used for synchronization, the main difference lies in the number of threads that can access the resource simultaneously. A lock allows only one thread to access the resource at a time, whereas a semaphore allows multiple threads to access the resource simultaneously, up to a certain limit defined during initialization.
**Q: Can deadlock occur when using locks?**
A: Yes, deadlocks can occur if locks are not used correctly. Deadlock happens when two or more threads are waiting for each other to release locks, resulting in a situation where none of the threads can proceed. To avoid deadlocks, it’s important to ensure proper acquisition and release of locks in a consistent order.
**Q: Are locks necessary for every shared resource?**
A: Locks are not necessary for every shared resource. If a resource is read-only or can be accessed without causing conflicts or inconsistency, locks may not be required. Locks are mainly needed when multiple threads can potentially write or modify a shared resource simultaneously.
In conclusion, locking threads in Python is a crucial technique for achieving thread safety and avoiding data corruption. By using the Lock class from the threading module, you can control access to shared resources and ensure that only one thread can access the resource at a time. Remember to handle locks correctly and be mindful of potential deadlocks to maximize the efficiency and reliability of your multi-threaded applications.
Images related to the topic python with threading lock
Found 7 images related to python with threading lock theme
Article link: python with threading lock.
Learn more about the topic python with threading lock.
- Python threading. How do I lock a thread? – Stack Overflow
- threading — Thread-based parallelism — Python 3.11.4 …
- How to Use Python Threading Lock to Prevent Race Conditions
- How do I use locks in Python to ensure thread safety? – Gitnux Blog
- What Is the Python Global Interpreter Lock (GIL)?
- Python: Difference between Lock and Rlock objects – GeeksforGeeks
- How to Use Python Threading Lock to Prevent Race Conditions
- What are locks in Python? – Educative.io
- How do I implement a threading lock in Python? – Gitnux Blog
See more: https://nhanvietluanvan.com/luat-hoc